diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:35:18 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 15:35:18 +0000 |
commit | b750101eb236130cf056c675997decbac904cc49 (patch) | |
tree | a5df1a06754bdd014cb975c051c83b01c9a97532 /src/resolve | |
parent | Initial commit. (diff) | |
download | systemd-b750101eb236130cf056c675997decbac904cc49.tar.xz systemd-b750101eb236130cf056c675997decbac904cc49.zip |
Adding upstream version 252.22.upstream/252.22upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/resolve')
95 files changed, 42885 insertions, 0 deletions
diff --git a/src/resolve/RFCs b/src/resolve/RFCs new file mode 100644 index 0000000..7190c16 --- /dev/null +++ b/src/resolve/RFCs @@ -0,0 +1,60 @@ +Y = Comprehensively Implemented, to the point appropriate for resolved +D = Comprehensively Implemented, by a dependency of resolved +! = Missing and something we might want to implement +~ = Needs no explicit support or doesn't apply +? = Is this relevant today? + = We are working on this + +Y https://tools.ietf.org/html/rfc1034 → DOMAIN NAMES - CONCEPTS AND FACILITIES +Y https://tools.ietf.org/html/rfc1035 → DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION +? https://tools.ietf.org/html/rfc1101 → DNS Encoding of Network Names and Other Types +Y https://tools.ietf.org/html/rfc1123 → Requirements for Internet Hosts — Application and Support +~ https://tools.ietf.org/html/rfc1464 → Using the Domain Name System To Store Arbitrary String Attributes +Y https://tools.ietf.org/html/rfc1536 → Common DNS Implementation Errors and Suggested Fixes +Y https://tools.ietf.org/html/rfc1876 → A Means for Expressing Location Information in the Domain Name System +Y https://tools.ietf.org/html/rfc2181 → Clarifications to the DNS Specification +Y https://tools.ietf.org/html/rfc2308 → Negative Caching of DNS Queries (DNS NCACHE) +Y https://tools.ietf.org/html/rfc2782 → A DNS RR for specifying the location of services (DNS SRV) +D https://tools.ietf.org/html/rfc3492 → Punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA) +Y https://tools.ietf.org/html/rfc3596 → DNS Extensions to Support IP Version 6 +Y https://tools.ietf.org/html/rfc3597 → Handling of Unknown DNS Resource Record (RR) Types +Y https://tools.ietf.org/html/rfc4033 → DNS Security Introduction and Requirements +Y https://tools.ietf.org/html/rfc4034 → Resource Records for the DNS Security Extensions +Y https://tools.ietf.org/html/rfc4035 → Protocol Modifications for the DNS Security Extensions +! https://tools.ietf.org/html/rfc4183 → A Suggested Scheme for DNS Resolution of Networks and Gateways +Y https://tools.ietf.org/html/rfc4255 → Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints +Y https://tools.ietf.org/html/rfc4343 → Domain Name System (DNS) Case Insensitivity Clarification +~ https://tools.ietf.org/html/rfc4470 → Minimally Covering NSEC Records and DNSSEC On-line Signing +Y https://tools.ietf.org/html/rfc4501 → Domain Name System Uniform Resource Identifiers +Y https://tools.ietf.org/html/rfc4509 → Use of SHA-256 in DNSSEC Delegation Signer (DS) Resource Records (RRs) +~ https://tools.ietf.org/html/rfc4592 → The Role of Wildcards in the Domain Name System +~ https://tools.ietf.org/html/rfc4697 → Observed DNS Resolution Misbehavior +Y https://tools.ietf.org/html/rfc4795 → Link-Local Multicast Name Resolution (LLMNR) +Y https://tools.ietf.org/html/rfc5011 → Automated Updates of DNS Security (DNSSEC) Trust Anchors +Y https://tools.ietf.org/html/rfc5155 → DNS Security (DNSSEC) Hashed Authenticated Denial of Existence +Y https://tools.ietf.org/html/rfc5452 → Measures for Making DNS More Resilient against Forged Answers +Y https://tools.ietf.org/html/rfc5702 → Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource Records for DNSSEC +Y https://tools.ietf.org/html/rfc5890 → Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework +Y https://tools.ietf.org/html/rfc5891 → Internationalized Domain Names in Applications (IDNA): Protocol +Y https://tools.ietf.org/html/rfc5966 → DNS Transport over TCP - Implementation Requirements +Y https://tools.ietf.org/html/rfc6303 → Locally Served DNS Zones +Y https://tools.ietf.org/html/rfc6604 → xNAME RCODE and Status Bits Clarification +Y https://tools.ietf.org/html/rfc6605 → Elliptic Curve Digital Signature Algorithm (DSA) for DNSSEC + https://tools.ietf.org/html/rfc6672 → DNAME Redirection in the DNS +! https://tools.ietf.org/html/rfc6731 → Improved Recursive DNS Server Selection for Multi-Interfaced Nodes +Y https://tools.ietf.org/html/rfc6761 → Special-Use Domain Names + https://tools.ietf.org/html/rfc6762 → Multicast DNS + https://tools.ietf.org/html/rfc6763 → DNS-Based Service Discovery +~ https://tools.ietf.org/html/rfc6781 → DNSSEC Operational Practices, Version 2 +Y https://tools.ietf.org/html/rfc6840 → Clarifications and Implementation Notes for DNS Security (DNSSEC) +Y https://tools.ietf.org/html/rfc6891 → Extension Mechanisms for DNS (EDNS(0)) +Y https://tools.ietf.org/html/rfc6944 → Applicability Statement: DNS Security (DNSSEC) DNSKEY Algorithm Implementation Status +Y https://tools.ietf.org/html/rfc6975 → Signaling Cryptographic Algorithm Understanding in DNS Security Extensions (DNSSEC) +Y https://tools.ietf.org/html/rfc7129 → Authenticated Denial of Existence in the DNS +Y https://tools.ietf.org/html/rfc7646 → Definition and Use of DNSSEC Negative Trust Anchors +~ https://tools.ietf.org/html/rfc7719 → DNS Terminology +Y https://tools.ietf.org/html/rfc8080 → Edwards-Curve Digital Security Algorithm (EdDSA) for DNSSEC + +Also relevant: + + https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-statement-dotless-domains-considered-harmful/ diff --git a/src/resolve/dns-type.c b/src/resolve/dns-type.c new file mode 100644 index 0000000..da68b41 --- /dev/null +++ b/src/resolve/dns-type.c @@ -0,0 +1,316 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/socket.h> +#include <errno.h> + +#include "dns-type.h" +#include "parse-util.h" +#include "string-util.h" + +typedef const struct { + uint16_t type; + const char *name; +} dns_type; + +static const struct dns_type_name * +lookup_dns_type (register const char *str, register GPERF_LEN_TYPE len); + +#include "dns_type-from-name.h" +#include "dns_type-to-name.h" + +int dns_type_from_string(const char *s) { + const struct dns_type_name *sc; + + assert(s); + + sc = lookup_dns_type(s, strlen(s)); + if (sc) + return sc->id; + + s = startswith_no_case(s, "TYPE"); + if (s) { + unsigned x; + + if (safe_atou(s, &x) >= 0 && + x <= UINT16_MAX) + return (int) x; + } + + return _DNS_TYPE_INVALID; +} + +bool dns_type_is_pseudo(uint16_t type) { + + /* Checks whether the specified type is a "pseudo-type". What + * a "pseudo-type" precisely is, is defined only very weakly, + * but apparently entails all RR types that are not actually + * stored as RRs on the server and should hence also not be + * cached. We use this list primarily to validate NSEC type + * bitfields, and to verify what to cache. */ + + return IN_SET(type, + 0, /* A Pseudo RR type, according to RFC 2931 */ + DNS_TYPE_ANY, + DNS_TYPE_AXFR, + DNS_TYPE_IXFR, + DNS_TYPE_OPT, + DNS_TYPE_TSIG, + DNS_TYPE_TKEY + ); +} + +bool dns_class_is_pseudo(uint16_t class) { + return class == DNS_CLASS_ANY; +} + +bool dns_type_is_valid_query(uint16_t type) { + + /* The types valid as questions in packets */ + + return !IN_SET(type, + 0, + DNS_TYPE_OPT, + DNS_TYPE_TSIG, + DNS_TYPE_TKEY, + + /* RRSIG are technically valid as questions, but we refuse doing explicit queries for them, as + * they aren't really payload, but signatures for payload, and cannot be validated on their + * own. After all they are the signatures, and have no signatures of their own validating + * them. */ + DNS_TYPE_RRSIG); +} + +bool dns_type_is_zone_transer(uint16_t type) { + + /* Zone transfers, either normal or incremental */ + + return IN_SET(type, + DNS_TYPE_AXFR, + DNS_TYPE_IXFR); +} + +bool dns_type_is_valid_rr(uint16_t type) { + + /* The types valid as RR in packets (but not necessarily + * stored on servers). */ + + return !IN_SET(type, + DNS_TYPE_ANY, + DNS_TYPE_AXFR, + DNS_TYPE_IXFR); +} + +bool dns_class_is_valid_rr(uint16_t class) { + return class != DNS_CLASS_ANY; +} + +bool dns_type_may_redirect(uint16_t type) { + /* The following record types should never be redirected using + * CNAME/DNAME RRs. See + * <https://tools.ietf.org/html/rfc4035#section-2.5>. */ + + if (dns_type_is_pseudo(type)) + return false; + + return !IN_SET(type, + DNS_TYPE_CNAME, + DNS_TYPE_DNAME, + DNS_TYPE_NSEC3, + DNS_TYPE_NSEC, + DNS_TYPE_RRSIG, + DNS_TYPE_NXT, + DNS_TYPE_SIG, + DNS_TYPE_KEY); +} + +bool dns_type_may_wildcard(uint16_t type) { + + /* The following records may not be expanded from wildcard RRsets */ + + if (dns_type_is_pseudo(type)) + return false; + + return !IN_SET(type, + DNS_TYPE_NSEC3, + DNS_TYPE_SOA, + + /* Prohibited by https://tools.ietf.org/html/rfc4592#section-4.4 */ + DNS_TYPE_DNAME); +} + +bool dns_type_apex_only(uint16_t type) { + + /* Returns true for all RR types that may only appear signed in a zone apex */ + + return IN_SET(type, + DNS_TYPE_SOA, + DNS_TYPE_NS, /* this one can appear elsewhere, too, but not signed */ + DNS_TYPE_DNSKEY, + DNS_TYPE_NSEC3PARAM); +} + +bool dns_type_is_dnssec(uint16_t type) { + return IN_SET(type, + DNS_TYPE_DS, + DNS_TYPE_DNSKEY, + DNS_TYPE_RRSIG, + DNS_TYPE_NSEC, + DNS_TYPE_NSEC3, + DNS_TYPE_NSEC3PARAM); +} + +bool dns_type_is_obsolete(uint16_t type) { + return IN_SET(type, + /* Obsoleted by RFC 973 */ + DNS_TYPE_MD, + DNS_TYPE_MF, + DNS_TYPE_MAILA, + + /* Kinda obsoleted by RFC 2505 */ + DNS_TYPE_MB, + DNS_TYPE_MG, + DNS_TYPE_MR, + DNS_TYPE_MINFO, + DNS_TYPE_MAILB, + + /* RFC1127 kinda obsoleted this by recommending against its use */ + DNS_TYPE_WKS, + + /* Declared historical by RFC 6563 */ + DNS_TYPE_A6, + + /* Obsoleted by DNSSEC-bis */ + DNS_TYPE_NXT, + + /* RFC 1035 removed support for concepts that needed this from RFC 883 */ + DNS_TYPE_NULL); +} + +bool dns_type_needs_authentication(uint16_t type) { + + /* Returns true for all (non-obsolete) RR types where records are not useful if they aren't + * authenticated. I.e. everything that contains crypto keys. */ + + return IN_SET(type, + DNS_TYPE_CERT, + DNS_TYPE_SSHFP, + DNS_TYPE_IPSECKEY, + DNS_TYPE_DS, + DNS_TYPE_DNSKEY, + DNS_TYPE_TLSA, + DNS_TYPE_CDNSKEY, + DNS_TYPE_OPENPGPKEY, + DNS_TYPE_CAA); +} + +int dns_type_to_af(uint16_t t) { + switch (t) { + + case DNS_TYPE_A: + return AF_INET; + + case DNS_TYPE_AAAA: + return AF_INET6; + + case DNS_TYPE_ANY: + return AF_UNSPEC; + + default: + return -EINVAL; + } +} + +const char *dns_class_to_string(uint16_t class) { + + switch (class) { + + case DNS_CLASS_IN: + return "IN"; + + case DNS_CLASS_ANY: + return "ANY"; + } + + return NULL; +} + +int dns_class_from_string(const char *s) { + + if (!s) + return _DNS_CLASS_INVALID; + + if (strcaseeq(s, "IN")) + return DNS_CLASS_IN; + else if (strcaseeq(s, "ANY")) + return DNS_CLASS_ANY; + + return _DNS_CLASS_INVALID; +} + +const char* tlsa_cert_usage_to_string(uint8_t cert_usage) { + + switch (cert_usage) { + + case 0: + return "CA constraint"; + + case 1: + return "Service certificate constraint"; + + case 2: + return "Trust anchor assertion"; + + case 3: + return "Domain-issued certificate"; + + case 4 ... 254: + return "Unassigned"; + + case 255: + return "Private use"; + } + + return NULL; /* clang cannot count that we covered everything */ +} + +const char* tlsa_selector_to_string(uint8_t selector) { + switch (selector) { + + case 0: + return "Full Certificate"; + + case 1: + return "SubjectPublicKeyInfo"; + + case 2 ... 254: + return "Unassigned"; + + case 255: + return "Private use"; + } + + return NULL; +} + +const char* tlsa_matching_type_to_string(uint8_t selector) { + + switch (selector) { + + case 0: + return "No hash used"; + + case 1: + return "SHA-256"; + + case 2: + return "SHA-512"; + + case 3 ... 254: + return "Unassigned"; + + case 255: + return "Private use"; + } + + return NULL; +} diff --git a/src/resolve/dns-type.h b/src/resolve/dns-type.h new file mode 100644 index 0000000..c6be190 --- /dev/null +++ b/src/resolve/dns-type.h @@ -0,0 +1,162 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "macro.h" + +/* DNS record types, taken from + * http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml. + */ +enum { + /* 0 is reserved */ + DNS_TYPE_A = 0x01, + DNS_TYPE_NS, + DNS_TYPE_MD, + DNS_TYPE_MF, + DNS_TYPE_CNAME, + DNS_TYPE_SOA, + DNS_TYPE_MB, + DNS_TYPE_MG, + DNS_TYPE_MR, + DNS_TYPE_NULL, + DNS_TYPE_WKS, + DNS_TYPE_PTR, + DNS_TYPE_HINFO, + DNS_TYPE_MINFO, + DNS_TYPE_MX, + DNS_TYPE_TXT, + DNS_TYPE_RP, + DNS_TYPE_AFSDB, + DNS_TYPE_X25, + DNS_TYPE_ISDN, + DNS_TYPE_RT, + DNS_TYPE_NSAP, + DNS_TYPE_NSAP_PTR, + DNS_TYPE_SIG, + DNS_TYPE_KEY, + DNS_TYPE_PX, + DNS_TYPE_GPOS, + DNS_TYPE_AAAA, + DNS_TYPE_LOC, + DNS_TYPE_NXT, + DNS_TYPE_EID, + DNS_TYPE_NIMLOC, + DNS_TYPE_SRV, + DNS_TYPE_ATMA, + DNS_TYPE_NAPTR, + DNS_TYPE_KX, + DNS_TYPE_CERT, + DNS_TYPE_A6, + DNS_TYPE_DNAME, + DNS_TYPE_SINK, + DNS_TYPE_OPT, /* EDNS0 option */ + DNS_TYPE_APL, + DNS_TYPE_DS, + DNS_TYPE_SSHFP, + DNS_TYPE_IPSECKEY, + DNS_TYPE_RRSIG, + DNS_TYPE_NSEC, + DNS_TYPE_DNSKEY, + DNS_TYPE_DHCID, + DNS_TYPE_NSEC3, + DNS_TYPE_NSEC3PARAM, + DNS_TYPE_TLSA, + DNS_TYPE_SMIMEA, /* RFC 8162 */ + /* 0x36 (54) is not assigned */ + DNS_TYPE_HIP = 0x37, + DNS_TYPE_NINFO, + DNS_TYPE_RKEY, + DNS_TYPE_TALINK, + DNS_TYPE_CDS, + DNS_TYPE_CDNSKEY, + DNS_TYPE_OPENPGPKEY, + DNS_TYPE_CSYNC, + DNS_TYPE_ZONEMD, + DNS_TYPE_SVCB, /* RFC 9460 */ + DNS_TYPE_HTTPS, /* RFC 9460 */ + /* 0x42…0x62 (66…98) are not assigned */ + DNS_TYPE_SPF = 0x63, + DNS_TYPE_UINFO, + DNS_TYPE_UID, + DNS_TYPE_GID, + DNS_TYPE_UNSPEC, + DNS_TYPE_NID, + DNS_TYPE_L32, + DNS_TYPE_L64, + DNS_TYPE_LP, + DNS_TYPE_EUI48, + DNS_TYPE_EUI64, + /* 0x6e…0xf8 (110…248) are not assigned */ + DNS_TYPE_TKEY = 0xF9, + DNS_TYPE_TSIG, + DNS_TYPE_IXFR, + DNS_TYPE_AXFR, + DNS_TYPE_MAILB, + DNS_TYPE_MAILA, + DNS_TYPE_ANY, + DNS_TYPE_URI, + DNS_TYPE_CAA, + DNS_TYPE_AVC, + DNS_TYPE_DOA, + DNS_TYPE_AMTRELAY, + DNS_TYPE_RESINFO, + /* 0x106…0x7fff (262…32767) are not assigned */ + DNS_TYPE_TA = 0x8000, + DNS_TYPE_DLV, + /* 32770…65279 are not assigned */ + /* 65280…65534 are for private use */ + /* 65535 is reserved */ + _DNS_TYPE_MAX, + _DNS_TYPE_INVALID = -EINVAL, +}; + +assert_cc(DNS_TYPE_SMIMEA == 53); +assert_cc(DNS_TYPE_HTTPS == 65); +assert_cc(DNS_TYPE_EUI64 == 109); +assert_cc(DNS_TYPE_RESINFO == 261); +assert_cc(DNS_TYPE_ANY == 255); + +/* DNS record classes, see RFC 1035 */ +enum { + DNS_CLASS_IN = 0x01, + DNS_CLASS_ANY = 0xFF, + + _DNS_CLASS_MAX, + _DNS_CLASS_INVALID = -EINVAL, +}; + +#define _DNS_CLASS_STRING_MAX (sizeof "CLASS" + DECIMAL_STR_MAX(uint16_t)) +#define _DNS_TYPE_STRING_MAX (sizeof "CLASS" + DECIMAL_STR_MAX(uint16_t)) + +bool dns_type_is_pseudo(uint16_t type); +bool dns_type_is_valid_query(uint16_t type); +bool dns_type_is_valid_rr(uint16_t type); +bool dns_type_may_redirect(uint16_t type); +bool dns_type_is_dnssec(uint16_t type); +bool dns_type_is_obsolete(uint16_t type); +bool dns_type_may_wildcard(uint16_t type); +bool dns_type_apex_only(uint16_t type); +bool dns_type_needs_authentication(uint16_t type); +bool dns_type_is_zone_transer(uint16_t type); +int dns_type_to_af(uint16_t type); + +bool dns_class_is_pseudo(uint16_t class); +bool dns_class_is_valid_rr(uint16_t class); + +/* TYPE?? follows http://tools.ietf.org/html/rfc3597#section-5 */ +const char *dns_type_to_string(int type); +int dns_type_from_string(const char *s); + +const char *dns_class_to_string(uint16_t class); +int dns_class_from_string(const char *name); + +/* https://tools.ietf.org/html/draft-ietf-dane-protocol-23#section-7.2 */ +const char *tlsa_cert_usage_to_string(uint8_t cert_usage); + +/* https://tools.ietf.org/html/draft-ietf-dane-protocol-23#section-7.3 */ +const char *tlsa_selector_to_string(uint8_t selector); + +/* https://tools.ietf.org/html/draft-ietf-dane-protocol-23#section-7.4 */ +const char *tlsa_matching_type_to_string(uint8_t selector); + +/* https://tools.ietf.org/html/rfc6844#section-5.1 */ +#define CAA_FLAG_CRITICAL (1u << 7) diff --git a/src/resolve/dns_type-to-name.awk b/src/resolve/dns_type-to-name.awk new file mode 100644 index 0000000..92187d5 --- /dev/null +++ b/src/resolve/dns_type-to-name.awk @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +BEGIN{ + print "const char *dns_type_to_string(int type) {" + print " switch (type) {" +} +{ + printf " case DNS_TYPE_%s: return ", $1; + sub(/_/, "-"); + printf "\"%s\";\n", $1 +} +END{ + print " default: return NULL;" + print " }" + print "}" +} diff --git a/src/resolve/fuzz-dns-packet.c b/src/resolve/fuzz-dns-packet.c new file mode 100644 index 0000000..de5ee20 --- /dev/null +++ b/src/resolve/fuzz-dns-packet.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fuzz.h" +#include "memory-util.h" +#include "resolved-dns-packet.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + + if (outside_size_range(size, 0, DNS_PACKET_SIZE_MAX)) + return 0; + + assert_se(dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0); + p->size = 0; /* by default append starts after the header, undo that */ + assert_se(dns_packet_append_blob(p, data, size, NULL) >= 0); + if (size < DNS_PACKET_HEADER_SIZE) { + /* make sure we pad the packet back up to the minimum header size */ + assert_se(p->allocated >= DNS_PACKET_HEADER_SIZE); + memzero(DNS_PACKET_DATA(p) + size, DNS_PACKET_HEADER_SIZE - size); + p->size = DNS_PACKET_HEADER_SIZE; + } + (void) dns_packet_extract(p); + + return 0; +} diff --git a/src/resolve/fuzz-dns-packet.options b/src/resolve/fuzz-dns-packet.options new file mode 100644 index 0000000..678d526 --- /dev/null +++ b/src/resolve/fuzz-dns-packet.options @@ -0,0 +1,2 @@ +[libfuzzer] +max_len = 65536 diff --git a/src/resolve/fuzz-etc-hosts.c b/src/resolve/fuzz-etc-hosts.c new file mode 100644 index 0000000..050c85d --- /dev/null +++ b/src/resolve/fuzz-etc-hosts.c @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "fd-util.h" +#include "fuzz.h" +#include "resolved-etc-hosts.h" + +int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_(etc_hosts_free) EtcHosts h = {}; + + if (!getenv("SYSTEMD_LOG_LEVEL")) + log_set_max_level(LOG_CRIT); + + f = data_to_file(data, size); + assert_se(f); + + (void) etc_hosts_parse(&h, f); + + return 0; +} diff --git a/src/resolve/generate-dns_type-gperf.py b/src/resolve/generate-dns_type-gperf.py new file mode 100755 index 0000000..0d818fb --- /dev/null +++ b/src/resolve/generate-dns_type-gperf.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1-or-later + +"""Generate %-from-name.gperf from %-list.txt +""" + +import sys + +name, prefix, input = sys.argv[1:] + +print("""\ +%{ +#if __GNUC__ >= 7 +_Pragma("GCC diagnostic ignored \\"-Wimplicit-fallthrough\\"") +#endif +%}""") +print("""\ +struct {}_name {{ const char* name; int id; }}; +%null-strings +%%""".format(name)) + +for line in open(input): + line = line.rstrip() + s = line.replace('_', '-') + print("{}, {}{}".format(s, prefix, line)) diff --git a/src/resolve/generate-dns_type-list.sed b/src/resolve/generate-dns_type-list.sed new file mode 100644 index 0000000..32af08c --- /dev/null +++ b/src/resolve/generate-dns_type-list.sed @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +s/.* DNS_TYPE_(\w+).*/\1/p diff --git a/src/resolve/meson.build b/src/resolve/meson.build new file mode 100644 index 0000000..e11aefc --- /dev/null +++ b/src/resolve/meson.build @@ -0,0 +1,242 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +resolve_includes = [includes, include_directories('.')] + +basic_dns_sources = files( + 'resolved-dns-dnssec.c', + 'resolved-dns-dnssec.h', + 'resolved-dns-packet.c', + 'resolved-dns-packet.h', + 'resolved-dns-rr.c', + 'resolved-dns-rr.h', + 'resolved-dns-answer.c', + 'resolved-dns-answer.h', + 'resolved-dns-question.c', + 'resolved-dns-question.h', + 'resolved-util.c', + 'resolved-util.h', + 'dns-type.c', + 'dns-type.h', +) + +systemd_resolved_sources = files( + 'resolved-bus.c', + 'resolved-bus.h', + 'resolved-conf.c', + 'resolved-conf.h', + 'resolved-def.h', + 'resolved-dns-cache.c', + 'resolved-dns-cache.h', + 'resolved-dns-query.c', + 'resolved-dns-query.h', + 'resolved-dns-scope.c', + 'resolved-dns-scope.h', + 'resolved-dns-search-domain.c', + 'resolved-dns-search-domain.h', + 'resolved-dns-server.c', + 'resolved-dns-server.h', + 'resolved-dns-stream.c', + 'resolved-dns-stream.h', + 'resolved-dns-stub.c', + 'resolved-dns-stub.h', + 'resolved-dns-synthesize.c', + 'resolved-dns-synthesize.h', + 'resolved-dns-transaction.c', + 'resolved-dns-transaction.h', + 'resolved-dns-trust-anchor.c', + 'resolved-dns-trust-anchor.h', + 'resolved-dns-zone.c', + 'resolved-dns-zone.h', + 'resolved-dnssd-bus.c', + 'resolved-dnssd-bus.h', + 'resolved-dnssd.c', + 'resolved-dnssd.h', + 'resolved-dnstls.h', + 'resolved-etc-hosts.c', + 'resolved-etc-hosts.h', + 'resolved-link-bus.c', + 'resolved-link-bus.h', + 'resolved-link.c', + 'resolved-link.h', + 'resolved-llmnr.c', + 'resolved-llmnr.h', + 'resolved-manager.c', + 'resolved-manager.h', + 'resolved-mdns.c', + 'resolved-mdns.h', + 'resolved-resolv-conf.c', + 'resolved-resolv-conf.h', + 'resolved-socket-graveyard.c', + 'resolved-socket-graveyard.h', + 'resolved-varlink.c', + 'resolved-varlink.h', +) + +resolvectl_sources = files( + 'resolvconf-compat.c', + 'resolvconf-compat.h', + 'resolvectl.c', + 'resolvectl.h', +) + +############################################################ + +dns_type_list_txt = custom_target( + 'dns_type-list.txt', + input : ['generate-dns_type-list.sed', 'dns-type.h'], + output : 'dns_type-list.txt', + command : [sed, '-n', '-r', '-f', '@INPUT0@', '@INPUT1@'], + capture : true) + +generate_dns_type_gperf = find_program('generate-dns_type-gperf.py') + +gperf_file = custom_target( + 'dns_type-from-name.gperf', + input : dns_type_list_txt, + output : 'dns_type-from-name.gperf', + command : [generate_dns_type_gperf, 'dns_type', 'DNS_TYPE_', '@INPUT@'], + capture : true) + +basic_dns_sources += custom_target( + 'dns_type-from-name.h', + input : gperf_file, + output : 'dns_type-from-name.h', + command : [gperf, + '-L', 'ANSI-C', '-t', '--ignore-case', + '-N', 'lookup_dns_type', + '-H', 'hash_dns_type_name', + '-p', '-C', + '@INPUT@'], + capture : true) + +basic_dns_sources += custom_target( + 'dns_type-to-name.h', + input : ['dns_type-to-name.awk', dns_type_list_txt], + output : 'dns_type-to-name.h', + command : [awk, '-f', '@INPUT0@', '@INPUT1@'], + capture : true) + +libsystemd_resolve_core = static_library( + 'systemd-resolve-core', + basic_dns_sources, + include_directories : includes, + build_by_default : false) + +systemd_resolved_sources += custom_target( + 'resolved_gperf.c', + input : 'resolved-gperf.gperf', + output : 'resolved-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +systemd_resolved_sources += custom_target( + 'resolved_dnssd_gperf.c', + input : 'resolved-dnssd-gperf.gperf', + output : 'resolved-dnssd-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + +systemd_resolved_dependencies = [threads, libm] + [lib_openssl_or_gcrypt] +if conf.get('ENABLE_DNS_OVER_TLS') == 1 + if conf.get('DNS_OVER_TLS_USE_GNUTLS') == 1 + systemd_resolved_sources += files( + 'resolved-dnstls-gnutls.c', + 'resolved-dnstls-gnutls.h', + ) + systemd_resolved_dependencies += libgnutls + elif conf.get('DNS_OVER_TLS_USE_OPENSSL') == 1 + systemd_resolved_sources += files( + 'resolved-dnstls-openssl.c', + 'resolved-dnstls-openssl.h', + ) + systemd_resolved_dependencies += libopenssl + else + error('unknown dependency for supporting DNS-over-TLS') + endif +endif + +if conf.get('ENABLE_RESOLVE') == 1 + install_data('org.freedesktop.resolve1.conf', + install_dir : dbuspolicydir) + install_data('org.freedesktop.resolve1.service', + install_dir : dbussystemservicedir) + install_data('org.freedesktop.resolve1.policy', + install_dir : polkitpolicydir) + install_data('resolv.conf', + install_dir : rootlibexecdir) +endif + +custom_target( + 'resolved.conf', + input : 'resolved.conf.in', + output : 'resolved.conf', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : conf.get('ENABLE_RESOLVE') == 1 and install_sysconfdir_samples, + install_dir : pkgsysconfdir) + +############################################################ + +tests += [ + [files('test-resolve-tables.c'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm]], + + [files('test-dns-packet.c'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm]], + + [files('test-resolved-etc-hosts.c', + 'resolved-etc-hosts.c', + 'resolved-etc-hosts.h'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm]], + + [files('test-resolved-packet.c'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm]], + + [files('test-resolved-stream.c') + + basic_dns_sources + systemd_resolved_sources, + [libshared], + [lib_openssl_or_gcrypt, + libm] + + systemd_resolved_dependencies, + resolve_includes], + + [files('test-dnssec.c'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm], + [], 'HAVE_OPENSSL_OR_GCRYPT'], + + [files('test-dnssec-complex.c'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm], + [], '', 'manual'], +] + +fuzzers += [ + [files('fuzz-dns-packet.c'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm]], + [files('fuzz-etc-hosts.c', + 'resolved-etc-hosts.c', + 'resolved-etc-hosts.h'), + [libsystemd_resolve_core, + libshared], + [lib_openssl_or_gcrypt, + libm]], +] + +systemd_resolved_sources += files('resolved.c') diff --git a/src/resolve/org.freedesktop.resolve1.conf b/src/resolve/org.freedesktop.resolve1.conf new file mode 100644 index 0000000..52ea558 --- /dev/null +++ b/src/resolve/org.freedesktop.resolve1.conf @@ -0,0 +1,27 @@ +<?xml version="1.0"?> <!--*-nxml-*--> +<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN" + "https://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd"> + +<!-- + This file is part of systemd. + + 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. +--> + +<busconfig> + + <policy user="systemd-resolve"> + <allow own="org.freedesktop.resolve1"/> + <allow send_destination="org.freedesktop.resolve1"/> + <allow receive_sender="org.freedesktop.resolve1"/> + </policy> + + <policy context="default"> + <allow send_destination="org.freedesktop.resolve1"/> + <allow receive_sender="org.freedesktop.resolve1"/> + </policy> + +</busconfig> diff --git a/src/resolve/org.freedesktop.resolve1.policy b/src/resolve/org.freedesktop.resolve1.policy new file mode 100644 index 0000000..502b975 --- /dev/null +++ b/src/resolve/org.freedesktop.resolve1.policy @@ -0,0 +1,142 @@ +<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*--> +<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN" + "https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd"> + +<!-- + SPDX-License-Identifier: LGPL-2.1-or-later + + This file is part of systemd. + + 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. +--> + +<policyconfig> + + <vendor>The systemd Project</vendor> + <vendor_url>https://systemd.io</vendor_url> + + <action id="org.freedesktop.resolve1.register-service"> + <description gettext-domain="systemd">Register a DNS-SD service</description> + <message gettext-domain="systemd">Authentication is required to register a DNS-SD service</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.unregister-service"> + <description gettext-domain="systemd">Unregister a DNS-SD service</description> + <message gettext-domain="systemd">Authentication is required to unregister a DNS-SD service</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-dns-servers"> + <description gettext-domain="systemd">Set DNS servers</description> + <message gettext-domain="systemd">Authentication is required to set DNS servers.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-domains"> + <description gettext-domain="systemd">Set domains</description> + <message gettext-domain="systemd">Authentication is required to set domains.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-default-route"> + <description gettext-domain="systemd">Set default route</description> + <message gettext-domain="systemd">Authentication is required to set default route.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-llmnr"> + <description gettext-domain="systemd">Enable/disable LLMNR</description> + <message gettext-domain="systemd">Authentication is required to enable or disable LLMNR.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-mdns"> + <description gettext-domain="systemd">Enable/disable multicast DNS</description> + <message gettext-domain="systemd">Authentication is required to enable or disable multicast DNS.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-dns-over-tls"> + <description gettext-domain="systemd">Enable/disable DNS over TLS</description> + <message gettext-domain="systemd">Authentication is required to enable or disable DNS over TLS.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-dnssec"> + <description gettext-domain="systemd">Enable/disable DNSSEC</description> + <message gettext-domain="systemd">Authentication is required to enable or disable DNSSEC.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.set-dnssec-negative-trust-anchors"> + <description gettext-domain="systemd">Set DNSSEC Negative Trust Anchors</description> + <message gettext-domain="systemd">Authentication is required to set DNSSEC Negative Trust Anchors.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + + <action id="org.freedesktop.resolve1.revert"> + <description gettext-domain="systemd">Revert name resolution settings</description> + <message gettext-domain="systemd">Authentication is required to reset name resolution settings.</message> + <defaults> + <allow_any>auth_admin</allow_any> + <allow_inactive>auth_admin</allow_inactive> + <allow_active>auth_admin_keep</allow_active> + </defaults> + <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate> + </action> + +</policyconfig> diff --git a/src/resolve/org.freedesktop.resolve1.service b/src/resolve/org.freedesktop.resolve1.service new file mode 100644 index 0000000..32a04f3 --- /dev/null +++ b/src/resolve/org.freedesktop.resolve1.service @@ -0,0 +1,14 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# 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. + +[D-BUS Service] +Name=org.freedesktop.resolve1 +Exec=/bin/false +User=root +SystemdService=dbus-org.freedesktop.resolve1.service diff --git a/src/resolve/resolv.conf b/src/resolve/resolv.conf new file mode 100644 index 0000000..b4e9a96 --- /dev/null +++ b/src/resolve/resolv.conf @@ -0,0 +1,19 @@ +# This file belongs to man:systemd-resolved(8). Do not edit. +# +# This is a static resolv.conf file for connecting local clients to the +# internal DNS stub resolver of systemd-resolved. This file lists no search +# domains. +# +# Run "resolvectl status" to see details about the uplink DNS servers +# currently in use. +# +# Third party programs must not access this file directly, but only through the +# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way, +# replace this symlink by a static file or a different symlink. +# +# See man:systemd-resolved.service(8) for details about the supported modes of +# operation for /etc/resolv.conf. + +nameserver 127.0.0.53 +options edns0 trust-ad +search . diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c new file mode 100644 index 0000000..d95df29 --- /dev/null +++ b/src/resolve/resolvconf-compat.c @@ -0,0 +1,277 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <net/if.h> + +#include "alloc-util.h" +#include "def.h" +#include "dns-domain.h" +#include "extract-word.h" +#include "fileio.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "resolvconf-compat.h" +#include "resolvectl.h" +#include "resolved-def.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" + +static int resolvconf_help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("resolvectl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s -a INTERFACE < FILE\n" + "%1$s -d INTERFACE\n" + "\n" + "Register DNS server and domain configuration with systemd-resolved.\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " -a Register per-interface DNS server and domain data\n" + " -d Unregister per-interface DNS server and domain data\n" + " -f Ignore if specified interface does not exist\n" + " -x Send DNS traffic preferably over this interface\n" + "\n" + "This is a compatibility alias for the resolvectl(1) tool, providing native\n" + "command line compatibility with the resolvconf(8) tool of various Linux\n" + "distributions and BSD systems. Some options supported by other implementations\n" + "are not supported and are ignored: -m, -p, -u. Various options supported by other\n" + "implementations are not supported and will cause the invocation to fail:\n" + "-I, -i, -l, -R, -r, -v, -V, --enable-updates, --disable-updates,\n" + "--updates-are-enabled.\n" + "\nSee the %2$s for details.\n", + program_invocation_short_name, + link); + + return 0; +} + +static int parse_nameserver(const char *string) { + int r; + + assert(string); + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&string, &word, NULL, 0); + if (r < 0) + return r; + if (r == 0) + break; + + if (strv_push(&arg_set_dns, word) < 0) + return log_oom(); + + word = NULL; + } + + return 0; +} + +static int parse_search_domain(const char *string) { + int r; + + assert(string); + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&string, &word, NULL, EXTRACT_UNQUOTE); + if (r < 0) + return r; + if (r == 0) + break; + + if (strv_push(&arg_set_domain, word) < 0) + return log_oom(); + + word = NULL; + } + + return 0; +} + +int resolvconf_parse_argv(int argc, char *argv[]) { + + enum { + ARG_VERSION = 0x100, + ARG_ENABLE_UPDATES, + ARG_DISABLE_UPDATES, + ARG_UPDATES_ARE_ENABLED, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + + /* The following are specific to Debian's original resolvconf */ + { "enable-updates", no_argument, NULL, ARG_ENABLE_UPDATES }, + { "disable-updates", no_argument, NULL, ARG_DISABLE_UPDATES }, + { "updates-are-enabled", no_argument, NULL, ARG_UPDATES_ARE_ENABLED }, + {} + }; + + enum { + TYPE_REGULAR, + TYPE_PRIVATE, /* -p: Not supported, treated identically to TYPE_REGULAR */ + TYPE_EXCLUSIVE, /* -x */ + } type = TYPE_REGULAR; + + int c, r; + + assert(argc >= 0); + assert(argv); + + /* openresolv checks these environment variables */ + if (getenv("IF_EXCLUSIVE")) + type = TYPE_EXCLUSIVE; + if (getenv("IF_PRIVATE")) + type = TYPE_PRIVATE; /* not actually supported */ + + arg_mode = _MODE_INVALID; + + while ((c = getopt_long(argc, argv, "hadxpfm:uIi:l:Rr:vV", options, NULL)) >= 0) + switch (c) { + + case 'h': + return resolvconf_help(); + + case ARG_VERSION: + return version(); + + /* -a and -d is what everybody can agree on */ + case 'a': + arg_mode = MODE_SET_LINK; + break; + + case 'd': + arg_mode = MODE_REVERT_LINK; + break; + + /* The exclusive/private/force stuff is an openresolv invention, we support in some skewed way */ + case 'x': + type = TYPE_EXCLUSIVE; + break; + + case 'p': + type = TYPE_PRIVATE; /* not actually supported */ + break; + + case 'f': + arg_ifindex_permissive = true; + break; + + /* The metrics stuff is an openresolv invention we ignore (and don't really need) */ + case 'm': + log_debug("Switch -%c ignored.", c); + break; + + /* -u supposedly should "update all subscribers". We have no subscribers, hence let's make + this a NOP, and exit immediately, cleanly. */ + case 'u': + log_info("Switch -%c ignored.", c); + return 0; + + /* The following options are openresolv inventions we don't support. */ + case 'I': + case 'i': + case 'l': + case 'R': + case 'r': + case 'v': + case 'V': + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Switch -%c not supported.", c); + + /* The Debian resolvconf commands we don't support. */ + case ARG_ENABLE_UPDATES: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Switch --enable-updates not supported."); + case ARG_DISABLE_UPDATES: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Switch --disable-updates not supported."); + case ARG_UPDATES_ARE_ENABLED: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Switch --updates-are-enabled not supported."); + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_mode == _MODE_INVALID) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected either -a or -d on the command line."); + + if (optind+1 != argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Expected interface name as argument."); + + r = ifname_resolvconf_mangle(argv[optind]); + if (r <= 0) + return r; + + optind++; + + if (arg_mode == MODE_SET_LINK) { + unsigned n = 0; + + for (;;) { + _cleanup_free_ char *line = NULL; + const char *a, *l; + + r = read_line(stdin, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Failed to read from stdin: %m"); + if (r == 0) + break; + + n++; + + l = strstrip(line); + if (IN_SET(*l, '#', ';', 0)) + continue; + + a = first_word(l, "nameserver"); + if (a) { + (void) parse_nameserver(a); + continue; + } + + a = first_word(l, "domain"); + if (!a) + a = first_word(l, "search"); + if (a) { + (void) parse_search_domain(a); + continue; + } + + log_syntax(NULL, LOG_DEBUG, "stdin", n, 0, "Ignoring resolv.conf line: %s", l); + } + + if (type == TYPE_EXCLUSIVE) { + + /* If -x mode is selected, let's preferably route non-suffixed lookups to this interface. This + * somewhat matches the original -x behaviour */ + + r = strv_extend(&arg_set_domain, "~."); + if (r < 0) + return log_oom(); + + } else if (type == TYPE_PRIVATE) + log_debug("Private DNS server data not supported, ignoring."); + + if (!arg_set_dns) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "No DNS servers specified, refusing operation."); + } + + return 1; /* work to do */ +} diff --git a/src/resolve/resolvconf-compat.h b/src/resolve/resolvconf-compat.h new file mode 100644 index 0000000..33a5318 --- /dev/null +++ b/src/resolve/resolvconf-compat.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int resolvconf_parse_argv(int argc, char *argv[]); diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c new file mode 100644 index 0000000..2217a42 --- /dev/null +++ b/src/resolve/resolvectl.c @@ -0,0 +1,3658 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <locale.h> +#include <net/if.h> + +#include "sd-bus.h" +#include "sd-netlink.h" + +#include "af-list.h" +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-error.h" +#include "bus-locator.h" +#include "bus-map-properties.h" +#include "bus-message-util.h" +#include "dns-domain.h" +#include "errno-list.h" +#include "escape.h" +#include "format-table.h" +#include "format-util.h" +#include "gcrypt-util.h" +#include "hostname-util.h" +#include "json.h" +#include "main-func.h" +#include "missing_network.h" +#include "netlink-util.h" +#include "openssl-util.h" +#include "pager.h" +#include "parse-argument.h" +#include "parse-util.h" +#include "pretty-print.h" +#include "process-util.h" +#include "resolvconf-compat.h" +#include "resolvectl.h" +#include "resolved-def.h" +#include "resolved-dns-packet.h" +#include "resolved-util.h" +#include "socket-netlink.h" +#include "sort-util.h" +#include "stdio-util.h" +#include "string-table.h" +#include "strv.h" +#include "terminal-util.h" +#include "utf8.h" +#include "varlink.h" +#include "verb-log-control.h" +#include "verbs.h" + +static int arg_family = AF_UNSPEC; +static int arg_ifindex = 0; +static char *arg_ifname = NULL; +static uint16_t arg_type = 0; +static uint16_t arg_class = 0; +static bool arg_legend = true; +static uint64_t arg_flags = 0; +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +static PagerFlags arg_pager_flags = 0; +bool arg_ifindex_permissive = false; /* If true, don't generate an error if the specified interface index doesn't exist */ +static const char *arg_service_family = NULL; + +typedef enum RawType { + RAW_NONE, + RAW_PAYLOAD, + RAW_PACKET, +} RawType; +static RawType arg_raw = RAW_NONE; + +ExecutionMode arg_mode = MODE_RESOLVE_HOST; + +char **arg_set_dns = NULL; +char **arg_set_domain = NULL; +static const char *arg_set_llmnr = NULL; +static const char *arg_set_mdns = NULL; +static const char *arg_set_dns_over_tls = NULL; +static const char *arg_set_dnssec = NULL; +static char **arg_set_nta = NULL; + +STATIC_DESTRUCTOR_REGISTER(arg_ifname, freep); +STATIC_DESTRUCTOR_REGISTER(arg_set_dns, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_set_domain, strv_freep); +STATIC_DESTRUCTOR_REGISTER(arg_set_nta, strv_freep); + +typedef enum StatusMode { + STATUS_ALL, + STATUS_DNS, + STATUS_DOMAIN, + STATUS_DEFAULT_ROUTE, + STATUS_LLMNR, + STATUS_MDNS, + STATUS_PRIVATE, + STATUS_DNSSEC, + STATUS_NTA, +} StatusMode; + +typedef struct InterfaceInfo { + int index; + const char *name; +} InterfaceInfo; + +static int interface_info_compare(const InterfaceInfo *a, const InterfaceInfo *b) { + int r; + + r = CMP(a->index, b->index); + if (r != 0) + return r; + + return strcmp_ptr(a->name, b->name); +} + +int ifname_mangle_full(const char *s, bool drop_protocol_specifier) { + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + _cleanup_strv_free_ char **found = NULL; + int r; + + assert(s); + + if (drop_protocol_specifier) { + _cleanup_free_ char *buf = NULL; + int ifindex_longest_name = -ENODEV; + + /* When invoked as resolvconf, drop the protocol specifier(s) at the end. */ + + buf = strdup(s); + if (!buf) + return log_oom(); + + for (;;) { + r = rtnl_resolve_interface(&rtnl, buf); + if (r > 0) { + if (ifindex_longest_name <= 0) + ifindex_longest_name = r; + + r = strv_extend(&found, buf); + if (r < 0) + return log_oom(); + } + + char *dot = strrchr(buf, '.'); + if (!dot) + break; + + *dot = '\0'; + } + + unsigned n = strv_length(found); + if (n > 1) { + _cleanup_free_ char *joined = NULL; + + joined = strv_join(found, ", "); + log_warning("Found multiple interfaces (%s) matching with '%s'. Using '%s' (ifindex=%i).", + strna(joined), s, found[0], ifindex_longest_name); + + } else if (n == 1) { + const char *proto; + + proto = ASSERT_PTR(startswith(s, found[0])); + if (!isempty(proto)) + log_info("Dropped protocol specifier '%s' from '%s'. Using '%s' (ifindex=%i).", + proto, s, found[0], ifindex_longest_name); + } + + r = ifindex_longest_name; + } else + r = rtnl_resolve_interface(&rtnl, s); + if (r < 0) { + if (ERRNO_IS_DEVICE_ABSENT(r) && arg_ifindex_permissive) { + log_debug_errno(r, "Interface '%s' not found, but -f specified, ignoring: %m", s); + return 0; /* done */ + } + return log_error_errno(r, "Failed to resolve interface \"%s\": %m", s); + } + + if (arg_ifindex > 0 && arg_ifindex != r) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified multiple different interfaces. Refusing."); + + arg_ifindex = r; + return free_and_strdup_warn(&arg_ifname, found ? found[0] : s); /* found */ +} + +static void print_source(uint64_t flags, usec_t rtt) { + if (!arg_legend) + return; + + if (flags == 0) + return; + + printf("\n%s-- Information acquired via", ansi_grey()); + + printf(" protocol%s%s%s%s%s", + flags & SD_RESOLVED_DNS ? " DNS" :"", + flags & SD_RESOLVED_LLMNR_IPV4 ? " LLMNR/IPv4" : "", + flags & SD_RESOLVED_LLMNR_IPV6 ? " LLMNR/IPv6" : "", + flags & SD_RESOLVED_MDNS_IPV4 ? " mDNS/IPv4" : "", + flags & SD_RESOLVED_MDNS_IPV6 ? " mDNS/IPv6" : ""); + + printf(" in %s.%s\n" + "%s-- Data is authenticated: %s; Data was acquired via local or encrypted transport: %s%s\n", + FORMAT_TIMESPAN(rtt, 100), + ansi_normal(), + ansi_grey(), + yes_no(flags & SD_RESOLVED_AUTHENTICATED), + yes_no(flags & SD_RESOLVED_CONFIDENTIAL), + ansi_normal()); + + if ((flags & (SD_RESOLVED_FROM_MASK|SD_RESOLVED_SYNTHETIC)) != 0) + printf("%s-- Data from:%s%s%s%s%s%s\n", + ansi_grey(), + FLAGS_SET(flags, SD_RESOLVED_SYNTHETIC) ? " synthetic" : "", + FLAGS_SET(flags, SD_RESOLVED_FROM_CACHE) ? " cache" : "", + FLAGS_SET(flags, SD_RESOLVED_FROM_ZONE) ? " zone" : "", + FLAGS_SET(flags, SD_RESOLVED_FROM_TRUST_ANCHOR) ? " trust-anchor" : "", + FLAGS_SET(flags, SD_RESOLVED_FROM_NETWORK) ? " network" : "", + ansi_normal()); +} + +static void print_ifindex_comment(int printed_so_far, int ifindex) { + char ifname[IF_NAMESIZE]; + int r; + + if (ifindex <= 0) + return; + + r = format_ifname(ifindex, ifname); + if (r < 0) + return (void) log_warning_errno(r, "Failed to resolve interface name for index %i, ignoring: %m", ifindex); + + printf("%*s%s-- link: %s%s", + 60 > printed_so_far ? 60 - printed_so_far : 0, " ", /* Align comment to the 60th column */ + ansi_grey(), ifname, ansi_normal()); +} + +static int resolve_host_error(const char *name, int r, const sd_bus_error *error) { + if (sd_bus_error_has_name(error, BUS_ERROR_DNS_NXDOMAIN)) + return log_error_errno(r, "%s: %s", name, bus_error_message(error, r)); + + return log_error_errno(r, "%s: resolve call failed: %s", name, bus_error_message(error, r)); +} + +static int resolve_host(sd_bus *bus, const char *name) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + const char *canonical = NULL; + unsigned c = 0; + uint64_t flags; + usec_t ts; + int r; + + assert(name); + + log_debug("Resolving %s (family %s, interface %s).", name, af_to_name(arg_family) ?: "*", isempty(arg_ifname) ? "*" : arg_ifname); + + r = bus_message_new_method_call(bus, &req, bus_resolve_mgr, "ResolveHostname"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "isit", arg_ifindex, name, arg_family, arg_flags); + if (r < 0) + return bus_log_create_error(r); + + ts = now(CLOCK_MONOTONIC); + + r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply); + if (r < 0) + return resolve_host_error(name, r, &error); + + ts = now(CLOCK_MONOTONIC) - ts; + + r = sd_bus_message_enter_container(reply, 'a', "(iiay)"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_enter_container(reply, 'r', "iiay")) > 0) { + _cleanup_free_ char *pretty = NULL; + int ifindex, family, k; + union in_addr_union a; + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(reply, "i", &ifindex); + if (r < 0) + return bus_log_parse_error(r); + + sd_bus_error_free(&error); + r = bus_message_read_in_addr_auto(reply, &error, &family, &a); + if (r < 0 && !sd_bus_error_has_name(&error, SD_BUS_ERROR_INVALID_ARGS)) + return log_error_errno(r, "%s: systemd-resolved returned invalid result: %s", name, bus_error_message(&error, r)); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_INVALID_ARGS)) { + log_debug_errno(r, "%s: systemd-resolved returned invalid result, ignoring: %s", name, bus_error_message(&error, r)); + continue; + } + + r = in_addr_ifindex_to_string(family, &a, ifindex, &pretty); + if (r < 0) + return log_error_errno(r, "Failed to print address for %s: %m", name); + + k = printf("%*s%s %s%s%s", + (int) strlen(name), c == 0 ? name : "", c == 0 ? ":" : " ", + ansi_highlight(), pretty, ansi_normal()); + + print_ifindex_comment(k, ifindex); + fputc('\n', stdout); + + c++; + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read(reply, "st", &canonical, &flags); + if (r < 0) + return bus_log_parse_error(r); + + if (!streq(name, canonical)) + printf("%*s%s (%s)\n", + (int) strlen(name), c == 0 ? name : "", c == 0 ? ":" : " ", + canonical); + + if (c == 0) + return log_error_errno(SYNTHETIC_ERRNO(ESRCH), + "%s: no addresses found", name); + + print_source(flags, ts); + + return 0; +} + +static int resolve_address(sd_bus *bus, int family, const union in_addr_union *address, int ifindex) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *pretty = NULL; + uint64_t flags; + unsigned c = 0; + usec_t ts; + int r; + + assert(bus); + assert(IN_SET(family, AF_INET, AF_INET6)); + assert(address); + + if (ifindex <= 0) + ifindex = arg_ifindex; + + r = in_addr_ifindex_to_string(family, address, ifindex, &pretty); + if (r < 0) + return log_oom(); + + log_debug("Resolving %s.", pretty); + + r = bus_message_new_method_call(bus, &req, bus_resolve_mgr, "ResolveAddress"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "ii", ifindex, family); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_array(req, 'y', address, FAMILY_ADDRESS_SIZE(family)); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "t", arg_flags); + if (r < 0) + return bus_log_create_error(r); + + ts = now(CLOCK_MONOTONIC); + + r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply); + if (r < 0) + return log_error_errno(r, "%s: resolve call failed: %s", pretty, bus_error_message(&error, r)); + + ts = now(CLOCK_MONOTONIC) - ts; + + r = sd_bus_message_enter_container(reply, 'a', "(is)"); + if (r < 0) + return bus_log_create_error(r); + + while ((r = sd_bus_message_enter_container(reply, 'r', "is")) > 0) { + const char *n; + int k; + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(reply, "is", &ifindex, &n); + if (r < 0) + return r; + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return r; + + k = printf("%*s%s %s%s%s", + (int) strlen(pretty), c == 0 ? pretty : "", + c == 0 ? ":" : " ", + ansi_highlight(), n, ansi_normal()); + + print_ifindex_comment(k, ifindex); + fputc('\n', stdout); + + c++; + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read(reply, "t", &flags); + if (r < 0) + return bus_log_parse_error(r); + + if (c == 0) + return log_error_errno(SYNTHETIC_ERRNO(ESRCH), + "%s: no names found", pretty); + + print_source(flags, ts); + + return 0; +} + +static int output_rr_packet(const void *d, size_t l, int ifindex) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + int r; + + r = dns_resource_record_new_from_raw(&rr, d, l); + if (r < 0) + return log_error_errno(r, "Failed to parse RR: %m"); + + if (arg_raw == RAW_PAYLOAD) { + void *data; + ssize_t k; + + k = dns_resource_record_payload(rr, &data); + if (k < 0) + return log_error_errno(k, "Cannot dump RR: %m"); + fwrite(data, 1, k, stdout); + } else { + const char *s; + int k; + + s = dns_resource_record_to_string(rr); + if (!s) + return log_oom(); + + k = printf("%s", s); + print_ifindex_comment(k, ifindex); + fputc('\n', stdout); + } + + return 0; +} + +static int idna_candidate(const char *name, char **ret) { + _cleanup_free_ char *idnafied = NULL; + int r; + + assert(name); + assert(ret); + + r = dns_name_apply_idna(name, &idnafied); + if (r < 0) + return log_error_errno(r, "Failed to apply IDNA to name '%s': %m", name); + if (r > 0 && !streq(name, idnafied)) { + *ret = TAKE_PTR(idnafied); + return true; + } + + *ret = NULL; + return false; +} + +static bool single_label_nonsynthetic(const char *name) { + _cleanup_free_ char *first_label = NULL; + int r; + + if (!dns_name_is_single_label(name)) + return false; + + if (is_localhost(name) || is_gateway_hostname(name)) + return false; + + r = resolve_system_hostname(NULL, &first_label); + if (r < 0) { + log_warning_errno(r, "Failed to determine the hostname: %m"); + return false; + } + + return !streq(name, first_label); +} + +static int resolve_record(sd_bus *bus, const char *name, uint16_t class, uint16_t type, bool warn_missing) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *idnafied = NULL; + bool needs_authentication = false; + unsigned n = 0; + uint64_t flags; + usec_t ts; + int r; + + assert(name); + + log_debug("Resolving %s %s %s (interface %s).", name, dns_class_to_string(class), dns_type_to_string(type), isempty(arg_ifname) ? "*" : arg_ifname); + + if (dns_name_dot_suffixed(name) == 0 && single_label_nonsynthetic(name)) + log_notice("(Note that search domains are not appended when --type= is specified. " + "Please specify fully qualified domain names, or remove --type= switch from invocation in order to request regular hostname resolution.)"); + + r = idna_candidate(name, &idnafied); + if (r < 0) + return r; + if (r > 0) + log_notice("(Note that IDNA translation is not applied when --type= is specified. " + "Please specify translated domain names — i.e. '%s' — when resolving raw records, or remove --type= switch from invocation in order to request regular hostname resolution.", + idnafied); + + r = bus_message_new_method_call(bus, &req, bus_resolve_mgr, "ResolveRecord"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "isqqt", arg_ifindex, name, class, type, arg_flags); + if (r < 0) + return bus_log_create_error(r); + + ts = now(CLOCK_MONOTONIC); + + r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply); + if (r < 0) { + if (warn_missing || r != -ENXIO) + log_error("%s: resolve call failed: %s", name, bus_error_message(&error, r)); + return r; + } + + ts = now(CLOCK_MONOTONIC) - ts; + + r = sd_bus_message_enter_container(reply, 'a', "(iqqay)"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_enter_container(reply, 'r', "iqqay")) > 0) { + uint16_t c, t; + int ifindex; + const void *d; + size_t l; + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(reply, "iqq", &ifindex, &c, &t); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read_array(reply, 'y', &d, &l); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (arg_raw == RAW_PACKET) { + uint64_t u64 = htole64(l); + + fwrite(&u64, sizeof(u64), 1, stdout); + fwrite(d, 1, l, stdout); + } else { + r = output_rr_packet(d, l, ifindex); + if (r < 0) + return r; + } + + if (dns_type_needs_authentication(t)) + needs_authentication = true; + + n++; + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read(reply, "t", &flags); + if (r < 0) + return bus_log_parse_error(r); + + if (n == 0) { + if (warn_missing) + log_error("%s: no records found", name); + return -ESRCH; + } + + print_source(flags, ts); + + if ((flags & SD_RESOLVED_AUTHENTICATED) == 0 && needs_authentication) { + fflush(stdout); + + fprintf(stderr, "\n%s" + "WARNING: The resources shown contain cryptographic key data which could not be\n" + " authenticated. It is not suitable to authenticate any communication.\n" + " This is usually indication that DNSSEC authentication was not enabled\n" + " or is not available for the selected protocol or DNS servers.%s\n", + ansi_highlight_red(), + ansi_normal()); + } + + return 0; +} + +static int resolve_rfc4501(sd_bus *bus, const char *name) { + uint16_t type = 0, class = 0; + const char *p, *q, *n; + int r; + + assert(bus); + assert(name); + assert(startswith(name, "dns:")); + + /* Parse RFC 4501 dns: URIs */ + + p = name + 4; + + if (p[0] == '/') { + const char *e; + + if (p[1] != '/') + goto invalid; + + e = strchr(p + 2, '/'); + if (!e) + goto invalid; + + if (e != p + 2) + log_warning("DNS authority specification not supported; ignoring specified authority."); + + p = e + 1; + } + + q = strchr(p, '?'); + if (q) { + n = strndupa_safe(p, q - p); + q++; + + for (;;) { + const char *f; + + f = startswith_no_case(q, "class="); + if (f) { + _cleanup_free_ char *t = NULL; + const char *e; + + if (class != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "DNS class specified twice."); + + e = strchrnul(f, ';'); + t = strndup(f, e - f); + if (!t) + return log_oom(); + + r = dns_class_from_string(t); + if (r < 0) + return log_error_errno(r, "Unknown DNS class %s.", t); + + class = r; + + if (*e == ';') { + q = e + 1; + continue; + } + + break; + } + + f = startswith_no_case(q, "type="); + if (f) { + _cleanup_free_ char *t = NULL; + const char *e; + + if (type != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "DNS type specified twice."); + + e = strchrnul(f, ';'); + t = strndup(f, e - f); + if (!t) + return log_oom(); + + r = dns_type_from_string(t); + if (r < 0) + return log_error_errno(r, "Unknown DNS type %s: %m", t); + + type = r; + + if (*e == ';') { + q = e + 1; + continue; + } + + break; + } + + goto invalid; + } + } else + n = p; + + if (class == 0) + class = arg_class ?: DNS_CLASS_IN; + if (type == 0) + type = arg_type ?: DNS_TYPE_A; + + return resolve_record(bus, n, class, type, true); + +invalid: + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Invalid DNS URI: %s", name); +} + +static int verb_query(int argc, char **argv, void *userdata) { + sd_bus *bus = userdata; + int q, r = 0; + + if (arg_type != 0) + STRV_FOREACH(p, argv + 1) { + q = resolve_record(bus, *p, arg_class, arg_type, true); + if (q < 0) + r = q; + } + + else + STRV_FOREACH(p, argv + 1) { + if (startswith(*p, "dns:")) + q = resolve_rfc4501(bus, *p); + else { + int family, ifindex; + union in_addr_union a; + + q = in_addr_ifindex_from_string_auto(*p, &family, &a, &ifindex); + if (q >= 0) + q = resolve_address(bus, family, &a, ifindex); + else + q = resolve_host(bus, *p); + } + if (q < 0) + r = q; + } + + return r; +} + +static int resolve_service(sd_bus *bus, const char *name, const char *type, const char *domain) { + const char *canonical_name, *canonical_type, *canonical_domain; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + size_t indent, sz; + uint64_t flags; + const char *p; + unsigned c; + usec_t ts; + int r; + + assert(bus); + assert(domain); + + name = empty_to_null(name); + type = empty_to_null(type); + + if (name) + log_debug("Resolving service \"%s\" of type %s in %s (family %s, interface %s).", name, type, domain, af_to_name(arg_family) ?: "*", isempty(arg_ifname) ? "*" : arg_ifname); + else if (type) + log_debug("Resolving service type %s of %s (family %s, interface %s).", type, domain, af_to_name(arg_family) ?: "*", isempty(arg_ifname) ? "*" : arg_ifname); + else + log_debug("Resolving service type %s (family %s, interface %s).", domain, af_to_name(arg_family) ?: "*", isempty(arg_ifname) ? "*" : arg_ifname); + + r = bus_message_new_method_call(bus, &req, bus_resolve_mgr, "ResolveService"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "isssit", arg_ifindex, name, type, domain, arg_family, arg_flags); + if (r < 0) + return bus_log_create_error(r); + + ts = now(CLOCK_MONOTONIC); + + r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply); + if (r < 0) + return log_error_errno(r, "Resolve call failed: %s", bus_error_message(&error, r)); + + ts = now(CLOCK_MONOTONIC) - ts; + + r = sd_bus_message_enter_container(reply, 'a', "(qqqsa(iiay)s)"); + if (r < 0) + return bus_log_parse_error(r); + + indent = + (name ? strlen(name) + 1 : 0) + + (type ? strlen(type) + 1 : 0) + + strlen(domain) + 2; + + c = 0; + while ((r = sd_bus_message_enter_container(reply, 'r', "qqqsa(iiay)s")) > 0) { + uint16_t priority, weight, port; + const char *hostname, *canonical; + + r = sd_bus_message_read(reply, "qqqs", &priority, &weight, &port, &hostname); + if (r < 0) + return bus_log_parse_error(r); + + if (name) + printf("%*s%s", (int) strlen(name), c == 0 ? name : "", c == 0 ? "/" : " "); + if (type) + printf("%*s%s", (int) strlen(type), c == 0 ? type : "", c == 0 ? "/" : " "); + + printf("%*s%s %s:%u [priority=%u, weight=%u]\n", + (int) strlen(domain), c == 0 ? domain : "", + c == 0 ? ":" : " ", + hostname, port, + priority, weight); + + r = sd_bus_message_enter_container(reply, 'a', "(iiay)"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_enter_container(reply, 'r', "iiay")) > 0) { + _cleanup_free_ char *pretty = NULL; + int ifindex, family, k; + union in_addr_union a; + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(reply, "i", &ifindex); + if (r < 0) + return bus_log_parse_error(r); + + sd_bus_error_free(&error); + r = bus_message_read_in_addr_auto(reply, &error, &family, &a); + if (r < 0 && !sd_bus_error_has_name(&error, SD_BUS_ERROR_INVALID_ARGS)) + return log_error_errno(r, "%s: systemd-resolved returned invalid result: %s", name, bus_error_message(&error, r)); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + if (sd_bus_error_has_name(&error, SD_BUS_ERROR_INVALID_ARGS)) { + log_debug_errno(r, "%s: systemd-resolved returned invalid result, ignoring: %s", name, bus_error_message(&error, r)); + continue; + } + + r = in_addr_ifindex_to_string(family, &a, ifindex, &pretty); + if (r < 0) + return log_error_errno(r, "Failed to print address for %s: %m", name); + + k = printf("%*s%s", (int) indent, "", pretty); + print_ifindex_comment(k, ifindex); + fputc('\n', stdout); + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read(reply, "s", &canonical); + if (r < 0) + return bus_log_parse_error(r); + + if (!streq(hostname, canonical)) + printf("%*s(%s)\n", (int) indent, "", canonical); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + c++; + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_enter_container(reply, 'a', "ay"); + if (r < 0) + return bus_log_parse_error(r); + + while ((r = sd_bus_message_read_array(reply, 'y', (const void**) &p, &sz)) > 0) { + _cleanup_free_ char *escaped = NULL; + + escaped = cescape_length(p, sz); + if (!escaped) + return log_oom(); + + printf("%*s%s\n", (int) indent, "", escaped); + } + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_exit_container(reply); + if (r < 0) + return bus_log_parse_error(r); + + r = sd_bus_message_read(reply, "ssst", &canonical_name, &canonical_type, &canonical_domain, &flags); + if (r < 0) + return bus_log_parse_error(r); + + canonical_name = empty_to_null(canonical_name); + canonical_type = empty_to_null(canonical_type); + + if (!streq_ptr(name, canonical_name) || + !streq_ptr(type, canonical_type) || + !streq_ptr(domain, canonical_domain)) { + + printf("%*s(", (int) indent, ""); + + if (canonical_name) + printf("%s/", canonical_name); + if (canonical_type) + printf("%s/", canonical_type); + + printf("%s)\n", canonical_domain); + } + + print_source(flags, ts); + + return 0; +} + +static int verb_service(int argc, char **argv, void *userdata) { + sd_bus *bus = userdata; + + if (argc == 2) + return resolve_service(bus, NULL, NULL, argv[1]); + else if (argc == 3) + return resolve_service(bus, NULL, argv[1], argv[2]); + else + return resolve_service(bus, argv[1], argv[2], argv[3]); +} + +static int resolve_openpgp(sd_bus *bus, const char *address) { + const char *domain, *full; + int r; + _cleanup_free_ char *hashed = NULL; + + assert(bus); + assert(address); + + domain = strrchr(address, '@'); + if (!domain) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Address does not contain '@': \"%s\"", address); + if (domain == address || domain[1] == '\0') + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Address starts or ends with '@': \"%s\"", address); + domain++; + + r = string_hashsum_sha256(address, domain - 1 - address, &hashed); + if (r < 0) + return log_error_errno(r, "Hashing failed: %m"); + + strshorten(hashed, 56); + + full = strjoina(hashed, "._openpgpkey.", domain); + log_debug("Looking up \"%s\".", full); + + r = resolve_record(bus, full, + arg_class ?: DNS_CLASS_IN, + arg_type ?: DNS_TYPE_OPENPGPKEY, false); + + if (IN_SET(r, -ENXIO, -ESRCH)) { /* NXDOMAIN or NODATA? */ + hashed = mfree(hashed); + r = string_hashsum_sha224(address, domain - 1 - address, &hashed); + if (r < 0) + return log_error_errno(r, "Hashing failed: %m"); + + full = strjoina(hashed, "._openpgpkey.", domain); + log_debug("Looking up \"%s\".", full); + + return resolve_record(bus, full, + arg_class ?: DNS_CLASS_IN, + arg_type ?: DNS_TYPE_OPENPGPKEY, true); + } + + return r; +} + +static int verb_openpgp(int argc, char **argv, void *userdata) { + sd_bus *bus = userdata; + int q, r = 0; + + STRV_FOREACH(p, argv + 1) { + q = resolve_openpgp(bus, *p); + if (q < 0) + r = q; + } + + return r; +} + +static int resolve_tlsa(sd_bus *bus, const char *family, const char *address) { + const char *port; + uint16_t port_num = 443; + _cleanup_free_ char *full = NULL; + int r; + + assert(bus); + assert(address); + + port = strrchr(address, ':'); + if (port) { + r = parse_ip_port(port + 1, &port_num); + if (r < 0) + return log_error_errno(r, "Invalid port \"%s\".", port + 1); + + address = strndupa_safe(address, port - address); + } + + r = asprintf(&full, "_%u._%s.%s", + port_num, + family, + address); + if (r < 0) + return log_oom(); + + log_debug("Looking up \"%s\".", full); + + return resolve_record(bus, full, + arg_class ?: DNS_CLASS_IN, + arg_type ?: DNS_TYPE_TLSA, true); +} + +static bool service_family_is_valid(const char *s) { + return STR_IN_SET(s, "tcp", "udp", "sctp"); +} + +static int verb_tlsa(int argc, char **argv, void *userdata) { + sd_bus *bus = userdata; + char **args = argv + 1; + const char *family = "tcp"; + int q, r = 0; + + if (service_family_is_valid(argv[1])) { + family = argv[1]; + args++; + } + + STRV_FOREACH(p, args) { + q = resolve_tlsa(bus, family, *p); + if (q < 0) + r = q; + } + + return r; +} + +static int show_statistics(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(table_unrefp) Table *table = NULL; + sd_bus *bus = ASSERT_PTR(userdata); + uint64_t n_current_transactions, n_total_transactions, + cache_size, n_cache_hit, n_cache_miss, + n_dnssec_secure, n_dnssec_insecure, n_dnssec_bogus, n_dnssec_indeterminate; + int r, dnssec_supported; + + r = bus_get_property_trivial(bus, bus_resolve_mgr, "DNSSECSupported", &error, 'b', &dnssec_supported); + if (r < 0) + return log_error_errno(r, "Failed to get DNSSEC supported state: %s", bus_error_message(&error, r)); + + printf("DNSSEC supported by current servers: %s%s%s\n\n", + ansi_highlight(), + yes_no(dnssec_supported), + ansi_normal()); + + r = bus_get_property(bus, bus_resolve_mgr, "TransactionStatistics", &error, &reply, "(tt)"); + if (r < 0) + return log_error_errno(r, "Failed to get transaction statistics: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "(tt)", + &n_current_transactions, + &n_total_transactions); + if (r < 0) + return bus_log_parse_error(r); + + reply = sd_bus_message_unref(reply); + + r = bus_get_property(bus, bus_resolve_mgr, "CacheStatistics", &error, &reply, "(ttt)"); + if (r < 0) + return log_error_errno(r, "Failed to get cache statistics: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "(ttt)", + &cache_size, + &n_cache_hit, + &n_cache_miss); + if (r < 0) + return bus_log_parse_error(r); + + reply = sd_bus_message_unref(reply); + + r = bus_get_property(bus, bus_resolve_mgr, "DNSSECStatistics", &error, &reply, "(tttt)"); + if (r < 0) + return log_error_errno(r, "Failed to get DNSSEC statistics: %s", bus_error_message(&error, r)); + + r = sd_bus_message_read(reply, "(tttt)", + &n_dnssec_secure, + &n_dnssec_insecure, + &n_dnssec_bogus, + &n_dnssec_indeterminate); + if (r < 0) + return bus_log_parse_error(r); + + table = table_new("key", "value"); + if (!table) + return log_oom(); + + table_set_header(table, false); + + r = table_add_many(table, + TABLE_STRING, "Transactions", + TABLE_SET_COLOR, ansi_highlight(), + TABLE_EMPTY, + TABLE_STRING, "Current Transactions:", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_UINT64, n_current_transactions, + TABLE_STRING, "Total Transactions:", + TABLE_UINT64, n_total_transactions, + TABLE_EMPTY, TABLE_EMPTY, + TABLE_STRING, "Cache", + TABLE_SET_COLOR, ansi_highlight(), + TABLE_SET_ALIGN_PERCENT, 0, + TABLE_EMPTY, + TABLE_STRING, "Current Cache Size:", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_UINT64, cache_size, + TABLE_STRING, "Cache Hits:", + TABLE_UINT64, n_cache_hit, + TABLE_STRING, "Cache Misses:", + TABLE_UINT64, n_cache_miss, + TABLE_EMPTY, TABLE_EMPTY, + TABLE_STRING, "DNSSEC Verdicts", + TABLE_SET_COLOR, ansi_highlight(), + TABLE_SET_ALIGN_PERCENT, 0, + TABLE_EMPTY, + TABLE_STRING, "Secure:", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_UINT64, n_dnssec_secure, + TABLE_STRING, "Insecure:", + TABLE_UINT64, n_dnssec_insecure, + TABLE_STRING, "Bogus:", + TABLE_UINT64, n_dnssec_bogus, + TABLE_STRING, "Indeterminate:", + TABLE_UINT64, n_dnssec_indeterminate); + if (r < 0) + table_log_add_error(r); + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + + return 0; +} + +static int reset_statistics(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = userdata; + int r; + + r = bus_call_method(bus, bus_resolve_mgr, "ResetStatistics", &error, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to reset statistics: %s", bus_error_message(&error, r)); + + return 0; +} + +static int flush_caches(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = userdata; + int r; + + r = bus_call_method(bus, bus_resolve_mgr, "FlushCaches", &error, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to flush caches: %s", bus_error_message(&error, r)); + + return 0; +} + +static int reset_server_features(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = userdata; + int r; + + r = bus_call_method(bus, bus_resolve_mgr, "ResetServerFeatures", &error, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to reset server features: %s", bus_error_message(&error, r)); + + return 0; +} + +static int read_dns_server_one( + sd_bus_message *m, + bool with_ifindex, /* read "ifindex" reply that also carries an interface index */ + bool extended, /* read "extended" reply, i.e. with port number and server name */ + bool only_global, /* suppress entries with an (non-loopback) ifindex set (i.e. which are specific to some interface) */ + char **ret) { + + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *pretty = NULL; + union in_addr_union a; + const char *name = NULL; + int32_t ifindex = 0; + int family, r, k; + uint16_t port = 0; + + assert(m); + assert(ret); + + r = sd_bus_message_enter_container(m, 'r', with_ifindex ? (extended ? "iiayqs" : "iiay") : (extended ? "iayqs" : "iay")); + if (r <= 0) + return r; + + if (with_ifindex) { + r = sd_bus_message_read(m, "i", &ifindex); + if (r < 0) + return r; + } + + k = bus_message_read_in_addr_auto(m, &error, &family, &a); + if (k < 0 && !sd_bus_error_has_name(&error, SD_BUS_ERROR_INVALID_ARGS)) + return k; + + if (extended) { + r = sd_bus_message_read(m, "q", &port); + if (r < 0) + return r; + + r = sd_bus_message_read(m, "s", &name); + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + if (k < 0) { + log_debug("Invalid DNS server, ignoring: %s", bus_error_message(&error, k)); + *ret = NULL; + return 1; + } + + if (only_global && ifindex > 0 && ifindex != LOOPBACK_IFINDEX) { + /* This one has an (non-loopback) ifindex set, and we were told to suppress those. Hence do so. */ + *ret = NULL; + return 1; + } + + r = in_addr_port_ifindex_name_to_string(family, &a, port, ifindex, name, &pretty); + if (r < 0) + return r; + + *ret = TAKE_PTR(pretty); + + return 1; +} + +static int map_link_dns_servers_internal(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata, bool extended) { + char ***l = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', extended ? "(iayqs)" : "(iay)"); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *pretty = NULL; + + r = read_dns_server_one(m, /* with_ifindex= */ false, extended, /* only_global= */ false, &pretty); + if (r < 0) + return r; + if (r == 0) + break; + + if (isempty(pretty)) + continue; + + r = strv_consume(l, TAKE_PTR(pretty)); + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return 0; +} + +static int map_link_dns_servers(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + return map_link_dns_servers_internal(bus, member, m, error, userdata, false); +} + +static int map_link_dns_servers_ex(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + return map_link_dns_servers_internal(bus, member, m, error, userdata, true); +} + +static int map_link_current_dns_server(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + assert(m); + assert(userdata); + + return read_dns_server_one(m, /* with_ifindex= */ false, /* extended= */ false, /* only_global= */ false, userdata); +} + +static int map_link_current_dns_server_ex(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + assert(m); + assert(userdata); + + return read_dns_server_one(m, /* with_ifindex= */ false, /* extended= */ true, /* only_global= */ false, userdata); +} + +static int read_domain_one(sd_bus_message *m, bool with_ifindex, char **ret) { + _cleanup_free_ char *str = NULL; + int ifindex, route_only, r; + const char *domain; + + assert(m); + assert(ret); + + if (with_ifindex) + r = sd_bus_message_read(m, "(isb)", &ifindex, &domain, &route_only); + else + r = sd_bus_message_read(m, "(sb)", &domain, &route_only); + if (r <= 0) + return r; + + if (with_ifindex && ifindex != 0) { + /* only show the global ones here */ + *ret = NULL; + return 1; + } + + if (route_only) + str = strjoin("~", domain); + else + str = strdup(domain); + if (!str) + return -ENOMEM; + + *ret = TAKE_PTR(str); + + return 1; +} + +static int map_link_domains(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + char ***l = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', "(sb)"); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *pretty = NULL; + + r = read_domain_one(m, false, &pretty); + if (r < 0) + return r; + if (r == 0) + break; + + if (isempty(pretty)) + continue; + + r = strv_consume(l, TAKE_PTR(pretty)); + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return 0; +} + +static int status_print_strv_ifindex(int ifindex, const char *ifname, char **p) { + const unsigned indent = strlen("Global: "); /* Use the same indentation everywhere to make things nice */ + int pos1, pos2; + + if (ifname) + printf("%s%nLink %i (%s)%n%s:", ansi_highlight(), &pos1, ifindex, ifname, &pos2, ansi_normal()); + else + printf("%s%nGlobal%n%s:", ansi_highlight(), &pos1, &pos2, ansi_normal()); + + size_t cols = columns(), position = pos2 - pos1 + 2; + + STRV_FOREACH(i, p) { + size_t our_len = utf8_console_width(*i); /* This returns -1 on invalid utf-8 (which shouldn't happen). + * If that happens, we'll just print one item per line. */ + + if (position <= indent || size_add(size_add(position, 1), our_len) < cols) { + printf(" %s", *i); + position = size_add(size_add(position, 1), our_len); + } else { + printf("\n%*s%s", (int) indent, "", *i); + position = size_add(our_len, indent); + } + } + + printf("\n"); + + return 0; +} + +static int status_print_strv_global(char **p) { + return status_print_strv_ifindex(0, NULL, p); +} + +typedef struct LinkInfo { + uint64_t scopes_mask; + const char *llmnr; + const char *mdns; + const char *dns_over_tls; + const char *dnssec; + char *current_dns; + char *current_dns_ex; + char **dns; + char **dns_ex; + char **domains; + char **ntas; + bool dnssec_supported; + bool default_route; +} LinkInfo; + +typedef struct GlobalInfo { + char *current_dns; + char *current_dns_ex; + char **dns; + char **dns_ex; + char **fallback_dns; + char **fallback_dns_ex; + char **domains; + char **ntas; + const char *llmnr; + const char *mdns; + const char *dns_over_tls; + const char *dnssec; + const char *resolv_conf_mode; + bool dnssec_supported; +} GlobalInfo; + +static void link_info_clear(LinkInfo *p) { + free(p->current_dns); + free(p->current_dns_ex); + strv_free(p->dns); + strv_free(p->dns_ex); + strv_free(p->domains); + strv_free(p->ntas); +} + +static void global_info_clear(GlobalInfo *p) { + free(p->current_dns); + free(p->current_dns_ex); + strv_free(p->dns); + strv_free(p->dns_ex); + strv_free(p->fallback_dns); + strv_free(p->fallback_dns_ex); + strv_free(p->domains); + strv_free(p->ntas); +} + +static int dump_list(Table *table, const char *prefix, char * const *l) { + int r; + + if (strv_isempty(l)) + return 0; + + r = table_add_many(table, + TABLE_STRING, prefix, + TABLE_STRV_WRAPPED, l); + if (r < 0) + return table_log_add_error(r); + + return 0; +} + +static int strv_extend_extended_bool(char ***strv, const char *name, const char *value) { + int r; + + if (value) { + r = parse_boolean(value); + if (r >= 0) + return strv_extendf(strv, "%s%s", plus_minus(r), name); + } + + return strv_extendf(strv, "%s=%s", name, value ?: "???"); +} + +static char** link_protocol_status(const LinkInfo *info) { + _cleanup_strv_free_ char **s = NULL; + + if (strv_extendf(&s, "%sDefaultRoute", plus_minus(info->default_route)) < 0) + return NULL; + + if (strv_extend_extended_bool(&s, "LLMNR", info->llmnr) < 0) + return NULL; + + if (strv_extend_extended_bool(&s, "mDNS", info->mdns) < 0) + return NULL; + + if (strv_extend_extended_bool(&s, "DNSOverTLS", info->dns_over_tls) < 0) + return NULL; + + if (strv_extendf(&s, "DNSSEC=%s/%s", + info->dnssec ?: "???", + info->dnssec_supported ? "supported" : "unsupported") < 0) + return NULL; + + return TAKE_PTR(s); +} + +static char** global_protocol_status(const GlobalInfo *info) { + _cleanup_strv_free_ char **s = NULL; + + if (strv_extend_extended_bool(&s, "LLMNR", info->llmnr) < 0) + return NULL; + + if (strv_extend_extended_bool(&s, "mDNS", info->mdns) < 0) + return NULL; + + if (strv_extend_extended_bool(&s, "DNSOverTLS", info->dns_over_tls) < 0) + return NULL; + + if (strv_extendf(&s, "DNSSEC=%s/%s", + info->dnssec ?: "???", + info->dnssec_supported ? "supported" : "unsupported") < 0) + return NULL; + + return TAKE_PTR(s); +} + +static int status_ifindex(sd_bus *bus, int ifindex, const char *name, StatusMode mode, bool *empty_line) { + static const struct bus_properties_map property_map[] = { + { "ScopesMask", "t", NULL, offsetof(LinkInfo, scopes_mask) }, + { "DNS", "a(iay)", map_link_dns_servers, offsetof(LinkInfo, dns) }, + { "DNSEx", "a(iayqs)", map_link_dns_servers_ex, offsetof(LinkInfo, dns_ex) }, + { "CurrentDNSServer", "(iay)", map_link_current_dns_server, offsetof(LinkInfo, current_dns) }, + { "CurrentDNSServerEx", "(iayqs)", map_link_current_dns_server_ex, offsetof(LinkInfo, current_dns_ex) }, + { "Domains", "a(sb)", map_link_domains, offsetof(LinkInfo, domains) }, + { "DefaultRoute", "b", NULL, offsetof(LinkInfo, default_route) }, + { "LLMNR", "s", NULL, offsetof(LinkInfo, llmnr) }, + { "MulticastDNS", "s", NULL, offsetof(LinkInfo, mdns) }, + { "DNSOverTLS", "s", NULL, offsetof(LinkInfo, dns_over_tls) }, + { "DNSSEC", "s", NULL, offsetof(LinkInfo, dnssec) }, + { "DNSSECNegativeTrustAnchors", "as", bus_map_strv_sort, offsetof(LinkInfo, ntas) }, + { "DNSSECSupported", "b", NULL, offsetof(LinkInfo, dnssec_supported) }, + {} + }; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(link_info_clear) LinkInfo link_info = {}; + _cleanup_(table_unrefp) Table *table = NULL; + _cleanup_free_ char *p = NULL; + char ifi[DECIMAL_STR_MAX(int)], ifname[IF_NAMESIZE]; + int r; + + assert(bus); + assert(ifindex > 0); + + if (!name) { + r = format_ifname(ifindex, ifname); + if (r < 0) + return log_error_errno(r, "Failed to resolve interface name for %i: %m", ifindex); + + name = ifname; + } + + xsprintf(ifi, "%i", ifindex); + r = sd_bus_path_encode("/org/freedesktop/resolve1/link", ifi, &p); + if (r < 0) + return log_oom(); + + r = bus_map_all_properties(bus, + "org.freedesktop.resolve1", + p, + property_map, + BUS_MAP_BOOLEAN_AS_BOOL, + &error, + &m, + &link_info); + if (r < 0) + return log_error_errno(r, "Failed to get link data for %i: %s", ifindex, bus_error_message(&error, r)); + + pager_open(arg_pager_flags); + + if (mode == STATUS_DNS) + return status_print_strv_ifindex(ifindex, name, link_info.dns_ex ?: link_info.dns); + + if (mode == STATUS_DOMAIN) + return status_print_strv_ifindex(ifindex, name, link_info.domains); + + if (mode == STATUS_NTA) + return status_print_strv_ifindex(ifindex, name, link_info.ntas); + + if (mode == STATUS_DEFAULT_ROUTE) { + printf("%sLink %i (%s)%s: %s\n", + ansi_highlight(), ifindex, name, ansi_normal(), + yes_no(link_info.default_route)); + + return 0; + } + + if (mode == STATUS_LLMNR) { + printf("%sLink %i (%s)%s: %s\n", + ansi_highlight(), ifindex, name, ansi_normal(), + strna(link_info.llmnr)); + + return 0; + } + + if (mode == STATUS_MDNS) { + printf("%sLink %i (%s)%s: %s\n", + ansi_highlight(), ifindex, name, ansi_normal(), + strna(link_info.mdns)); + + return 0; + } + + if (mode == STATUS_PRIVATE) { + printf("%sLink %i (%s)%s: %s\n", + ansi_highlight(), ifindex, name, ansi_normal(), + strna(link_info.dns_over_tls)); + + return 0; + } + + if (mode == STATUS_DNSSEC) { + printf("%sLink %i (%s)%s: %s\n", + ansi_highlight(), ifindex, name, ansi_normal(), + strna(link_info.dnssec)); + + return 0; + } + + if (empty_line && *empty_line) + fputc('\n', stdout); + + printf("%sLink %i (%s)%s\n", + ansi_highlight(), ifindex, name, ansi_normal()); + + table = table_new("key", "value"); + if (!table) + return log_oom(); + + table_set_header(table, false); + + r = table_add_many(table, + TABLE_STRING, "Current Scopes:", + TABLE_SET_ALIGN_PERCENT, 100); + if (r < 0) + return table_log_add_error(r); + + if (link_info.scopes_mask == 0) + r = table_add_cell(table, NULL, TABLE_STRING, "none"); + else { + _cleanup_free_ char *buf = NULL; + size_t len; + + if (asprintf(&buf, "%s%s%s%s%s", + link_info.scopes_mask & SD_RESOLVED_DNS ? "DNS " : "", + link_info.scopes_mask & SD_RESOLVED_LLMNR_IPV4 ? "LLMNR/IPv4 " : "", + link_info.scopes_mask & SD_RESOLVED_LLMNR_IPV6 ? "LLMNR/IPv6 " : "", + link_info.scopes_mask & SD_RESOLVED_MDNS_IPV4 ? "mDNS/IPv4 " : "", + link_info.scopes_mask & SD_RESOLVED_MDNS_IPV6 ? "mDNS/IPv6 " : "") < 0) + return log_oom(); + + len = strlen(buf); + assert(len > 0); + buf[len - 1] = '\0'; + + r = table_add_cell(table, NULL, TABLE_STRING, buf); + } + if (r < 0) + return table_log_add_error(r); + + _cleanup_strv_free_ char **pstatus = link_protocol_status(&link_info); + if (!pstatus) + return log_oom(); + + r = table_add_many(table, + TABLE_STRING, "Protocols:", + TABLE_STRV_WRAPPED, pstatus); + if (r < 0) + return table_log_add_error(r); + + if (link_info.current_dns) { + r = table_add_many(table, + TABLE_STRING, "Current DNS Server:", + TABLE_STRING, link_info.current_dns_ex ?: link_info.current_dns); + if (r < 0) + return table_log_add_error(r); + } + + r = dump_list(table, "DNS Servers:", link_info.dns_ex ?: link_info.dns); + if (r < 0) + return r; + + r = dump_list(table, "DNS Domain:", link_info.domains); + if (r < 0) + return r; + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + + if (empty_line) + *empty_line = true; + + return 0; +} + +static int map_global_dns_servers_internal(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata, bool extended) { + char ***l = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', extended ? "(iiayqs)" : "(iiay)"); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *pretty = NULL; + + r = read_dns_server_one(m, /* with_ifindex= */ true, extended, /* only_global= */ true, &pretty); + if (r < 0) + return r; + if (r == 0) + break; + + if (isempty(pretty)) + continue; + + r = strv_consume(l, TAKE_PTR(pretty)); + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + return 0; +} + +static int map_global_dns_servers(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + return map_global_dns_servers_internal(bus, member, m, error, userdata, false); +} + +static int map_global_dns_servers_ex(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + return map_global_dns_servers_internal(bus, member, m, error, userdata, true); +} + +static int map_global_current_dns_server(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + return read_dns_server_one(m, /* with_ifindex= */ true, /* extended= */ false, /* only_global= */ true, userdata); +} + +static int map_global_current_dns_server_ex(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + return read_dns_server_one(m, /* with_ifindex= */ true, /* extended= */ true, /* only_global= */ true, userdata); +} + +static int map_global_domains(sd_bus *bus, const char *member, sd_bus_message *m, sd_bus_error *error, void *userdata) { + char ***l = ASSERT_PTR(userdata); + int r; + + assert(bus); + assert(member); + assert(m); + + r = sd_bus_message_enter_container(m, 'a', "(isb)"); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *pretty = NULL; + + r = read_domain_one(m, true, &pretty); + if (r < 0) + return r; + if (r == 0) + break; + + if (isempty(pretty)) + continue; + + r = strv_consume(l, TAKE_PTR(pretty)); + if (r < 0) + return r; + } + + r = sd_bus_message_exit_container(m); + if (r < 0) + return r; + + strv_sort(*l); + + return 0; +} + +static int status_global(sd_bus *bus, StatusMode mode, bool *empty_line) { + static const struct bus_properties_map property_map[] = { + { "DNS", "a(iiay)", map_global_dns_servers, offsetof(GlobalInfo, dns) }, + { "DNSEx", "a(iiayqs)", map_global_dns_servers_ex, offsetof(GlobalInfo, dns_ex) }, + { "FallbackDNS", "a(iiay)", map_global_dns_servers, offsetof(GlobalInfo, fallback_dns) }, + { "FallbackDNSEx", "a(iiayqs)", map_global_dns_servers_ex, offsetof(GlobalInfo, fallback_dns_ex) }, + { "CurrentDNSServer", "(iiay)", map_global_current_dns_server, offsetof(GlobalInfo, current_dns) }, + { "CurrentDNSServerEx", "(iiayqs)", map_global_current_dns_server_ex, offsetof(GlobalInfo, current_dns_ex) }, + { "Domains", "a(isb)", map_global_domains, offsetof(GlobalInfo, domains) }, + { "DNSSECNegativeTrustAnchors", "as", bus_map_strv_sort, offsetof(GlobalInfo, ntas) }, + { "LLMNR", "s", NULL, offsetof(GlobalInfo, llmnr) }, + { "MulticastDNS", "s", NULL, offsetof(GlobalInfo, mdns) }, + { "DNSOverTLS", "s", NULL, offsetof(GlobalInfo, dns_over_tls) }, + { "DNSSEC", "s", NULL, offsetof(GlobalInfo, dnssec) }, + { "DNSSECSupported", "b", NULL, offsetof(GlobalInfo, dnssec_supported) }, + { "ResolvConfMode", "s", NULL, offsetof(GlobalInfo, resolv_conf_mode) }, + {} + }; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL; + _cleanup_(global_info_clear) GlobalInfo global_info = {}; + _cleanup_(table_unrefp) Table *table = NULL; + int r; + + assert(bus); + assert(empty_line); + + r = bus_map_all_properties(bus, + "org.freedesktop.resolve1", + "/org/freedesktop/resolve1", + property_map, + BUS_MAP_BOOLEAN_AS_BOOL, + &error, + &m, + &global_info); + if (r < 0) + return log_error_errno(r, "Failed to get global data: %s", bus_error_message(&error, r)); + + pager_open(arg_pager_flags); + + if (mode == STATUS_DNS) + return status_print_strv_global(global_info.dns_ex ?: global_info.dns); + + if (mode == STATUS_DOMAIN) + return status_print_strv_global(global_info.domains); + + if (mode == STATUS_NTA) + return status_print_strv_global(global_info.ntas); + + if (mode == STATUS_LLMNR) { + printf("%sGlobal%s: %s\n", ansi_highlight(), ansi_normal(), + strna(global_info.llmnr)); + + return 0; + } + + if (mode == STATUS_MDNS) { + printf("%sGlobal%s: %s\n", ansi_highlight(), ansi_normal(), + strna(global_info.mdns)); + + return 0; + } + + if (mode == STATUS_PRIVATE) { + printf("%sGlobal%s: %s\n", ansi_highlight(), ansi_normal(), + strna(global_info.dns_over_tls)); + + return 0; + } + + if (mode == STATUS_DNSSEC) { + printf("%sGlobal%s: %s\n", ansi_highlight(), ansi_normal(), + strna(global_info.dnssec)); + + return 0; + } + + printf("%sGlobal%s\n", ansi_highlight(), ansi_normal()); + + table = table_new("key", "value"); + if (!table) + return log_oom(); + + table_set_header(table, false); + + _cleanup_strv_free_ char **pstatus = global_protocol_status(&global_info); + if (!pstatus) + return log_oom(); + + r = table_add_many(table, + TABLE_STRING, "Protocols:", + TABLE_SET_ALIGN_PERCENT, 100, + TABLE_STRV_WRAPPED, pstatus); + if (r < 0) + return table_log_add_error(r); + + if (global_info.resolv_conf_mode) { + r = table_add_many(table, + TABLE_STRING, "resolv.conf mode:", + TABLE_STRING, global_info.resolv_conf_mode); + if (r < 0) + return table_log_add_error(r); + } + + if (global_info.current_dns) { + r = table_add_many(table, + TABLE_STRING, "Current DNS Server:", + TABLE_STRING, global_info.current_dns_ex ?: global_info.current_dns); + if (r < 0) + return table_log_add_error(r); + } + + r = dump_list(table, "DNS Servers", global_info.dns_ex ?: global_info.dns); + if (r < 0) + return r; + + r = dump_list(table, "Fallback DNS Servers", global_info.fallback_dns_ex ?: global_info.fallback_dns); + if (r < 0) + return r; + + r = dump_list(table, "DNS Domain", global_info.domains); + if (r < 0) + return r; + + r = table_print(table, NULL); + if (r < 0) + return table_log_print_error(r); + + *empty_line = true; + + return 0; +} + +static int status_all(sd_bus *bus, StatusMode mode) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + bool empty_line = false; + int r; + + assert(bus); + + r = status_global(bus, mode, &empty_line); + if (r < 0) + return r; + + r = sd_netlink_open(&rtnl); + if (r < 0) + return log_error_errno(r, "Failed to connect to netlink: %m"); + + r = sd_rtnl_message_new_link(rtnl, &req, RTM_GETLINK, 0); + if (r < 0) + return rtnl_log_create_error(r); + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return rtnl_log_create_error(r); + + r = sd_netlink_call(rtnl, req, 0, &reply); + if (r < 0) + return log_error_errno(r, "Failed to enumerate links: %m"); + + _cleanup_free_ InterfaceInfo *infos = NULL; + size_t n_infos = 0; + + for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) { + const char *name; + int ifindex; + uint16_t type; + + r = sd_netlink_message_get_type(i, &type); + if (r < 0) + return rtnl_log_parse_error(r); + + if (type != RTM_NEWLINK) + continue; + + r = sd_rtnl_message_link_get_ifindex(i, &ifindex); + if (r < 0) + return rtnl_log_parse_error(r); + + if (ifindex == LOOPBACK_IFINDEX) + continue; + + r = sd_netlink_message_read_string(i, IFLA_IFNAME, &name); + if (r < 0) + return rtnl_log_parse_error(r); + + if (!GREEDY_REALLOC(infos, n_infos + 1)) + return log_oom(); + + infos[n_infos++] = (InterfaceInfo) { ifindex, name }; + } + + typesafe_qsort(infos, n_infos, interface_info_compare); + + r = 0; + for (size_t i = 0; i < n_infos; i++) { + int q = status_ifindex(bus, infos[i].index, infos[i].name, mode, &empty_line); + if (q < 0 && r >= 0) + r = q; + } + + return r; +} + +static int verb_status(int argc, char **argv, void *userdata) { + sd_bus *bus = userdata; + _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; + int r = 0; + + if (argc > 1) { + bool empty_line = false; + + STRV_FOREACH(ifname, argv + 1) { + int ifindex, q; + + ifindex = rtnl_resolve_interface(&rtnl, *ifname); + if (ifindex < 0) { + log_warning_errno(ifindex, "Failed to resolve interface \"%s\", ignoring: %m", *ifname); + continue; + } + + q = status_ifindex(bus, ifindex, NULL, STATUS_ALL, &empty_line); + if (q < 0) + r = q; + } + } else + r = status_all(bus, STATUS_ALL); + + return r; +} + +static int call_dns(sd_bus *bus, char **dns, const BusLocator *locator, sd_bus_error *error, bool extended) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL; + int r; + + r = bus_message_new_method_call(bus, &req, locator, extended ? "SetLinkDNSEx" : "SetLinkDNS"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "i", arg_ifindex); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(req, 'a', extended ? "(iayqs)" : "(iay)"); + if (r < 0) + return bus_log_create_error(r); + + /* If only argument is the empty string, then call SetLinkDNS() with an + * empty list, which will clear the list of domains for an interface. */ + if (!strv_equal(dns, STRV_MAKE(""))) + STRV_FOREACH(p, dns) { + _cleanup_free_ char *name = NULL; + struct in_addr_data data; + uint16_t port; + int ifindex; + + r = in_addr_port_ifindex_name_from_string_auto(*p, &data.family, &data.address, &port, &ifindex, &name); + if (r < 0) + return log_error_errno(r, "Failed to parse DNS server address: %s", *p); + + if (ifindex != 0 && ifindex != arg_ifindex) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid ifindex: %i", ifindex); + + r = sd_bus_message_open_container(req, 'r', extended ? "iayqs" : "iay"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "i", data.family); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_array(req, 'y', &data.address, FAMILY_ADDRESS_SIZE(data.family)); + if (r < 0) + return bus_log_create_error(r); + + if (extended) { + r = sd_bus_message_append(req, "q", port); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "s", name); + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(req); + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(req); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_call(bus, req, 0, error, NULL); + if (r < 0 && extended && sd_bus_error_has_name(error, SD_BUS_ERROR_UNKNOWN_METHOD)) { + sd_bus_error_free(error); + return call_dns(bus, dns, locator, error, false); + } + return r; +} + +static int verb_dns(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_DNS); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_DNS, NULL); + + r = call_dns(bus, argv + 2, bus_resolve_mgr, &error, true); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = call_dns(bus, argv + 2, bus_network_mgr, &error, true); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set DNS configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int call_domain(sd_bus *bus, char **domain, const BusLocator *locator, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL; + int r; + + r = bus_message_new_method_call(bus, &req, locator, "SetLinkDomains"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "i", arg_ifindex); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_open_container(req, 'a', "(sb)"); + if (r < 0) + return bus_log_create_error(r); + + /* If only argument is the empty string, then call SetLinkDomains() with an + * empty list, which will clear the list of domains for an interface. */ + if (!strv_equal(domain, STRV_MAKE(""))) + STRV_FOREACH(p, domain) { + const char *n; + + n = **p == '~' ? *p + 1 : *p; + + r = dns_name_is_valid(n); + if (r < 0) + return log_error_errno(r, "Failed to validate specified domain %s: %m", n); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Domain not valid: %s", + n); + + r = sd_bus_message_append(req, "(sb)", n, **p == '~'); + if (r < 0) + return bus_log_create_error(r); + } + + r = sd_bus_message_close_container(req); + if (r < 0) + return bus_log_create_error(r); + + return sd_bus_call(bus, req, 0, error, NULL); +} + +static int verb_domain(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_DOMAIN); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_DOMAIN, NULL); + + r = call_domain(bus, argv + 2, bus_resolve_mgr, &error); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = call_domain(bus, argv + 2, bus_network_mgr, &error); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set domain configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_default_route(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r, b; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_DEFAULT_ROUTE); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_DEFAULT_ROUTE, NULL); + + b = parse_boolean(argv[2]); + if (b < 0) + return log_error_errno(b, "Failed to parse boolean argument: %s", argv[2]); + + r = bus_call_method(bus, bus_resolve_mgr, "SetLinkDefaultRoute", &error, NULL, "ib", arg_ifindex, b); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = bus_call_method(bus, bus_network_mgr, "SetLinkDefaultRoute", &error, NULL, "ib", arg_ifindex, b); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set default route configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_llmnr(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_LLMNR); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_LLMNR, NULL); + + r = bus_call_method(bus, bus_resolve_mgr, "SetLinkLLMNR", &error, NULL, "is", arg_ifindex, argv[2]); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = bus_call_method(bus, bus_network_mgr, "SetLinkLLMNR", &error, NULL, "is", arg_ifindex, argv[2]); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set LLMNR configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_mdns(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_MDNS); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_MDNS, NULL); + + r = bus_call_method(bus, bus_resolve_mgr, "SetLinkMulticastDNS", &error, NULL, "is", arg_ifindex, argv[2]); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = bus_call_method( + bus, + bus_network_mgr, + "SetLinkMulticastDNS", + &error, + NULL, + "is", arg_ifindex, argv[2]); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set MulticastDNS configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_dns_over_tls(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_PRIVATE); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_PRIVATE, NULL); + + r = bus_call_method(bus, bus_resolve_mgr, "SetLinkDNSOverTLS", &error, NULL, "is", arg_ifindex, argv[2]); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = bus_call_method( + bus, + bus_network_mgr, + "SetLinkDNSOverTLS", + &error, + NULL, + "is", arg_ifindex, argv[2]); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set DNSOverTLS configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_dnssec(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_DNSSEC); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_DNSSEC, NULL); + + r = bus_call_method(bus, bus_resolve_mgr, "SetLinkDNSSEC", &error, NULL, "is", arg_ifindex, argv[2]); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = bus_call_method(bus, bus_network_mgr, "SetLinkDNSSEC", &error, NULL, "is", arg_ifindex, argv[2]); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set DNSSEC configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int call_nta(sd_bus *bus, char **nta, const BusLocator *locator, sd_bus_error *error) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL; + int r; + + r = bus_message_new_method_call(bus, &req, locator, "SetLinkDNSSECNegativeTrustAnchors"); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append(req, "i", arg_ifindex); + if (r < 0) + return bus_log_create_error(r); + + r = sd_bus_message_append_strv(req, nta); + if (r < 0) + return bus_log_create_error(r); + + return sd_bus_call(bus, req, 0, error, NULL); +} + +static int verb_nta(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + bool clear; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return status_all(bus, STATUS_NTA); + + if (argc < 3) + return status_ifindex(bus, arg_ifindex, NULL, STATUS_NTA, NULL); + + /* If only argument is the empty string, then call SetLinkDNSSECNegativeTrustAnchors() + * with an empty list, which will clear the list of domains for an interface. */ + clear = strv_equal(argv + 2, STRV_MAKE("")); + + if (!clear) + STRV_FOREACH(p, argv + 2) { + r = dns_name_is_valid(*p); + if (r < 0) + return log_error_errno(r, "Failed to validate specified domain %s: %m", *p); + if (r == 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Domain not valid: %s", + *p); + } + + r = call_nta(bus, clear ? NULL : argv + 2, bus_resolve_mgr, &error); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = call_nta(bus, clear ? NULL : argv + 2, bus_network_mgr, &error); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to set DNSSEC NTA configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_revert_link(int argc, char **argv, void *userdata) { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus *bus = ASSERT_PTR(userdata); + int r; + + if (argc >= 2) { + r = ifname_mangle(argv[1]); + if (r < 0) + return r; + } + + if (arg_ifindex <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Interface argument required."); + + r = bus_call_method(bus, bus_resolve_mgr, "RevertLink", &error, NULL, "i", arg_ifindex); + if (r < 0 && sd_bus_error_has_name(&error, BUS_ERROR_LINK_BUSY)) { + sd_bus_error_free(&error); + + r = bus_call_method(bus, bus_network_mgr, "RevertLinkDNS", &error, NULL, "i", arg_ifindex); + } + if (r < 0) { + if (arg_ifindex_permissive && + sd_bus_error_has_name(&error, BUS_ERROR_NO_SUCH_LINK)) + return 0; + + return log_error_errno(r, "Failed to revert interface configuration: %s", bus_error_message(&error, r)); + } + + return 0; +} + +static int verb_log_level(int argc, char *argv[], void *userdata) { + sd_bus *bus = ASSERT_PTR(userdata); + + assert(IN_SET(argc, 1, 2)); + + return verb_log_control_common(bus, "org.freedesktop.resolve1", argv[0], argc == 2 ? argv[1] : NULL); +} + +static int monitor_rkey_from_json(JsonVariant *v, DnsResourceKey **ret_key) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + uint16_t type = 0, class = 0; + const char *name = NULL; + int r; + + JsonDispatch dispatch_table[] = { + { "class", JSON_VARIANT_INTEGER, json_dispatch_uint16, PTR_TO_SIZE(&class), JSON_MANDATORY }, + { "type", JSON_VARIANT_INTEGER, json_dispatch_uint16, PTR_TO_SIZE(&type), JSON_MANDATORY }, + { "name", JSON_VARIANT_STRING, json_dispatch_const_string, PTR_TO_SIZE(&name), JSON_MANDATORY }, + {} + }; + + assert(v); + assert(ret_key); + + r = json_dispatch(v, dispatch_table, NULL, 0, NULL); + if (r < 0) + return r; + + key = dns_resource_key_new(class, type, name); + if (!key) + return -ENOMEM; + + *ret_key = TAKE_PTR(key); + return 0; +} + +static int print_question(char prefix, const char *color, JsonVariant *question) { + JsonVariant *q = NULL; + int r; + + assert(color); + + JSON_VARIANT_ARRAY_FOREACH(q, question) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + char buf[DNS_RESOURCE_KEY_STRING_MAX]; + + r = monitor_rkey_from_json(q, &key); + if (r < 0) { + log_warning_errno(r, "Received monitor message with invalid question key, ignoring: %m"); + continue; + } + + printf("%s%s %c%s: %s\n", + color, + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), + prefix, + ansi_normal(), + dns_resource_key_to_string(key, buf, sizeof(buf))); + } + + return 0; +} + +static int print_answer(JsonVariant *answer) { + JsonVariant *a; + int r; + + JSON_VARIANT_ARRAY_FOREACH(a, answer) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + _cleanup_free_ void *d = NULL; + JsonVariant *jraw; + const char *s; + size_t l; + + jraw = json_variant_by_key(a, "raw"); + if (!jraw) { + log_warning("Received monitor answer lacking valid raw data, ignoring."); + continue; + } + + r = json_variant_unbase64(jraw, &d, &l); + if (r < 0) { + log_warning_errno(r, "Failed to undo base64 encoding of monitor answer raw data, ignoring."); + continue; + } + + r = dns_resource_record_new_from_raw(&rr, d, l); + if (r < 0) { + log_warning_errno(r, "Failed to parse monitor answer RR, ignoring: %m"); + continue; + } + + s = dns_resource_record_to_string(rr); + if (!s) + return log_oom(); + + printf("%s%s A%s: %s\n", + ansi_highlight_yellow(), + special_glyph(SPECIAL_GLYPH_ARROW_LEFT), + ansi_normal(), + s); + } + + return 0; +} + +static void monitor_query_dump(JsonVariant *v) { + _cleanup_(json_variant_unrefp) JsonVariant *question = NULL, *answer = NULL, *collected_questions = NULL; + int rcode = -1, error = 0, r; + const char *state = NULL; + + assert(v); + + JsonDispatch dispatch_table[] = { + { "question", JSON_VARIANT_ARRAY, json_dispatch_variant, PTR_TO_SIZE(&question), JSON_MANDATORY }, + { "answer", JSON_VARIANT_ARRAY, json_dispatch_variant, PTR_TO_SIZE(&answer), 0 }, + { "collectedQuestions", JSON_VARIANT_ARRAY, json_dispatch_variant, PTR_TO_SIZE(&collected_questions), 0 }, + { "state", JSON_VARIANT_STRING, json_dispatch_const_string, PTR_TO_SIZE(&state), JSON_MANDATORY }, + { "rcode", JSON_VARIANT_INTEGER, json_dispatch_int, PTR_TO_SIZE(&rcode), 0 }, + { "errno", JSON_VARIANT_INTEGER, json_dispatch_int, PTR_TO_SIZE(&error), 0 }, + {} + }; + + r = json_dispatch(v, dispatch_table, NULL, 0, NULL); + if (r < 0) + return (void) log_warning("Received malformed monitor message, ignoring."); + + /* First show the current question */ + print_question('Q', ansi_highlight_cyan(), question); + + /* And then show the questions that led to this one in case this was a CNAME chain */ + print_question('C', ansi_highlight_grey(), collected_questions); + + printf("%s%s S%s: %s\n", + streq_ptr(state, "success") ? ansi_highlight_green() : ansi_highlight_red(), + special_glyph(SPECIAL_GLYPH_ARROW_LEFT), + ansi_normal(), + strna(streq_ptr(state, "errno") ? errno_to_name(error) : + streq_ptr(state, "rcode-failure") ? dns_rcode_to_string(rcode) : + state)); + + print_answer(answer); +} + +static int monitor_reply( + Varlink *link, + JsonVariant *parameters, + const char *error_id, + VarlinkReplyFlags flags, + void *userdata) { + + assert(link); + + if (error_id) { + bool disconnect; + + disconnect = streq(error_id, VARLINK_ERROR_DISCONNECTED); + if (disconnect) + log_info("Disconnected."); + else + log_error("Varlink error: %s", error_id); + + (void) sd_event_exit(ASSERT_PTR(varlink_get_event(link)), disconnect ? EXIT_SUCCESS : EXIT_FAILURE); + return 0; + } + + if (json_variant_by_key(parameters, "ready")) { + /* The first message coming in will just indicate that we are now subscribed. We let our + * caller know if they asked for it. Once the caller sees this they should know that we are + * not going to miss any queries anymore. */ + (void) sd_notify(/* unset_environment=false */ false, "READY=1"); + return 0; + } + + if (arg_json_format_flags & JSON_FORMAT_OFF) { + monitor_query_dump(parameters); + printf("\n"); + } else + json_variant_dump(parameters, arg_json_format_flags, NULL, NULL); + + fflush(stdout); + + return 0; +} + +static int verb_monitor(int argc, char *argv[], void *userdata) { + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_(varlink_unrefp) Varlink *vl = NULL; + int r, c; + + r = sd_event_default(&event); + if (r < 0) + return log_error_errno(r, "Failed to get event loop: %m"); + + r = sd_event_set_signal_exit(event, true); + if (r < 0) + return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m"); + + r = varlink_connect_address(&vl, "/run/systemd/resolve/io.systemd.Resolve.Monitor"); + if (r < 0) + return log_error_errno(r, "Failed to connect to query monitoring service /run/systemd/resolve/io.systemd.Resolve.Monitor: %m"); + + r = varlink_set_relative_timeout(vl, USEC_INFINITY); /* We want the monitor to run basically forever */ + if (r < 0) + return log_error_errno(r, "Failed to set varlink time-out: %m"); + + r = varlink_attach_event(vl, event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + r = varlink_bind_reply(vl, monitor_reply); + if (r < 0) + return log_error_errno(r, "Failed to bind reply callback to varlink connection: %m"); + + r = varlink_observe(vl, "io.systemd.Resolve.Monitor.SubscribeQueryResults", NULL); + if (r < 0) + return log_error_errno(r, "Failed to issue SubscribeQueryResults() varlink call: %m"); + + r = sd_event_loop(event); + if (r < 0) + return log_error_errno(r, "Failed to run event loop: %m"); + + r = sd_event_get_exit_code(event, &c); + if (r < 0) + return log_error_errno(r, "Failed to get exit code: %m"); + + return c; +} + +static void help_protocol_types(void) { + if (arg_legend) + puts("Known protocol types:"); + puts("dns\n" + "llmnr\n" + "llmnr-ipv4\n" + "llmnr-ipv6\n" + "mdns\n" + "mdns-ipv4\n" + "mdns-ipv6"); +} + +static void help_dns_types(void) { + if (arg_legend) + puts("Known DNS RR types:"); + + DUMP_STRING_TABLE(dns_type, int, _DNS_TYPE_MAX); +} + +static void help_dns_classes(void) { + if (arg_legend) + puts("Known DNS RR classes:"); + + DUMP_STRING_TABLE(dns_class, int, _DNS_CLASS_MAX); +} + +static int compat_help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("resolvectl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] HOSTNAME|ADDRESS...\n" + "%1$s [OPTIONS...] --service [[NAME] TYPE] DOMAIN\n" + "%1$s [OPTIONS...] --openpgp EMAIL@DOMAIN...\n" + "%1$s [OPTIONS...] --statistics\n" + "%1$s [OPTIONS...] --reset-statistics\n" + "\n" + "%2$sResolve domain names, IPv4 and IPv6 addresses, DNS records, and services.%3$s\n\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " -4 Resolve IPv4 addresses\n" + " -6 Resolve IPv6 addresses\n" + " -i --interface=INTERFACE Look on interface\n" + " -p --protocol=PROTO|help Look via protocol\n" + " -t --type=TYPE|help Query RR with DNS type\n" + " -c --class=CLASS|help Query RR with DNS class\n" + " --service Resolve service (SRV)\n" + " --service-address=BOOL Resolve address for services (default: yes)\n" + " --service-txt=BOOL Resolve TXT records for services (default: yes)\n" + " --openpgp Query OpenPGP public key\n" + " --tlsa Query TLS public key\n" + " --cname=BOOL Follow CNAME redirects (default: yes)\n" + " --search=BOOL Use search domains for single-label names\n" + " (default: yes)\n" + " --raw[=payload|packet] Dump the answer as binary data\n" + " --legend=BOOL Print headers and additional info (default: yes)\n" + " --statistics Show resolver statistics\n" + " --reset-statistics Reset resolver statistics\n" + " --status Show link and server status\n" + " --flush-caches Flush all local DNS caches\n" + " --reset-server-features\n" + " Forget learnt DNS server feature levels\n" + " --set-dns=SERVER Set per-interface DNS server address\n" + " --set-domain=DOMAIN Set per-interface search domain\n" + " --set-llmnr=MODE Set per-interface LLMNR mode\n" + " --set-mdns=MODE Set per-interface MulticastDNS mode\n" + " --set-dnsovertls=MODE Set per-interface DNS-over-TLS mode\n" + " --set-dnssec=MODE Set per-interface DNSSEC mode\n" + " --set-nta=DOMAIN Set per-interface DNSSEC NTA\n" + " --revert Revert per-interface configuration\n" + "\nSee the %4$s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int native_help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("resolvectl", "1", &link); + if (r < 0) + return log_oom(); + + printf("%s [OPTIONS...] COMMAND ...\n" + "\n" + "%sSend control commands to the network name resolution manager, or%s\n" + "%sresolve domain names, IPv4 and IPv6 addresses, DNS records, and services.%s\n" + "\nCommands:\n" + " query HOSTNAME|ADDRESS... Resolve domain names, IPv4 and IPv6 addresses\n" + " service [[NAME] TYPE] DOMAIN Resolve service (SRV)\n" + " openpgp EMAIL@DOMAIN... Query OpenPGP public key\n" + " tlsa DOMAIN[:PORT]... Query TLS public key\n" + " status [LINK...] Show link and server status\n" + " statistics Show resolver statistics\n" + " reset-statistics Reset resolver statistics\n" + " flush-caches Flush all local DNS caches\n" + " reset-server-features Forget learnt DNS server feature levels\n" + " monitor Monitor DNS queries\n" + " dns [LINK [SERVER...]] Get/set per-interface DNS server address\n" + " domain [LINK [DOMAIN...]] Get/set per-interface search domain\n" + " default-route [LINK [BOOL]] Get/set per-interface default route flag\n" + " llmnr [LINK [MODE]] Get/set per-interface LLMNR mode\n" + " mdns [LINK [MODE]] Get/set per-interface MulticastDNS mode\n" + " dnsovertls [LINK [MODE]] Get/set per-interface DNS-over-TLS mode\n" + " dnssec [LINK [MODE]] Get/set per-interface DNSSEC mode\n" + " nta [LINK [DOMAIN...]] Get/set per-interface DNSSEC NTA\n" + " revert LINK Revert per-interface configuration\n" + " log-level [LEVEL] Get/set logging threshold for systemd-resolved\n" + "\nOptions:\n" + " -h --help Show this help\n" + " --version Show package version\n" + " --no-pager Do not pipe output into a pager\n" + " -4 Resolve IPv4 addresses\n" + " -6 Resolve IPv6 addresses\n" + " -i --interface=INTERFACE Look on interface\n" + " -p --protocol=PROTO|help Look via protocol\n" + " -t --type=TYPE|help Query RR with DNS type\n" + " -c --class=CLASS|help Query RR with DNS class\n" + " --service-address=BOOL Resolve address for services (default: yes)\n" + " --service-txt=BOOL Resolve TXT records for services (default: yes)\n" + " --cname=BOOL Follow CNAME redirects (default: yes)\n" + " --validate=BOOL Allow DNSSEC validation (default: yes)\n" + " --synthesize=BOOL Allow synthetic response (default: yes)\n" + " --cache=BOOL Allow response from cache (default: yes)\n" + " --zone=BOOL Allow response from locally registered mDNS/LLMNR\n" + " records (default: yes)\n" + " --trust-anchor=BOOL Allow response from local trust anchor (default:\n" + " yes)\n" + " --network=BOOL Allow response from network (default: yes)\n" + " --search=BOOL Use search domains for single-label names (default:\n" + " yes)\n" + " --raw[=payload|packet] Dump the answer as binary data\n" + " --legend=BOOL Print headers and additional info (default: yes)\n" + " --json=MODE Output as JSON\n" + " -j Same as --json=pretty on tty, --json=short\n" + " otherwise\n" + "\nSee the %s for details.\n", + program_invocation_short_name, + ansi_highlight(), + ansi_normal(), + ansi_highlight(), + ansi_normal(), + link); + + return 0; +} + +static int verb_help(int argc, char **argv, void *userdata) { + return native_help(); +} + +static int compat_parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_LEGEND, + ARG_SERVICE, + ARG_CNAME, + ARG_SERVICE_ADDRESS, + ARG_SERVICE_TXT, + ARG_OPENPGP, + ARG_TLSA, + ARG_RAW, + ARG_SEARCH, + ARG_STATISTICS, + ARG_RESET_STATISTICS, + ARG_STATUS, + ARG_FLUSH_CACHES, + ARG_RESET_SERVER_FEATURES, + ARG_NO_PAGER, + ARG_SET_DNS, + ARG_SET_DOMAIN, + ARG_SET_LLMNR, + ARG_SET_MDNS, + ARG_SET_PRIVATE, + ARG_SET_DNSSEC, + ARG_SET_NTA, + ARG_REVERT_LINK, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "type", required_argument, NULL, 't' }, + { "class", required_argument, NULL, 'c' }, + { "legend", required_argument, NULL, ARG_LEGEND }, + { "interface", required_argument, NULL, 'i' }, + { "protocol", required_argument, NULL, 'p' }, + { "cname", required_argument, NULL, ARG_CNAME }, + { "service", no_argument, NULL, ARG_SERVICE }, + { "service-address", required_argument, NULL, ARG_SERVICE_ADDRESS }, + { "service-txt", required_argument, NULL, ARG_SERVICE_TXT }, + { "openpgp", no_argument, NULL, ARG_OPENPGP }, + { "tlsa", optional_argument, NULL, ARG_TLSA }, + { "raw", optional_argument, NULL, ARG_RAW }, + { "search", required_argument, NULL, ARG_SEARCH }, + { "statistics", no_argument, NULL, ARG_STATISTICS, }, + { "reset-statistics", no_argument, NULL, ARG_RESET_STATISTICS }, + { "status", no_argument, NULL, ARG_STATUS }, + { "flush-caches", no_argument, NULL, ARG_FLUSH_CACHES }, + { "reset-server-features", no_argument, NULL, ARG_RESET_SERVER_FEATURES }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "set-dns", required_argument, NULL, ARG_SET_DNS }, + { "set-domain", required_argument, NULL, ARG_SET_DOMAIN }, + { "set-llmnr", required_argument, NULL, ARG_SET_LLMNR }, + { "set-mdns", required_argument, NULL, ARG_SET_MDNS }, + { "set-dnsovertls", required_argument, NULL, ARG_SET_PRIVATE }, + { "set-dnssec", required_argument, NULL, ARG_SET_DNSSEC }, + { "set-nta", required_argument, NULL, ARG_SET_NTA }, + { "revert", no_argument, NULL, ARG_REVERT_LINK }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h46i:t:c:p:", options, NULL)) >= 0) + switch (c) { + + case 'h': + return compat_help(); + + case ARG_VERSION: + return version(); + + case '4': + arg_family = AF_INET; + break; + + case '6': + arg_family = AF_INET6; + break; + + case 'i': + r = ifname_mangle(optarg); + if (r < 0) + return r; + break; + + case 't': + if (streq(optarg, "help")) { + help_dns_types(); + return 0; + } + + r = dns_type_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse RR record type %s: %m", optarg); + + arg_type = (uint16_t) r; + assert((int) arg_type == r); + + arg_mode = MODE_RESOLVE_RECORD; + break; + + case 'c': + if (streq(optarg, "help")) { + help_dns_classes(); + return 0; + } + + r = dns_class_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse RR record class %s: %m", optarg); + + arg_class = (uint16_t) r; + assert((int) arg_class == r); + + break; + + case ARG_LEGEND: + r = parse_boolean_argument("--legend=", optarg, &arg_legend); + if (r < 0) + return r; + break; + + case 'p': + if (streq(optarg, "help")) { + help_protocol_types(); + return 0; + } else if (streq(optarg, "dns")) + arg_flags |= SD_RESOLVED_DNS; + else if (streq(optarg, "llmnr")) + arg_flags |= SD_RESOLVED_LLMNR; + else if (streq(optarg, "llmnr-ipv4")) + arg_flags |= SD_RESOLVED_LLMNR_IPV4; + else if (streq(optarg, "llmnr-ipv6")) + arg_flags |= SD_RESOLVED_LLMNR_IPV6; + else if (streq(optarg, "mdns")) + arg_flags |= SD_RESOLVED_MDNS; + else if (streq(optarg, "mdns-ipv4")) + arg_flags |= SD_RESOLVED_MDNS_IPV4; + else if (streq(optarg, "mdns-ipv6")) + arg_flags |= SD_RESOLVED_MDNS_IPV6; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown protocol specifier: %s", optarg); + + break; + + case ARG_SERVICE: + arg_mode = MODE_RESOLVE_SERVICE; + break; + + case ARG_OPENPGP: + arg_mode = MODE_RESOLVE_OPENPGP; + break; + + case ARG_TLSA: + arg_mode = MODE_RESOLVE_TLSA; + if (!optarg || service_family_is_valid(optarg)) + arg_service_family = optarg; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown service family \"%s\".", optarg); + break; + + case ARG_RAW: + if (on_tty()) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), + "Refusing to write binary data to tty."); + + if (optarg == NULL || streq(optarg, "payload")) + arg_raw = RAW_PAYLOAD; + else if (streq(optarg, "packet")) + arg_raw = RAW_PACKET; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown --raw specifier \"%s\".", + optarg); + + arg_legend = false; + break; + + case ARG_CNAME: + r = parse_boolean_argument("--cname=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_CNAME, r == 0); + break; + + case ARG_SERVICE_ADDRESS: + r = parse_boolean_argument("--service-address=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_ADDRESS, r == 0); + break; + + case ARG_SERVICE_TXT: + r = parse_boolean_argument("--service-txt=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0); + break; + + case ARG_SEARCH: + r = parse_boolean_argument("--search=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_SEARCH, r == 0); + break; + + case ARG_STATISTICS: + arg_mode = MODE_STATISTICS; + break; + + case ARG_RESET_STATISTICS: + arg_mode = MODE_RESET_STATISTICS; + break; + + case ARG_FLUSH_CACHES: + arg_mode = MODE_FLUSH_CACHES; + break; + + case ARG_RESET_SERVER_FEATURES: + arg_mode = MODE_RESET_SERVER_FEATURES; + break; + + case ARG_STATUS: + arg_mode = MODE_STATUS; + break; + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_SET_DNS: + r = strv_extend(&arg_set_dns, optarg); + if (r < 0) + return log_oom(); + + arg_mode = MODE_SET_LINK; + break; + + case ARG_SET_DOMAIN: + r = strv_extend(&arg_set_domain, optarg); + if (r < 0) + return log_oom(); + + arg_mode = MODE_SET_LINK; + break; + + case ARG_SET_LLMNR: + arg_set_llmnr = optarg; + arg_mode = MODE_SET_LINK; + break; + + case ARG_SET_MDNS: + arg_set_mdns = optarg; + arg_mode = MODE_SET_LINK; + break; + + case ARG_SET_PRIVATE: + arg_set_dns_over_tls = optarg; + arg_mode = MODE_SET_LINK; + break; + + case ARG_SET_DNSSEC: + arg_set_dnssec = optarg; + arg_mode = MODE_SET_LINK; + break; + + case ARG_SET_NTA: + r = strv_extend(&arg_set_nta, optarg); + if (r < 0) + return log_oom(); + + arg_mode = MODE_SET_LINK; + break; + + case ARG_REVERT_LINK: + arg_mode = MODE_REVERT_LINK; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_type == 0 && arg_class != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--class= may only be used in conjunction with --type=."); + + if (arg_type != 0 && arg_mode == MODE_RESOLVE_SERVICE) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--service and --type= may not be combined."); + + if (arg_type != 0 && arg_class == 0) + arg_class = DNS_CLASS_IN; + + if (arg_class != 0 && arg_type == 0) + arg_type = DNS_TYPE_A; + + if (IN_SET(arg_mode, MODE_SET_LINK, MODE_REVERT_LINK)) { + + if (arg_ifindex <= 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--set-dns=, --set-domain=, --set-llmnr=, --set-mdns=, --set-dnsovertls=, --set-dnssec=, --set-nta= and --revert require --interface=."); + } + + return 1 /* work to do */; +} + +static int native_parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + ARG_LEGEND, + ARG_CNAME, + ARG_VALIDATE, + ARG_SYNTHESIZE, + ARG_CACHE, + ARG_ZONE, + ARG_TRUST_ANCHOR, + ARG_NETWORK, + ARG_SERVICE_ADDRESS, + ARG_SERVICE_TXT, + ARG_RAW, + ARG_SEARCH, + ARG_NO_PAGER, + ARG_JSON, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "type", required_argument, NULL, 't' }, + { "class", required_argument, NULL, 'c' }, + { "legend", required_argument, NULL, ARG_LEGEND }, + { "interface", required_argument, NULL, 'i' }, + { "protocol", required_argument, NULL, 'p' }, + { "cname", required_argument, NULL, ARG_CNAME }, + { "validate", required_argument, NULL, ARG_VALIDATE }, + { "synthesize", required_argument, NULL, ARG_SYNTHESIZE }, + { "cache", required_argument, NULL, ARG_CACHE }, + { "zone", required_argument, NULL, ARG_ZONE }, + { "trust-anchor", required_argument, NULL, ARG_TRUST_ANCHOR }, + { "network", required_argument, NULL, ARG_NETWORK }, + { "service-address", required_argument, NULL, ARG_SERVICE_ADDRESS }, + { "service-txt", required_argument, NULL, ARG_SERVICE_TXT }, + { "raw", optional_argument, NULL, ARG_RAW }, + { "search", required_argument, NULL, ARG_SEARCH }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "json", required_argument, NULL, ARG_JSON }, + {} + }; + + int c, r; + + assert(argc >= 0); + assert(argv); + + while ((c = getopt_long(argc, argv, "h46i:t:c:p:j", options, NULL)) >= 0) + switch (c) { + + case 'h': + return native_help(); + + case ARG_VERSION: + return version(); + + case '4': + arg_family = AF_INET; + break; + + case '6': + arg_family = AF_INET6; + break; + + case 'i': + r = ifname_mangle(optarg); + if (r < 0) + return r; + break; + + case 't': + if (streq(optarg, "help")) { + help_dns_types(); + return 0; + } + + r = dns_type_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse RR record type %s: %m", optarg); + + arg_type = (uint16_t) r; + assert((int) arg_type == r); + + break; + + case 'c': + if (streq(optarg, "help")) { + help_dns_classes(); + return 0; + } + + r = dns_class_from_string(optarg); + if (r < 0) + return log_error_errno(r, "Failed to parse RR record class %s: %m", optarg); + + arg_class = (uint16_t) r; + assert((int) arg_class == r); + + break; + + case ARG_LEGEND: + r = parse_boolean_argument("--legend=", optarg, &arg_legend); + if (r < 0) + return r; + break; + + case 'p': + if (streq(optarg, "help")) { + help_protocol_types(); + return 0; + } else if (streq(optarg, "dns")) + arg_flags |= SD_RESOLVED_DNS; + else if (streq(optarg, "llmnr")) + arg_flags |= SD_RESOLVED_LLMNR; + else if (streq(optarg, "llmnr-ipv4")) + arg_flags |= SD_RESOLVED_LLMNR_IPV4; + else if (streq(optarg, "llmnr-ipv6")) + arg_flags |= SD_RESOLVED_LLMNR_IPV6; + else if (streq(optarg, "mdns")) + arg_flags |= SD_RESOLVED_MDNS; + else if (streq(optarg, "mdns-ipv4")) + arg_flags |= SD_RESOLVED_MDNS_IPV4; + else if (streq(optarg, "mdns-ipv6")) + arg_flags |= SD_RESOLVED_MDNS_IPV6; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown protocol specifier: %s", + optarg); + + break; + + case ARG_RAW: + if (on_tty()) + return log_error_errno(SYNTHETIC_ERRNO(ENOTTY), + "Refusing to write binary data to tty."); + + if (optarg == NULL || streq(optarg, "payload")) + arg_raw = RAW_PAYLOAD; + else if (streq(optarg, "packet")) + arg_raw = RAW_PACKET; + else + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Unknown --raw specifier \"%s\".", + optarg); + + arg_legend = false; + break; + + case ARG_CNAME: + r = parse_boolean_argument("--cname=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_CNAME, r == 0); + break; + + case ARG_VALIDATE: + r = parse_boolean_argument("--validate=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_VALIDATE, r == 0); + break; + + case ARG_SYNTHESIZE: + r = parse_boolean_argument("--synthesize=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_SYNTHESIZE, r == 0); + break; + + case ARG_CACHE: + r = parse_boolean_argument("--cache=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_CACHE, r == 0); + break; + + case ARG_ZONE: + r = parse_boolean_argument("--zone=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_ZONE, r == 0); + break; + + case ARG_TRUST_ANCHOR: + r = parse_boolean_argument("--trust-anchor=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_TRUST_ANCHOR, r == 0); + break; + + case ARG_NETWORK: + r = parse_boolean_argument("--network=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_NETWORK, r == 0); + break; + + case ARG_SERVICE_ADDRESS: + r = parse_boolean_argument("--service-address=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_ADDRESS, r == 0); + break; + + case ARG_SERVICE_TXT: + r = parse_boolean_argument("--service-txt=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0); + break; + + case ARG_SEARCH: + r = parse_boolean_argument("--search=", optarg, NULL); + if (r < 0) + return r; + SET_FLAG(arg_flags, SD_RESOLVED_NO_SEARCH, r == 0); + break; + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + + break; + + case 'j': + arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_type == 0 && arg_class != 0) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "--class= may only be used in conjunction with --type=."); + + if (arg_type != 0 && arg_class == 0) + arg_class = DNS_CLASS_IN; + + if (arg_class != 0 && arg_type == 0) + arg_type = DNS_TYPE_A; + + return 1 /* work to do */; +} + +static int native_main(int argc, char *argv[], sd_bus *bus) { + + static const Verb verbs[] = { + { "help", VERB_ANY, VERB_ANY, 0, verb_help }, + { "status", VERB_ANY, VERB_ANY, VERB_DEFAULT, verb_status }, + { "query", 2, VERB_ANY, 0, verb_query }, + { "service", 2, 4, 0, verb_service }, + { "openpgp", 2, VERB_ANY, 0, verb_openpgp }, + { "tlsa", 2, VERB_ANY, 0, verb_tlsa }, + { "statistics", VERB_ANY, 1, 0, show_statistics }, + { "reset-statistics", VERB_ANY, 1, 0, reset_statistics }, + { "flush-caches", VERB_ANY, 1, 0, flush_caches }, + { "reset-server-features", VERB_ANY, 1, 0, reset_server_features }, + { "dns", VERB_ANY, VERB_ANY, 0, verb_dns }, + { "domain", VERB_ANY, VERB_ANY, 0, verb_domain }, + { "default-route", VERB_ANY, 3, 0, verb_default_route }, + { "llmnr", VERB_ANY, 3, 0, verb_llmnr }, + { "mdns", VERB_ANY, 3, 0, verb_mdns }, + { "dnsovertls", VERB_ANY, 3, 0, verb_dns_over_tls }, + { "dnssec", VERB_ANY, 3, 0, verb_dnssec }, + { "nta", VERB_ANY, VERB_ANY, 0, verb_nta }, + { "revert", VERB_ANY, 2, 0, verb_revert_link }, + { "log-level", VERB_ANY, 2, 0, verb_log_level }, + { "monitor", VERB_ANY, 1, 0, verb_monitor }, + {} + }; + + return dispatch_verb(argc, argv, verbs, bus); +} + +static int translate(const char *verb, const char *single_arg, size_t num_args, char **args, sd_bus *bus) { + char **fake, **p; + size_t num; + + assert(verb); + assert(num_args == 0 || args); + + num = !!single_arg + num_args + 1; + + p = fake = newa0(char *, num + 1); + *p++ = (char *) verb; + if (single_arg) + *p++ = (char *) single_arg; + for (size_t i = 0; i < num_args; i++) + *p++ = args[i]; + + optind = 0; + return native_main((int) num, fake, bus); +} + +static int compat_main(int argc, char *argv[], sd_bus *bus) { + int r = 0; + + switch (arg_mode) { + case MODE_RESOLVE_HOST: + case MODE_RESOLVE_RECORD: + return translate("query", NULL, argc - optind, argv + optind, bus); + + case MODE_RESOLVE_SERVICE: + return translate("service", NULL, argc - optind, argv + optind, bus); + + case MODE_RESOLVE_OPENPGP: + return translate("openpgp", NULL, argc - optind, argv + optind, bus); + + case MODE_RESOLVE_TLSA: + return translate("tlsa", arg_service_family, argc - optind, argv + optind, bus); + + case MODE_STATISTICS: + return translate("statistics", NULL, 0, NULL, bus); + + case MODE_RESET_STATISTICS: + return translate("reset-statistics", NULL, 0, NULL, bus); + + case MODE_FLUSH_CACHES: + return translate("flush-caches", NULL, 0, NULL, bus); + + case MODE_RESET_SERVER_FEATURES: + return translate("reset-server-features", NULL, 0, NULL, bus); + + case MODE_STATUS: + return translate("status", NULL, argc - optind, argv + optind, bus); + + case MODE_SET_LINK: + assert(arg_ifname); + + if (arg_set_dns) { + r = translate("dns", arg_ifname, strv_length(arg_set_dns), arg_set_dns, bus); + if (r < 0) + return r; + } + + if (arg_set_domain) { + r = translate("domain", arg_ifname, strv_length(arg_set_domain), arg_set_domain, bus); + if (r < 0) + return r; + } + + if (arg_set_nta) { + r = translate("nta", arg_ifname, strv_length(arg_set_nta), arg_set_nta, bus); + if (r < 0) + return r; + } + + if (arg_set_llmnr) { + r = translate("llmnr", arg_ifname, 1, (char **) &arg_set_llmnr, bus); + if (r < 0) + return r; + } + + if (arg_set_mdns) { + r = translate("mdns", arg_ifname, 1, (char **) &arg_set_mdns, bus); + if (r < 0) + return r; + } + + if (arg_set_dns_over_tls) { + r = translate("dnsovertls", arg_ifname, 1, (char **) &arg_set_dns_over_tls, bus); + if (r < 0) + return r; + } + + if (arg_set_dnssec) { + r = translate("dnssec", arg_ifname, 1, (char **) &arg_set_dnssec, bus); + if (r < 0) + return r; + } + + return r; + + case MODE_REVERT_LINK: + assert(arg_ifname); + + return translate("revert", arg_ifname, 0, NULL, bus); + + case _MODE_INVALID: + assert_not_reached(); + } + + return 0; +} + +static int run(int argc, char **argv) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + bool compat = false; + int r; + + setlocale(LC_ALL, ""); + log_setup(); + + if (invoked_as(argv, "resolvconf")) { + compat = true; + r = resolvconf_parse_argv(argc, argv); + } else if (invoked_as(argv, "systemd-resolve")) { + compat = true; + r = compat_parse_argv(argc, argv); + } else + r = native_parse_argv(argc, argv); + if (r <= 0) + return r; + + r = sd_bus_open_system(&bus); + if (r < 0) + return log_error_errno(r, "sd_bus_open_system: %m"); + + if (compat) + return compat_main(argc, argv, bus); + + return native_main(argc, argv, bus); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/resolve/resolvectl.h b/src/resolve/resolvectl.h new file mode 100644 index 0000000..3e404da --- /dev/null +++ b/src/resolve/resolvectl.h @@ -0,0 +1,35 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <in-addr-util.h> +#include <stdbool.h> +#include <sys/types.h> + +typedef enum ExecutionMode { + MODE_RESOLVE_HOST, + MODE_RESOLVE_RECORD, + MODE_RESOLVE_SERVICE, + MODE_RESOLVE_OPENPGP, + MODE_RESOLVE_TLSA, + MODE_STATISTICS, + MODE_RESET_STATISTICS, + MODE_FLUSH_CACHES, + MODE_RESET_SERVER_FEATURES, + MODE_STATUS, + MODE_SET_LINK, + MODE_REVERT_LINK, + _MODE_INVALID = -EINVAL, +} ExecutionMode; + +extern ExecutionMode arg_mode; +extern char **arg_set_dns; +extern char **arg_set_domain; +extern bool arg_ifindex_permissive; + +int ifname_mangle_full(const char *s, bool drop_protocol_specifier); +static inline int ifname_mangle(const char *s) { + return ifname_mangle_full(s, false); +} +static inline int ifname_resolvconf_mangle(const char *s) { + return ifname_mangle_full(s, true); +} diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c new file mode 100644 index 0000000..919573c --- /dev/null +++ b/src/resolve/resolved-bus.c @@ -0,0 +1,2309 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-get-properties.h" +#include "bus-log-control-api.h" +#include "bus-message-util.h" +#include "bus-polkit.h" +#include "dns-domain.h" +#include "format-util.h" +#include "memory-util.h" +#include "missing_capability.h" +#include "resolved-bus.h" +#include "resolved-def.h" +#include "resolved-dns-synthesize.h" +#include "resolved-dnssd-bus.h" +#include "resolved-dnssd.h" +#include "resolved-link-bus.h" +#include "resolved-resolv-conf.h" +#include "socket-netlink.h" +#include "stdio-util.h" +#include "strv.h" +#include "syslog-util.h" +#include "user-util.h" +#include "utf8.h" + +BUS_DEFINE_PROPERTY_GET_ENUM(bus_property_get_resolve_support, resolve_support, ResolveSupport); + +static int query_on_bus_track(sd_bus_track *t, void *userdata) { + DnsQuery *q = ASSERT_PTR(userdata); + + assert(t); + + if (!DNS_TRANSACTION_IS_LIVE(q->state)) + return 0; + + log_debug("Client of active query vanished, aborting query."); + dns_query_complete(q, DNS_TRANSACTION_ABORTED); + return 0; +} + +static int dns_query_bus_track(DnsQuery *q, sd_bus_message *m) { + int r; + + assert(q); + assert(m); + + if (!q->bus_track) { + r = sd_bus_track_new(sd_bus_message_get_bus(m), &q->bus_track, query_on_bus_track, q); + if (r < 0) + return r; + } + + r = sd_bus_track_add_sender(q->bus_track, m); + if (r < 0) + return r; + + return 0; +} + +static sd_bus_message *dns_query_steal_request(DnsQuery *q) { + assert(q); + + /* Find the main query, it's the one that owns the message */ + while (q->auxiliary_for) + q = q->auxiliary_for; + + /* Let's take the request message out of the DnsQuery object, so that we never send requests twice */ + return TAKE_PTR(q->bus_request); +} + +_sd_printf_(3, 4) static int reply_method_errorf( + DnsQuery *query, + const char *error_name, + const char *format, + ...) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL; + va_list ap; + int r; + + assert(query); + assert(format); + + req = dns_query_steal_request(query); + if (!req) /* No bus message set anymore? then we already replied already, let's not answer a second time */ + return 0; + + va_start(ap, format); + r = sd_bus_reply_method_errorfv(req, error_name, format, ap); + va_end(ap); + + return r; +} + +_sd_printf_(3, 4) static int reply_method_errnof( + DnsQuery *query, + int err, + const char *format, + ...) { + + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL; + int r; + + assert(query); + + req = dns_query_steal_request(query); + if (!req) /* No bus message set anymore? then we already replied already, let's not answer a second time */ + return 0; + + if (format) { + va_list ap; + + va_start(ap, format); + r = sd_bus_reply_method_errnofv(req, err, format, ap); + va_end(ap); + } else + r = sd_bus_reply_method_errno(req, err, NULL); + + return r; +} + +static int reply_query_state(DnsQuery *q) { + assert(q); + + switch (q->state) { + + case DNS_TRANSACTION_NO_SERVERS: + return reply_method_errorf(q, BUS_ERROR_NO_NAME_SERVERS, "No appropriate name servers or networks for name found"); + + case DNS_TRANSACTION_TIMEOUT: + return reply_method_errorf(q, SD_BUS_ERROR_TIMEOUT, "Query timed out"); + + case DNS_TRANSACTION_ATTEMPTS_MAX_REACHED: + return reply_method_errorf(q, SD_BUS_ERROR_TIMEOUT, "All attempts to contact name servers or networks failed"); + + case DNS_TRANSACTION_INVALID_REPLY: + return reply_method_errorf(q, BUS_ERROR_INVALID_REPLY, "Received invalid reply"); + + case DNS_TRANSACTION_ERRNO: + return reply_method_errnof(q, q->answer_errno, "Lookup failed due to system error: %m"); + + case DNS_TRANSACTION_ABORTED: + return reply_method_errorf(q, BUS_ERROR_ABORTED, "Query aborted"); + + case DNS_TRANSACTION_DNSSEC_FAILED: + return reply_method_errorf(q, BUS_ERROR_DNSSEC_FAILED, "DNSSEC validation failed: %s", + dnssec_result_to_string(q->answer_dnssec_result)); + + case DNS_TRANSACTION_NO_TRUST_ANCHOR: + return reply_method_errorf(q, BUS_ERROR_NO_TRUST_ANCHOR, "No suitable trust anchor known"); + + case DNS_TRANSACTION_RR_TYPE_UNSUPPORTED: + return reply_method_errorf(q, BUS_ERROR_RR_TYPE_UNSUPPORTED, "Server does not support requested resource record type"); + + case DNS_TRANSACTION_NETWORK_DOWN: + return reply_method_errorf(q, BUS_ERROR_NETWORK_DOWN, "Network is down"); + + case DNS_TRANSACTION_NOT_FOUND: + /* We return this as NXDOMAIN. This is only generated when a host doesn't implement LLMNR/TCP, and we + * thus quickly know that we cannot resolve an in-addr.arpa or ip6.arpa address. */ + return reply_method_errorf(q, BUS_ERROR_DNS_NXDOMAIN, "'%s' not found", dns_query_string(q)); + + case DNS_TRANSACTION_NO_SOURCE: + return reply_method_errorf(q, BUS_ERROR_NO_SOURCE, "All suitable resolution sources turned off"); + + case DNS_TRANSACTION_STUB_LOOP: + return reply_method_errorf(q, BUS_ERROR_STUB_LOOP, "Configured DNS server loops back to us"); + + case DNS_TRANSACTION_RCODE_FAILURE: { + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL; + + req = dns_query_steal_request(q); + if (!req) /* No bus message set anymore? then we already replied already, let's not answer a second time */ + return 0; + + if (q->answer_rcode == DNS_RCODE_NXDOMAIN) + sd_bus_error_setf(&error, BUS_ERROR_DNS_NXDOMAIN, "Name '%s' not found", dns_query_string(q)); + else { + const char *rc, *n; + + rc = FORMAT_DNS_RCODE(q->answer_rcode); + n = strjoina(_BUS_ERROR_DNS, rc); + sd_bus_error_setf(&error, n, "Could not resolve '%s', server or network returned error %s", dns_query_string(q), rc); + } + + return sd_bus_reply_method_error(req, &error); + } + + case DNS_TRANSACTION_NULL: + case DNS_TRANSACTION_PENDING: + case DNS_TRANSACTION_VALIDATING: + case DNS_TRANSACTION_SUCCESS: + default: + assert_not_reached(); + } +} + +static int append_address(sd_bus_message *reply, DnsResourceRecord *rr, int ifindex) { + int r; + + assert(reply); + assert(rr); + + r = sd_bus_message_open_container(reply, 'r', "iiay"); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "i", ifindex); + if (r < 0) + return r; + + if (rr->key->type == DNS_TYPE_A) { + r = sd_bus_message_append(reply, "i", AF_INET); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', &rr->a.in_addr, sizeof(struct in_addr)); + + } else if (rr->key->type == DNS_TYPE_AAAA) { + r = sd_bus_message_append(reply, "i", AF_INET6); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', &rr->aaaa.in6_addr, sizeof(struct in6_addr)); + } else + return -EAFNOSUPPORT; + + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return 0; +} + +static void bus_method_resolve_hostname_complete(DnsQuery *query) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = query; + _cleanup_free_ char *normalized = NULL; + DnsQuestion *question; + DnsResourceRecord *rr; + unsigned added = 0; + int ifindex, r; + + assert(q); + + if (q->state != DNS_TRANSACTION_SUCCESS) { + r = reply_query_state(q); + goto finish; + } + + r = dns_query_process_cname_many(q); + if (r == -ELOOP) { + r = reply_method_errorf(q, BUS_ERROR_CNAME_LOOP, "CNAME loop detected, or CNAME resolving disabled on '%s'", dns_query_string(q)); + goto finish; + } + if (r < 0) + goto finish; + if (r == DNS_QUERY_CNAME) { + /* This was a cname, and the query was restarted. */ + TAKE_PTR(q); + return; + } + + r = sd_bus_message_new_method_return(q->bus_request, &reply); + if (r < 0) + goto finish; + + r = sd_bus_message_open_container(reply, 'a', "(iiay)"); + if (r < 0) + goto finish; + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) { + + r = dns_question_matches_rr(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain)); + if (r < 0) + goto finish; + if (r == 0) + continue; + + r = append_address(reply, rr, ifindex); + if (r < 0) + goto finish; + + if (!canonical) + canonical = dns_resource_record_ref(rr); + + added++; + } + + if (added <= 0) { + r = reply_method_errorf(q, BUS_ERROR_NO_SUCH_RR, "'%s' does not have any RR of the requested type", dns_query_string(q)); + goto finish; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + goto finish; + + /* The key names are not necessarily normalized, make sure that they are when we return them to our + * bus clients. */ + assert(canonical); + r = dns_name_normalize(dns_resource_key_name(canonical->key), 0, &normalized); + if (r < 0) + goto finish; + + /* Return the precise spelling and uppercasing and CNAME target reported by the server */ + r = sd_bus_message_append( + reply, "st", + normalized, + dns_query_reply_flags_make(q)); + if (r < 0) + goto finish; + + q->bus_request = sd_bus_message_unref(q->bus_request); + r = sd_bus_send(q->manager->bus, reply, NULL); + +finish: + if (r < 0) { + log_error_errno(r, "Failed to send hostname reply: %m"); + (void) reply_method_errnof(q, r, NULL); + } +} + +static int validate_and_mangle_flags( + const char *name, + uint64_t *flags, + uint64_t ok, + sd_bus_error *error) { + + assert(flags); + + /* Checks that the client supplied interface index and flags parameter actually are valid and make + * sense in our method call context. Specifically: + * + * 1. Checks that the interface index is either 0 (meaning *all* interfaces) or positive + * + * 2. Only the protocols flags and a bunch of NO_XYZ flags are set, at most. Plus additional flags + * specific to our method, passed in the "ok" parameter. + * + * 3. If zero protocol flags are specified it is automatically turned into *all* protocols. This way + * clients can simply pass 0 as flags and all will work as it should. They can also use this so + * that clients don't have to know all the protocols resolved implements, but can just specify 0 + * to mean "all supported protocols". + */ + + if (*flags & ~(SD_RESOLVED_PROTOCOLS_ALL| + SD_RESOLVED_NO_CNAME| + SD_RESOLVED_NO_VALIDATE| + SD_RESOLVED_NO_SYNTHESIZE| + SD_RESOLVED_NO_CACHE| + SD_RESOLVED_NO_ZONE| + SD_RESOLVED_NO_TRUST_ANCHOR| + SD_RESOLVED_NO_NETWORK| + ok)) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags parameter"); + + if ((*flags & SD_RESOLVED_PROTOCOLS_ALL) == 0) /* If no protocol is enabled, enable all */ + *flags |= SD_RESOLVED_PROTOCOLS_ALL; + + /* Imply SD_RESOLVED_NO_SEARCH if permitted and name is dot suffixed. */ + if (name && FLAGS_SET(ok, SD_RESOLVED_NO_SEARCH) && dns_name_dot_suffixed(name) > 0) + *flags |= SD_RESOLVED_NO_SEARCH; + + return 0; +} + +static int parse_as_address(sd_bus_message *m, int ifindex, const char *hostname, int family, uint64_t flags) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_free_ char *canonical = NULL; + union in_addr_union parsed; + int r, ff, parsed_ifindex = 0; + + /* Check if the hostname is actually already an IP address formatted as string. In that case just parse it, + * let's not attempt to look it up. */ + + r = in_addr_ifindex_from_string_auto(hostname, &ff, &parsed, &parsed_ifindex); + if (r < 0) /* not an address */ + return 0; + + if (family != AF_UNSPEC && ff != family) + return sd_bus_reply_method_errorf(m, BUS_ERROR_NO_SUCH_RR, "The specified address is not of the requested family."); + if (ifindex > 0 && parsed_ifindex > 0 && parsed_ifindex != ifindex) + return sd_bus_reply_method_errorf(m, BUS_ERROR_NO_SUCH_RR, "The specified address interface index does not match requested interface."); + + if (parsed_ifindex > 0) + ifindex = parsed_ifindex; + + r = sd_bus_message_new_method_return(m, &reply); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(iiay)"); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'r', "iiay"); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "ii", ifindex, ff); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', &parsed, FAMILY_ADDRESS_SIZE(ff)); + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + /* When an IP address is specified we just return it as canonical name, in order to avoid a DNS + * look-up. However, we reformat it to make sure it's in a truly canonical form (i.e. on IPv6 the inner + * omissions are always done the same way). */ + r = in_addr_ifindex_to_string(ff, &parsed, ifindex, &canonical); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "st", canonical, + SD_RESOLVED_FLAGS_MAKE(dns_synthesize_protocol(flags), ff, true, true) | + SD_RESOLVED_SYNTHETIC); + if (r < 0) + return r; + + return sd_bus_send(sd_bus_message_get_bus(m), reply, NULL); +} + +void bus_client_log(sd_bus_message *m, const char *what) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + const char *comm = NULL; + uid_t uid = UID_INVALID; + pid_t pid = 0; + int r; + + assert(m); + assert(what); + + if (!DEBUG_LOGGING) + return; + + r = sd_bus_query_sender_creds(m, SD_BUS_CREDS_PID|SD_BUS_CREDS_UID|SD_BUS_CREDS_COMM|SD_BUS_CREDS_AUGMENT, &creds); + if (r < 0) + return (void) log_debug_errno(r, "Failed to query client credentials, ignoring: %m"); + + (void) sd_bus_creds_get_uid(creds, &uid); + (void) sd_bus_creds_get_pid(creds, &pid); + (void) sd_bus_creds_get_comm(creds, &comm); + + log_debug("D-Bus %s request from client PID " PID_FMT " (%s) with UID " UID_FMT, + what, pid, strna(comm), uid); +} + +static int bus_method_resolve_hostname(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + Manager *m = ASSERT_PTR(userdata); + const char *hostname; + int family, ifindex; + uint64_t flags; + int r; + + assert(message); + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(message, "isit", &ifindex, &hostname, &family, &flags); + if (r < 0) + return r; + + if (ifindex < 0) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index"); + + if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Unknown address family %i", family); + + r = validate_and_mangle_flags(hostname, &flags, SD_RESOLVED_NO_SEARCH, error); + if (r < 0) + return r; + + r = parse_as_address(message, ifindex, hostname, family, flags); + if (r != 0) + return r; + + r = dns_name_is_valid(hostname); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid hostname '%s'", hostname); + + r = dns_question_new_address(&question_utf8, family, hostname, false); + if (r < 0) + return r; + + r = dns_question_new_address(&question_idna, family, hostname, true); + if (r < 0 && r != -EALREADY) + return r; + + bus_client_log(message, "hostname resolution"); + + r = dns_query_new(m, &q, question_utf8, question_idna ?: question_utf8, NULL, ifindex, flags); + if (r < 0) + return r; + + q->bus_request = sd_bus_message_ref(message); + q->request_family = family; + q->complete = bus_method_resolve_hostname_complete; + + r = dns_query_bus_track(q, message); + if (r < 0) + return r; + + r = dns_query_go(q); + if (r < 0) + return r; + + TAKE_PTR(q); + return 1; +} + +static void bus_method_resolve_address_complete(DnsQuery *query) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = query; + DnsQuestion *question; + DnsResourceRecord *rr; + unsigned added = 0; + int ifindex, r; + + assert(q); + + if (q->state != DNS_TRANSACTION_SUCCESS) { + r = reply_query_state(q); + goto finish; + } + + r = dns_query_process_cname_many(q); + if (r == -ELOOP) { + r = reply_method_errorf(q, BUS_ERROR_CNAME_LOOP, "CNAME loop detected, or CNAME resolving disabled on '%s'", dns_query_string(q)); + goto finish; + } + if (r < 0) + goto finish; + if (r == DNS_QUERY_CNAME) { + /* This was a cname, and the query was restarted. */ + TAKE_PTR(q); + return; + } + + r = sd_bus_message_new_method_return(q->bus_request, &reply); + if (r < 0) + goto finish; + + r = sd_bus_message_open_container(reply, 'a', "(is)"); + if (r < 0) + goto finish; + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) { + _cleanup_free_ char *normalized = NULL; + + r = dns_question_matches_rr(question, rr, NULL); + if (r < 0) + goto finish; + if (r == 0) + continue; + + r = dns_name_normalize(rr->ptr.name, 0, &normalized); + if (r < 0) + goto finish; + + r = sd_bus_message_append(reply, "(is)", ifindex, normalized); + if (r < 0) + goto finish; + + added++; + } + + if (added <= 0) { + r = reply_method_errorf(q, BUS_ERROR_NO_SUCH_RR, + "Address %s does not have any RR of requested type", + IN_ADDR_TO_STRING(q->request_family, &q->request_address)); + goto finish; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + goto finish; + + r = sd_bus_message_append(reply, "t", dns_query_reply_flags_make(q)); + if (r < 0) + goto finish; + + q->bus_request = sd_bus_message_unref(q->bus_request); + r = sd_bus_send(q->manager->bus, reply, NULL); + +finish: + if (r < 0) { + log_error_errno(r, "Failed to send address reply: %m"); + (void) reply_method_errnof(q, r, NULL); + } +} + +static int bus_method_resolve_address(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + Manager *m = ASSERT_PTR(userdata); + union in_addr_union a; + int family, ifindex; + uint64_t flags; + int r; + + assert(message); + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(message, "i", &ifindex); + if (r < 0) + return r; + + r = bus_message_read_in_addr_auto(message, error, &family, &a); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "t", &flags); + if (r < 0) + return r; + + if (ifindex < 0) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index"); + + r = validate_and_mangle_flags(NULL, &flags, 0, error); + if (r < 0) + return r; + + r = dns_question_new_reverse(&question, family, &a); + if (r < 0) + return r; + + bus_client_log(message, "address resolution"); + + r = dns_query_new(m, &q, question, question, NULL, ifindex, flags|SD_RESOLVED_NO_SEARCH); + if (r < 0) + return r; + + q->bus_request = sd_bus_message_ref(message); + q->request_family = family; + q->request_address = a; + q->complete = bus_method_resolve_address_complete; + + r = dns_query_bus_track(q, message); + if (r < 0) + return r; + + r = dns_query_go(q); + if (r < 0) + return r; + + TAKE_PTR(q); + return 1; +} + +static int bus_message_append_rr(sd_bus_message *m, DnsResourceRecord *rr, int ifindex) { + int r; + + assert(m); + assert(rr); + + r = sd_bus_message_open_container(m, 'r', "iqqay"); + if (r < 0) + return r; + + r = sd_bus_message_append(m, "iqq", + ifindex, + rr->key->class, + rr->key->type); + if (r < 0) + return r; + + r = dns_resource_record_to_wire_format(rr, false); + if (r < 0) + return r; + + r = sd_bus_message_append_array(m, 'y', rr->wire_format, rr->wire_format_size); + if (r < 0) + return r; + + return sd_bus_message_close_container(m); +} + +static void bus_method_resolve_record_complete(DnsQuery *query) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = query; + DnsResourceRecord *rr; + DnsQuestion *question; + unsigned added = 0; + int ifindex; + int r; + + assert(q); + + if (q->state != DNS_TRANSACTION_SUCCESS) { + r = reply_query_state(q); + goto finish; + } + + r = dns_query_process_cname_many(q); + if (r == -ELOOP) { + r = reply_method_errorf(q, BUS_ERROR_CNAME_LOOP, "CNAME loop detected, or CNAME resolving disabled on '%s'", dns_query_string(q)); + goto finish; + } + if (r < 0) + goto finish; + if (r == DNS_QUERY_CNAME) { + /* This was a cname, and the query was restarted. */ + TAKE_PTR(q); + return; + } + + r = sd_bus_message_new_method_return(q->bus_request, &reply); + if (r < 0) + goto finish; + + r = sd_bus_message_open_container(reply, 'a', "(iqqay)"); + if (r < 0) + goto finish; + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) { + r = dns_question_matches_rr(question, rr, NULL); + if (r < 0) + goto finish; + if (r == 0) + continue; + + r = bus_message_append_rr(reply, rr, ifindex); + if (r < 0) + goto finish; + + added++; + } + + if (added <= 0) { + r = reply_method_errorf(q, BUS_ERROR_NO_SUCH_RR, "Name '%s' does not have any RR of the requested type", dns_query_string(q)); + goto finish; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + goto finish; + + r = sd_bus_message_append(reply, "t", dns_query_reply_flags_make(q)); + if (r < 0) + goto finish; + + q->bus_request = sd_bus_message_unref(q->bus_request); + r = sd_bus_send(q->manager->bus, reply, NULL); + +finish: + if (r < 0) { + log_error_errno(r, "Failed to send record reply: %m"); + (void) reply_method_errnof(q, r, NULL); + } +} + +static int bus_method_resolve_record(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + Manager *m = ASSERT_PTR(userdata); + uint16_t class, type; + const char *name; + int r, ifindex; + uint64_t flags; + + assert(message); + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(message, "isqqt", &ifindex, &name, &class, &type, &flags); + if (r < 0) + return r; + + if (ifindex < 0) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index"); + + r = dns_name_is_valid(name); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid name '%s'", name); + + if (!dns_type_is_valid_query(type)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Specified resource record type %" PRIu16 " may not be used in a query.", type); + if (dns_type_is_zone_transer(type)) + return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Zone transfers not permitted via this programming interface."); + if (dns_type_is_obsolete(type)) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Specified DNS resource record type %" PRIu16 " is obsolete.", type); + + r = validate_and_mangle_flags(name, &flags, 0, error); + if (r < 0) + return r; + + question = dns_question_new(1); + if (!question) + return -ENOMEM; + + key = dns_resource_key_new(class, type, name); + if (!key) + return -ENOMEM; + + r = dns_question_add(question, key, 0); + if (r < 0) + return r; + + bus_client_log(message, "resource record resolution"); + + /* Setting SD_RESOLVED_CLAMP_TTL: let's request that the TTL is fixed up for locally cached entries, + * after all we return it in the wire format blob. */ + r = dns_query_new(m, &q, question, question, NULL, ifindex, flags|SD_RESOLVED_NO_SEARCH|SD_RESOLVED_CLAMP_TTL); + if (r < 0) + return r; + + q->bus_request = sd_bus_message_ref(message); + q->complete = bus_method_resolve_record_complete; + + r = dns_query_bus_track(q, message); + if (r < 0) + return r; + + r = dns_query_go(q); + if (r < 0) + return r; + + TAKE_PTR(q); + return 1; +} + +static int append_srv(DnsQuery *q, sd_bus_message *reply, DnsResourceRecord *rr) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL; + _cleanup_free_ char *normalized = NULL; + int r; + + assert(q); + assert(reply); + assert(rr); + assert(rr->key); + + if (rr->key->type != DNS_TYPE_SRV) + return 0; + + if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) { + /* First, let's see if we could find an appropriate A or AAAA + * record for the SRV record */ + LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) { + DnsResourceRecord *zz; + DnsQuestion *question; + + if (aux->state != DNS_TRANSACTION_SUCCESS) + continue; + if (aux->auxiliary_result != 0) + continue; + + question = dns_query_question_for_protocol(aux, aux->answer_protocol); + + r = dns_name_equal(dns_question_first_name(question), rr->srv.name); + if (r < 0) + return r; + if (r == 0) + continue; + + DNS_ANSWER_FOREACH(zz, aux->answer) { + + r = dns_question_matches_rr(question, zz, NULL); + if (r < 0) + return r; + if (r == 0) + continue; + + canonical = dns_resource_record_ref(zz); + break; + } + + if (canonical) + break; + } + + /* Is there are successful A/AAAA lookup for this SRV RR? If not, don't add it */ + if (!canonical) + return 0; + } + + r = sd_bus_message_open_container(reply, 'r', "qqqsa(iiay)s"); + if (r < 0) + return r; + + r = dns_name_normalize(rr->srv.name, 0, &normalized); + if (r < 0) + return r; + + r = sd_bus_message_append( + reply, + "qqqs", + rr->srv.priority, rr->srv.weight, rr->srv.port, normalized); + if (r < 0) + return r; + + r = sd_bus_message_open_container(reply, 'a', "(iiay)"); + if (r < 0) + return r; + + if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) { + LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) { + DnsResourceRecord *zz; + DnsQuestion *question; + int ifindex; + + if (aux->state != DNS_TRANSACTION_SUCCESS) + continue; + if (aux->auxiliary_result != 0) + continue; + + question = dns_query_question_for_protocol(aux, aux->answer_protocol); + + r = dns_name_equal(dns_question_first_name(question), rr->srv.name); + if (r < 0) + return r; + if (r == 0) + continue; + + DNS_ANSWER_FOREACH_IFINDEX(zz, ifindex, aux->answer) { + + r = dns_question_matches_rr(question, zz, NULL); + if (r < 0) + return r; + if (r == 0) + continue; + + r = append_address(reply, zz, ifindex); + if (r < 0) + return r; + } + } + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + if (canonical) { + normalized = mfree(normalized); + + r = dns_name_normalize(dns_resource_key_name(canonical->key), 0, &normalized); + if (r < 0) + return r; + } + + /* Note that above we appended the hostname as encoded in the + * SRV, and here the canonical hostname this maps to. */ + r = sd_bus_message_append(reply, "s", normalized); + if (r < 0) + return r; + + r = sd_bus_message_close_container(reply); + if (r < 0) + return r; + + return 1; +} + +static int append_txt(sd_bus_message *reply, DnsResourceRecord *rr) { + int r; + + assert(reply); + assert(rr); + assert(rr->key); + + if (rr->key->type != DNS_TYPE_TXT) + return 0; + + LIST_FOREACH(items, i, rr->txt.items) { + + if (i->length <= 0) + continue; + + r = sd_bus_message_append_array(reply, 'y', i->data, i->length); + if (r < 0) + return r; + } + + return 1; +} + +static void resolve_service_all_complete(DnsQuery *query) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL; + _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL; + _cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = query; + DnsQuestion *question; + DnsResourceRecord *rr; + unsigned added = 0; + int r; + + assert(q); + + if (q->block_all_complete > 0) { + TAKE_PTR(q); + return; + } + + if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) { + DnsQuery *bad = NULL; + bool have_success = false; + + LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) { + + switch (aux->state) { + + case DNS_TRANSACTION_PENDING: + /* If an auxiliary query is still pending, let's wait */ + TAKE_PTR(q); + return; + + case DNS_TRANSACTION_SUCCESS: + if (aux->auxiliary_result == 0) + have_success = true; + else + bad = aux; + break; + + default: + bad = aux; + break; + } + } + + if (!have_success) { + /* We can only return one error, hence pick the last error we encountered */ + + assert(bad); + + if (bad->state == DNS_TRANSACTION_SUCCESS) { + assert(bad->auxiliary_result != 0); + + if (bad->auxiliary_result == -ELOOP) { + r = reply_method_errorf(q, BUS_ERROR_CNAME_LOOP, "CNAME loop detected, or CNAME resolving disabled on '%s'", dns_query_string(bad)); + goto finish; + } + + assert(bad->auxiliary_result < 0); + r = bad->auxiliary_result; + goto finish; + } + + r = reply_query_state(bad); + goto finish; + } + } + + r = sd_bus_message_new_method_return(q->bus_request, &reply); + if (r < 0) + goto finish; + + r = sd_bus_message_open_container(reply, 'a', "(qqqsa(iiay)s)"); + if (r < 0) + goto finish; + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + DNS_ANSWER_FOREACH(rr, q->answer) { + r = dns_question_matches_rr(question, rr, NULL); + if (r < 0) + goto finish; + if (r == 0) + continue; + + r = append_srv(q, reply, rr); + if (r < 0) + goto finish; + if (r == 0) /* not an SRV record */ + continue; + + if (!canonical) + canonical = dns_resource_record_ref(rr); + + added++; + } + + if (added <= 0) { + r = reply_method_errorf(q, BUS_ERROR_NO_SUCH_RR, "'%s' does not have any RR of the requested type", dns_query_string(q)); + goto finish; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + goto finish; + + r = sd_bus_message_open_container(reply, 'a', "ay"); + if (r < 0) + goto finish; + + DNS_ANSWER_FOREACH(rr, q->answer) { + r = dns_question_matches_rr(question, rr, NULL); + if (r < 0) + goto finish; + if (r == 0) + continue; + + r = append_txt(reply, rr); + if (r < 0) + goto finish; + } + + r = sd_bus_message_close_container(reply); + if (r < 0) + goto finish; + + assert(canonical); + r = dns_service_split(dns_resource_key_name(canonical->key), &name, &type, &domain); + if (r < 0) + goto finish; + + r = sd_bus_message_append( + reply, + "ssst", + name, type, domain, + dns_query_reply_flags_make(q)); + if (r < 0) + goto finish; + + q->bus_request = sd_bus_message_unref(q->bus_request); + r = sd_bus_send(q->manager->bus, reply, NULL); + +finish: + if (r < 0) { + log_error_errno(r, "Failed to send service reply: %m"); + (void) reply_method_errnof(q, r, NULL); + } +} + +static void resolve_service_hostname_complete(DnsQuery *q) { + int r; + + assert(q); + assert(q->auxiliary_for); + + if (q->state != DNS_TRANSACTION_SUCCESS) { + resolve_service_all_complete(q->auxiliary_for); + return; + } + + r = dns_query_process_cname_many(q); + if (r == DNS_QUERY_CNAME) /* This was a cname, and the query was restarted. */ + return; + + /* This auxiliary lookup is finished or failed, let's see if all are finished now. */ + q->auxiliary_result = r < 0 ? r : 0; + resolve_service_all_complete(q->auxiliary_for); +} + +static int resolve_service_hostname(DnsQuery *q, DnsResourceRecord *rr, int ifindex) { + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + _cleanup_(dns_query_freep) DnsQuery *aux = NULL; + int r; + + assert(q); + assert(rr); + assert(rr->key); + assert(rr->key->type == DNS_TYPE_SRV); + + /* OK, we found an SRV record for the service. Let's resolve + * the hostname included in it */ + + r = dns_question_new_address(&question, q->request_family, rr->srv.name, false); + if (r < 0) + return r; + + r = dns_query_new(q->manager, &aux, question, question, NULL, ifindex, q->flags|SD_RESOLVED_NO_SEARCH); + if (r < 0) + return r; + + aux->request_family = q->request_family; + aux->complete = resolve_service_hostname_complete; + + r = dns_query_make_auxiliary(aux, q); + if (r == -EAGAIN) + /* Too many auxiliary lookups? If so, don't complain, + * let's just not add this one, we already have more + * than enough */ + return 0; + if (r < 0) + return r; + + /* Note that auxiliary queries do not track the original bus + * client, only the primary request does that. */ + + r = dns_query_go(aux); + if (r < 0) + return r; + + TAKE_PTR(aux); + return 1; +} + +static void bus_method_resolve_service_complete(DnsQuery *query) { + _cleanup_(dns_query_freep) DnsQuery *q = query; + bool has_root_domain = false; + DnsResourceRecord *rr; + DnsQuestion *question; + unsigned found = 0; + int ifindex, r; + + assert(q); + + if (q->state != DNS_TRANSACTION_SUCCESS) { + r = reply_query_state(q); + goto finish; + } + + r = dns_query_process_cname_many(q); + if (r == -ELOOP) { + r = reply_method_errorf(q, BUS_ERROR_CNAME_LOOP, "CNAME loop detected, or CNAME resolving disabled on '%s'", dns_query_string(q)); + goto finish; + } + if (r < 0) + goto finish; + if (r == DNS_QUERY_CNAME) { + /* This was a cname, and the query was restarted. */ + TAKE_PTR(q); + return; + } + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) { + r = dns_question_matches_rr(question, rr, NULL); + if (r < 0) + goto finish; + if (r == 0) + continue; + + if (rr->key->type != DNS_TYPE_SRV) + continue; + + if (dns_name_is_root(rr->srv.name)) { + has_root_domain = true; + continue; + } + + if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) { + q->block_all_complete++; + r = resolve_service_hostname(q, rr, ifindex); + q->block_all_complete--; + + if (r < 0) + goto finish; + } + + found++; + } + + if (has_root_domain && found <= 0) { + /* If there's exactly one SRV RR and it uses the root domain as hostname, then the service is + * explicitly not offered on the domain. Report this as a recognizable error. See RFC 2782, + * Section "Usage Rules". */ + r = reply_method_errorf(q, BUS_ERROR_NO_SUCH_SERVICE, "'%s' does not provide the requested service", dns_query_string(q)); + goto finish; + } + + if (found <= 0) { + r = reply_method_errorf(q, BUS_ERROR_NO_SUCH_RR, "'%s' does not have any RR of the requested type", dns_query_string(q)); + goto finish; + } + + /* Maybe we are already finished? check now... */ + resolve_service_all_complete(TAKE_PTR(q)); + return; + +finish: + if (r < 0) { + log_error_errno(r, "Failed to send service reply: %m"); + (void) reply_method_errnof(q, r, NULL); + } +} + +static int bus_method_resolve_service(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + const char *name, *type, *domain; + Manager *m = ASSERT_PTR(userdata); + int family, ifindex; + uint64_t flags; + int r; + + assert(message); + + assert_cc(sizeof(int) == sizeof(int32_t)); + + r = sd_bus_message_read(message, "isssit", &ifindex, &name, &type, &domain, &family, &flags); + if (r < 0) + return r; + + if (ifindex < 0) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index"); + + if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Unknown address family %i", family); + + if (isempty(name)) + name = NULL; + else if (!dns_service_name_is_valid(name)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid service name '%s'", name); + + if (isempty(type)) + type = NULL; + else if (!dns_srv_type_is_valid(type)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid SRV service type '%s'", type); + + r = dns_name_is_valid(domain); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid domain '%s'", domain); + + if (name && !type) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Service name cannot be specified without service type."); + + r = validate_and_mangle_flags(name, &flags, SD_RESOLVED_NO_TXT|SD_RESOLVED_NO_ADDRESS, error); + if (r < 0) + return r; + + r = dns_question_new_service(&question_utf8, name, type, domain, !(flags & SD_RESOLVED_NO_TXT), false); + if (r < 0) + return r; + + r = dns_question_new_service(&question_idna, name, type, domain, !(flags & SD_RESOLVED_NO_TXT), true); + if (r < 0) + return r; + + bus_client_log(message, "service resolution"); + + r = dns_query_new(m, &q, question_utf8, question_idna, NULL, ifindex, flags|SD_RESOLVED_NO_SEARCH); + if (r < 0) + return r; + + q->bus_request = sd_bus_message_ref(message); + q->request_family = family; + q->complete = bus_method_resolve_service_complete; + + r = dns_query_bus_track(q, message); + if (r < 0) + return r; + + r = dns_query_go(q); + if (r < 0) + return r; + + TAKE_PTR(q); + return 1; +} + +int bus_dns_server_append(sd_bus_message *reply, DnsServer *s, bool with_ifindex, bool extended) { + int r; + + assert(reply); + + if (!s) { + if (with_ifindex) { + if (extended) + return sd_bus_message_append(reply, "(iiayqs)", 0, AF_UNSPEC, 0, 0, NULL); + else + return sd_bus_message_append(reply, "(iiay)", 0, AF_UNSPEC, 0); + } else { + if (extended) + return sd_bus_message_append(reply, "(iayqs)", AF_UNSPEC, 0, 0, NULL); + else + return sd_bus_message_append(reply, "(iay)", AF_UNSPEC, 0); + } + } + + r = sd_bus_message_open_container(reply, 'r', with_ifindex ? (extended ? "iiayqs" : "iiay") : (extended ? "iayqs" : "iay")); + if (r < 0) + return r; + + if (with_ifindex) { + r = sd_bus_message_append(reply, "i", dns_server_ifindex(s)); + if (r < 0) + return r; + } + + r = sd_bus_message_append(reply, "i", s->family); + if (r < 0) + return r; + + r = sd_bus_message_append_array(reply, 'y', &s->address, FAMILY_ADDRESS_SIZE(s->family)); + if (r < 0) + return r; + + if (extended) { + r = sd_bus_message_append(reply, "q", s->port); + if (r < 0) + return r; + + r = sd_bus_message_append(reply, "s", s->server_name); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int bus_property_get_dns_servers_internal( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error, + bool extended) { + + Manager *m = ASSERT_PTR(userdata); + Link *l; + int r; + + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', extended ? "(iiayqs)" : "(iiay)"); + if (r < 0) + return r; + + LIST_FOREACH(servers, s, m->dns_servers) { + r = bus_dns_server_append(reply, s, true, extended); + if (r < 0) + return r; + } + + HASHMAP_FOREACH(l, m->links) + LIST_FOREACH(servers, s, l->dns_servers) { + r = bus_dns_server_append(reply, s, true, extended); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int bus_property_get_dns_servers( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return bus_property_get_dns_servers_internal(bus, path, interface, property, reply, userdata, error, false); +} + +static int bus_property_get_dns_servers_ex( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return bus_property_get_dns_servers_internal(bus, path, interface, property, reply, userdata, error, true); +} + +static int bus_property_get_fallback_dns_servers_internal( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error, + bool extended) { + + DnsServer **f = ASSERT_PTR(userdata); + int r; + + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', extended ? "(iiayqs)" : "(iiay)"); + if (r < 0) + return r; + + LIST_FOREACH(servers, s, *f) { + r = bus_dns_server_append(reply, s, true, extended); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int bus_property_get_fallback_dns_servers( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return bus_property_get_fallback_dns_servers_internal(bus, path, interface, property, reply, userdata, error, false); +} + +static int bus_property_get_fallback_dns_servers_ex( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return bus_property_get_fallback_dns_servers_internal(bus, path, interface, property, reply, userdata, error, true); +} + +static int bus_property_get_current_dns_server_internal( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error, + bool extended) { + + DnsServer *s; + + assert(reply); + assert(userdata); + + s = *(DnsServer **) userdata; + + return bus_dns_server_append(reply, s, true, extended); +} + +static int bus_property_get_current_dns_server( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return bus_property_get_current_dns_server_internal(bus, path, interface, property, reply, userdata, error, false); +} + +static int bus_property_get_current_dns_server_ex( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return bus_property_get_current_dns_server_internal(bus, path, interface, property, reply, userdata, error, true); +} + +static int bus_property_get_domains( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + Link *l; + int r; + + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "(isb)"); + if (r < 0) + return r; + + LIST_FOREACH(domains, d, m->search_domains) { + r = sd_bus_message_append(reply, "(isb)", 0, d->name, d->route_only); + if (r < 0) + return r; + } + + HASHMAP_FOREACH(l, m->links) { + LIST_FOREACH(domains, d, l->search_domains) { + r = sd_bus_message_append(reply, "(isb)", l->ifindex, d->name, d->route_only); + if (r < 0) + return r; + } + } + + return sd_bus_message_close_container(reply); +} + +static int bus_property_get_transaction_statistics( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + + assert(reply); + + return sd_bus_message_append(reply, "(tt)", + (uint64_t) hashmap_size(m->dns_transactions), + (uint64_t) m->n_transactions_total); +} + +static int bus_property_get_cache_statistics( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + uint64_t size = 0, hit = 0, miss = 0; + Manager *m = ASSERT_PTR(userdata); + + assert(reply); + + LIST_FOREACH(scopes, s, m->dns_scopes) { + size += dns_cache_size(&s->cache); + hit += s->cache.n_hit; + miss += s->cache.n_miss; + } + + return sd_bus_message_append(reply, "(ttt)", size, hit, miss); +} + +static int bus_property_get_dnssec_statistics( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + + assert(reply); + + return sd_bus_message_append(reply, "(tttt)", + (uint64_t) m->n_dnssec_verdict[DNSSEC_SECURE], + (uint64_t) m->n_dnssec_verdict[DNSSEC_INSECURE], + (uint64_t) m->n_dnssec_verdict[DNSSEC_BOGUS], + (uint64_t) m->n_dnssec_verdict[DNSSEC_INDETERMINATE]); +} + +static int bus_property_get_ntas( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Manager *m = ASSERT_PTR(userdata); + const char *domain; + int r; + + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "s"); + if (r < 0) + return r; + + SET_FOREACH(domain, m->trust_anchor.negative_by_name) { + r = sd_bus_message_append(reply, "s", domain); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static BUS_DEFINE_PROPERTY_GET_ENUM(bus_property_get_dns_stub_listener_mode, dns_stub_listener_mode, DnsStubListenerMode); +static BUS_DEFINE_PROPERTY_GET(bus_property_get_dnssec_supported, "b", Manager, manager_dnssec_supported); +static BUS_DEFINE_PROPERTY_GET2(bus_property_get_dnssec_mode, "s", Manager, manager_get_dnssec_mode, dnssec_mode_to_string); +static BUS_DEFINE_PROPERTY_GET2(bus_property_get_dns_over_tls_mode, "s", Manager, manager_get_dns_over_tls_mode, dns_over_tls_mode_to_string); + +static int bus_property_get_resolv_conf_mode( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + int r; + + assert(reply); + + r = resolv_conf_mode(); + if (r < 0) { + log_warning_errno(r, "Failed to test /etc/resolv.conf mode, ignoring: %m"); + return sd_bus_message_append(reply, "s", NULL); + } + + return sd_bus_message_append(reply, "s", resolv_conf_mode_to_string(r)); +} + +static int bus_method_reset_statistics(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + + assert(message); + + bus_client_log(message, "statistics reset"); + + LIST_FOREACH(scopes, s, m->dns_scopes) + s->cache.n_hit = s->cache.n_miss = 0; + + m->n_transactions_total = 0; + zero(m->n_dnssec_verdict); + + return sd_bus_reply_method_return(message, NULL); +} + +static int get_any_link(Manager *m, int ifindex, Link **ret, sd_bus_error *error) { + Link *l; + + assert(m); + assert(ret); + + l = hashmap_get(m->links, INT_TO_PTR(ifindex)); + if (!l) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %i not known", ifindex); + + *ret = l; + return 0; +} + +static int call_link_method(Manager *m, sd_bus_message *message, sd_bus_message_handler_t handler, sd_bus_error *error) { + int ifindex, r; + Link *l; + + assert(m); + assert(message); + assert(handler); + + r = bus_message_read_ifindex(message, error, &ifindex); + if (r < 0) + return r; + + r = get_any_link(m, ifindex, &l, error); + if (r < 0) + return r; + + return handler(message, l, error); +} + +static int bus_method_set_link_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dns_servers, error); +} + +static int bus_method_set_link_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dns_servers_ex, error); +} + +static int bus_method_set_link_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_domains, error); +} + +static int bus_method_set_link_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_default_route, error); +} + +static int bus_method_set_link_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_llmnr, error); +} + +static int bus_method_set_link_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_mdns, error); +} + +static int bus_method_set_link_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dns_over_tls, error); +} + +static int bus_method_set_link_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dnssec, error); +} + +static int bus_method_set_link_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_set_dnssec_negative_trust_anchors, error); +} + +static int bus_method_revert_link(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return call_link_method(userdata, message, bus_link_method_revert, error); +} + +static int bus_method_get_link(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_free_ char *p = NULL; + Manager *m = ASSERT_PTR(userdata); + int r, ifindex; + Link *l; + + assert(message); + + r = bus_message_read_ifindex(message, error, &ifindex); + if (r < 0) + return r; + + r = get_any_link(m, ifindex, &l, error); + if (r < 0) + return r; + + p = link_bus_path(l); + if (!p) + return -ENOMEM; + + return sd_bus_reply_method_return(message, "o", p); +} + +static int bus_method_flush_caches(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + + assert(message); + + bus_client_log(message, "cache flush"); + + manager_flush_caches(m, LOG_INFO); + + return sd_bus_reply_method_return(message, NULL); +} + +static int bus_method_reset_server_features(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + + assert(message); + + bus_client_log(message, "server feature reset"); + + manager_reset_server_features(m); + + return sd_bus_reply_method_return(message, NULL); +} + +static int dnssd_service_on_bus_track(sd_bus_track *t, void *userdata) { + DnssdService *s = ASSERT_PTR(userdata); + + assert(t); + + log_debug("Client of active request vanished, destroying DNS-SD service."); + dnssd_service_free(s); + + return 0; +} + +static int bus_method_register_service(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + _cleanup_(dnssd_service_freep) DnssdService *service = NULL; + _cleanup_(sd_bus_track_unrefp) sd_bus_track *bus_track = NULL; + const char *name, *name_template, *type; + _cleanup_free_ char *path = NULL; + DnssdService *s = NULL; + Manager *m = ASSERT_PTR(userdata); + uid_t euid; + int r; + + assert(message); + + if (m->mdns_support != RESOLVE_SUPPORT_YES) + return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Support for MulticastDNS is disabled"); + + service = new0(DnssdService, 1); + if (!service) + return log_oom(); + + r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds); + if (r < 0) + return r; + + r = sd_bus_creds_get_euid(creds, &euid); + if (r < 0) + return r; + service->originator = euid; + + r = sd_bus_message_read(message, "sssqqq", &name, &name_template, &type, + &service->port, &service->priority, + &service->weight); + if (r < 0) + return r; + + s = hashmap_get(m->dnssd_services, name); + if (s) + return sd_bus_error_setf(error, BUS_ERROR_DNSSD_SERVICE_EXISTS, "DNS-SD service '%s' exists already", name); + + if (!dnssd_srv_type_is_valid(type)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "DNS-SD service type '%s' is invalid", type); + + service->name = strdup(name); + if (!service->name) + return log_oom(); + + service->name_template = strdup(name_template); + if (!service->name_template) + return log_oom(); + + service->type = strdup(type); + if (!service->type) + return log_oom(); + + r = dnssd_render_instance_name(m, service, NULL); + if (r < 0) + return r; + + r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "a{say}"); + if (r < 0) + return r; + + while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{say}")) > 0) { + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + DnsTxtItem *last = NULL; + + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "say")) > 0) { + const char *key; + const void *value; + size_t size; + DnsTxtItem *i; + + r = sd_bus_message_read(message, "s", &key); + if (r < 0) + return r; + + if (isempty(key)) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Keys in DNS-SD TXT RRs can't be empty"); + + if (!ascii_is_valid(key)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "TXT key '%s' contains non-ASCII symbols", key); + + r = sd_bus_message_read_array(message, 'y', &value, &size); + if (r < 0) + return r; + + r = dnssd_txt_item_new_from_data(key, value, size, &i); + if (r < 0) + return r; + + LIST_INSERT_AFTER(items, txt_data->txts, last, i); + last = i; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (txt_data->txts) { + LIST_PREPEND(items, service->txt_data_items, txt_data); + txt_data = NULL; + } + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!service->txt_data_items) { + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + r = dns_txt_item_new_empty(&txt_data->txts); + if (r < 0) + return r; + + LIST_PREPEND(items, service->txt_data_items, txt_data); + txt_data = NULL; + } + + r = sd_bus_path_encode("/org/freedesktop/resolve1/dnssd", service->name, &path); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_SYS_ADMIN, + "org.freedesktop.resolve1.register-service", + NULL, false, UID_INVALID, + &m->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + r = hashmap_ensure_put(&m->dnssd_services, &string_hash_ops, service->name, service); + if (r < 0) + return r; + + r = sd_bus_track_new(sd_bus_message_get_bus(message), &bus_track, dnssd_service_on_bus_track, service); + if (r < 0) + return r; + + r = sd_bus_track_add_sender(bus_track, message); + if (r < 0) + return r; + + service->manager = m; + + service = NULL; + + manager_refresh_rrs(m); + + return sd_bus_reply_method_return(message, "o", path); +} + +static int call_dnssd_method(Manager *m, sd_bus_message *message, sd_bus_message_handler_t handler, sd_bus_error *error) { + _cleanup_free_ char *name = NULL; + DnssdService *s = NULL; + const char *path; + int r; + + assert(m); + assert(message); + assert(handler); + + r = sd_bus_message_read(message, "o", &path); + if (r < 0) + return r; + + r = sd_bus_path_decode(path, "/org/freedesktop/resolve1/dnssd", &name); + if (r == 0) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_DNSSD_SERVICE, "DNS-SD service with object path '%s' does not exist", path); + if (r < 0) + return r; + + s = hashmap_get(m->dnssd_services, name); + if (!s) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_DNSSD_SERVICE, "DNS-SD service '%s' not known", name); + + return handler(message, s, error); +} + +static int bus_method_unregister_service(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = ASSERT_PTR(userdata); + + assert(message); + + return call_dnssd_method(m, message, bus_dnssd_method_unregister, error); +} + +static const sd_bus_vtable resolve_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_PROPERTY("LLMNRHostname", "s", NULL, offsetof(Manager, llmnr_hostname), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("LLMNR", "s", bus_property_get_resolve_support, offsetof(Manager, llmnr_support), 0), + SD_BUS_PROPERTY("MulticastDNS", "s", bus_property_get_resolve_support, offsetof(Manager, mdns_support), 0), + SD_BUS_PROPERTY("DNSOverTLS", "s", bus_property_get_dns_over_tls_mode, 0, 0), + SD_BUS_PROPERTY("DNS", "a(iiay)", bus_property_get_dns_servers, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("DNSEx", "a(iiayqs)", bus_property_get_dns_servers_ex, 0, SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("FallbackDNS", "a(iiay)", bus_property_get_fallback_dns_servers, offsetof(Manager, fallback_dns_servers), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("FallbackDNSEx", "a(iiayqs)", bus_property_get_fallback_dns_servers_ex, offsetof(Manager, fallback_dns_servers), SD_BUS_VTABLE_PROPERTY_CONST), + SD_BUS_PROPERTY("CurrentDNSServer", "(iiay)", bus_property_get_current_dns_server, offsetof(Manager, current_dns_server), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("CurrentDNSServerEx", "(iiayqs)", bus_property_get_current_dns_server_ex, offsetof(Manager, current_dns_server), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE), + SD_BUS_PROPERTY("Domains", "a(isb)", bus_property_get_domains, 0, 0), + SD_BUS_PROPERTY("TransactionStatistics", "(tt)", bus_property_get_transaction_statistics, 0, 0), + SD_BUS_PROPERTY("CacheStatistics", "(ttt)", bus_property_get_cache_statistics, 0, 0), + SD_BUS_PROPERTY("DNSSEC", "s", bus_property_get_dnssec_mode, 0, 0), + SD_BUS_PROPERTY("DNSSECStatistics", "(tttt)", bus_property_get_dnssec_statistics, 0, 0), + SD_BUS_PROPERTY("DNSSECSupported", "b", bus_property_get_dnssec_supported, 0, 0), + SD_BUS_PROPERTY("DNSSECNegativeTrustAnchors", "as", bus_property_get_ntas, 0, 0), + SD_BUS_PROPERTY("DNSStubListener", "s", bus_property_get_dns_stub_listener_mode, offsetof(Manager, dns_stub_listener_mode), 0), + SD_BUS_PROPERTY("ResolvConfMode", "s", bus_property_get_resolv_conf_mode, 0, 0), + + SD_BUS_METHOD_WITH_ARGS("ResolveHostname", + SD_BUS_ARGS("i", ifindex, "s", name, "i", family, "t", flags), + SD_BUS_RESULT("a(iiay)", addresses, "s", canonical, "t", flags), + bus_method_resolve_hostname, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ResolveAddress", + SD_BUS_ARGS("i", ifindex, "i", family, "ay", address, "t", flags), + SD_BUS_RESULT("a(is)", names, "t", flags), + bus_method_resolve_address, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ResolveRecord", + SD_BUS_ARGS("i", ifindex, "s", name, "q", class, "q", type, "t", flags), + SD_BUS_RESULT("a(iqqay)", records, "t", flags), + bus_method_resolve_record, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ResolveService", + SD_BUS_ARGS("i", ifindex, + "s", name, + "s", type, + "s", domain, + "i", family, + "t", flags), + SD_BUS_RESULT("a(qqqsa(iiay)s)", srv_data, + "aay", txt_data, + "s", canonical_name, + "s", canonical_type, + "s", canonical_domain, + "t", flags), + bus_method_resolve_service, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("GetLink", + SD_BUS_ARGS("i", ifindex), + SD_BUS_RESULT("o", path), + bus_method_get_link, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNS", + SD_BUS_ARGS("i", ifindex, "a(iay)", addresses), + SD_BUS_NO_RESULT, + bus_method_set_link_dns_servers, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSEx", + SD_BUS_ARGS("i", ifindex, "a(iayqs)", addresses), + SD_BUS_NO_RESULT, + bus_method_set_link_dns_servers_ex, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDomains", + SD_BUS_ARGS("i", ifindex, "a(sb)", domains), + SD_BUS_NO_RESULT, + bus_method_set_link_domains, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDefaultRoute", + SD_BUS_ARGS("i", ifindex, "b", enable), + SD_BUS_NO_RESULT, + bus_method_set_link_default_route, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkLLMNR", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_llmnr, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkMulticastDNS", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_mdns, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSOverTLS", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_dns_over_tls, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSSEC", + SD_BUS_ARGS("i", ifindex, "s", mode), + SD_BUS_NO_RESULT, + bus_method_set_link_dnssec, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLinkDNSSECNegativeTrustAnchors", + SD_BUS_ARGS("i", ifindex, "as", names), + SD_BUS_NO_RESULT, + bus_method_set_link_dnssec_negative_trust_anchors, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RevertLink", + SD_BUS_ARGS("i", ifindex), + SD_BUS_NO_RESULT, + bus_method_revert_link, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("RegisterService", + SD_BUS_ARGS("s", name, + "s", name_template, + "s", type, + "q", service_port, + "q", service_priority, + "q", service_weight, + "aa{say}", txt_datas), + SD_BUS_RESULT("o", service_path), + bus_method_register_service, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("UnregisterService", + SD_BUS_ARGS("o", service_path), + SD_BUS_NO_RESULT, + bus_method_unregister_service, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ResetStatistics", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_method_reset_statistics, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("FlushCaches", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_method_flush_caches, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("ResetServerFeatures", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_method_reset_server_features, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END, +}; + +const BusObjectImplementation manager_object = { + "/org/freedesktop/resolve1", + "org.freedesktop.resolve1.Manager", + .vtables = BUS_VTABLES(resolve_vtable), + .children = BUS_IMPLEMENTATIONS(&link_object, + &dnssd_object), +}; + +static int match_prepare_for_sleep(sd_bus_message *message, void *userdata, sd_bus_error *ret_error) { + Manager *m = ASSERT_PTR(userdata); + int b, r; + + assert(message); + + r = sd_bus_message_read(message, "b", &b); + if (r < 0) { + bus_log_parse_error(r); + return 0; + } + + if (b) + return 0; + + log_debug("Coming back from suspend, verifying all RRs..."); + + manager_verify_all(m); + return 0; +} + +int manager_connect_bus(Manager *m) { + int r; + + assert(m); + + if (m->bus) + return 0; + + r = bus_open_system_watch_bind_with_description(&m->bus, "bus-api-resolve"); + if (r < 0) + return log_error_errno(r, "Failed to connect to system bus: %m"); + + r = bus_add_implementation(m->bus, &manager_object, m); + if (r < 0) + return r; + + r = bus_log_control_api_register(m->bus); + if (r < 0) + return r; + + r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.resolve1", 0, NULL, NULL); + if (r < 0) + return log_error_errno(r, "Failed to request name: %m"); + + r = sd_bus_attach_event(m->bus, m->event, 0); + if (r < 0) + return log_error_errno(r, "Failed to attach bus to event loop: %m"); + + r = sd_bus_match_signal_async( + m->bus, + NULL, + "org.freedesktop.login1", + "/org/freedesktop/login1", + "org.freedesktop.login1.Manager", + "PrepareForSleep", + match_prepare_for_sleep, + NULL, + m); + if (r < 0) + log_warning_errno(r, "Failed to request match for PrepareForSleep, ignoring: %m"); + + return 0; +} + +int _manager_send_changed(Manager *manager, const char *property, ...) { + assert(manager); + + if (sd_bus_is_ready(manager->bus) <= 0) + return 0; + + char **l = strv_from_stdarg_alloca(property); + + int r = sd_bus_emit_properties_changed_strv( + manager->bus, + "/org/freedesktop/resolve1", + "org.freedesktop.resolve1.Manager", + l); + if (r < 0) + log_notice_errno(r, "Failed to emit notification about changed property %s: %m", property); + return r; +} diff --git a/src/resolve/resolved-bus.h b/src/resolve/resolved-bus.h new file mode 100644 index 0000000..6c2bd26 --- /dev/null +++ b/src/resolve/resolved-bus.h @@ -0,0 +1,17 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "bus-object.h" +#include "resolved-manager.h" + +extern const BusObjectImplementation manager_object; + +int manager_connect_bus(Manager *m); +int _manager_send_changed(Manager *manager, const char *property, ...) _sentinel_; +#define manager_send_changed(manager, ...) _manager_send_changed(manager, __VA_ARGS__, NULL) +int bus_dns_server_append(sd_bus_message *reply, DnsServer *s, bool with_ifindex, bool extended); +int bus_property_get_resolve_support(sd_bus *bus, const char *path, const char *interface, + const char *property, sd_bus_message *reply, + void *userdata, sd_bus_error *error); + +void bus_client_log(sd_bus_message *m, const char *what); diff --git a/src/resolve/resolved-conf.c b/src/resolve/resolved-conf.c new file mode 100644 index 0000000..6fd193a --- /dev/null +++ b/src/resolve/resolved-conf.c @@ -0,0 +1,503 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "conf-parser.h" +#include "def.h" +#include "extract-word.h" +#include "hexdecoct.h" +#include "parse-util.h" +#include "resolved-conf.h" +#include "resolved-dnssd.h" +#include "resolved-manager.h" +#include "resolved-dns-search-domain.h" +#include "resolved-dns-stub.h" +#include "dns-domain.h" +#include "socket-netlink.h" +#include "specifier.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "utf8.h" + +DEFINE_CONFIG_PARSE_ENUM(config_parse_dns_stub_listener_mode, dns_stub_listener_mode, DnsStubListenerMode, "Failed to parse DNS stub listener mode setting"); + +static int manager_add_dns_server_by_string(Manager *m, DnsServerType type, const char *word) { + _cleanup_free_ char *server_name = NULL; + union in_addr_union address; + int family, r, ifindex = 0; + uint16_t port; + DnsServer *s; + + assert(m); + assert(word); + + r = in_addr_port_ifindex_name_from_string_auto(word, &family, &address, &port, &ifindex, &server_name); + if (r < 0) + return r; + + /* Silently filter out 0.0.0.0, 127.0.0.53, 127.0.0.54 (our own stub DNS listener) */ + if (!dns_server_address_valid(family, &address)) + return 0; + + /* By default, the port number is determined with the transaction feature level. + * See dns_transaction_port() and dns_server_port(). */ + if (IN_SET(port, 53, 853)) + port = 0; + + /* Filter out duplicates */ + s = dns_server_find(manager_get_first_dns_server(m, type), family, &address, port, ifindex, server_name); + if (s) { + /* Drop the marker. This is used to find the servers that ceased to exist, see + * manager_mark_dns_servers() and manager_flush_marked_dns_servers(). */ + dns_server_move_back_and_unmark(s); + return 0; + } + + return dns_server_new(m, NULL, type, NULL, family, &address, port, ifindex, server_name); +} + +int manager_parse_dns_server_string_and_warn(Manager *m, DnsServerType type, const char *string) { + int r; + + assert(m); + assert(string); + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&string, &word, NULL, 0); + if (r <= 0) + return r; + + r = manager_add_dns_server_by_string(m, type, word); + if (r < 0) + log_warning_errno(r, "Failed to add DNS server address '%s', ignoring: %m", word); + } +} + +static int manager_add_search_domain_by_string(Manager *m, const char *domain) { + DnsSearchDomain *d; + bool route_only; + int r; + + assert(m); + assert(domain); + + route_only = *domain == '~'; + if (route_only) + domain++; + + if (dns_name_is_root(domain) || streq(domain, "*")) { + route_only = true; + domain = "."; + } + + r = dns_search_domain_find(m->search_domains, domain, &d); + if (r < 0) + return r; + if (r > 0) + dns_search_domain_move_back_and_unmark(d); + else { + r = dns_search_domain_new(m, &d, DNS_SEARCH_DOMAIN_SYSTEM, NULL, domain); + if (r < 0) + return r; + } + + d->route_only = route_only; + return 0; +} + +int manager_parse_search_domains_and_warn(Manager *m, const char *string) { + int r; + + assert(m); + assert(string); + + for (;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&string, &word, NULL, EXTRACT_UNQUOTE); + if (r <= 0) + return r; + + r = manager_add_search_domain_by_string(m, word); + if (r < 0) + log_warning_errno(r, "Failed to add search domain '%s', ignoring: %m", word); + } +} + +int config_parse_dns_servers( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) + /* Empty assignment means clear the list */ + dns_server_unlink_all(manager_get_first_dns_server(m, ltype)); + else { + /* Otherwise, add to the list */ + r = manager_parse_dns_server_string_and_warn(m, ltype, rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse DNS server string '%s', ignoring.", rvalue); + return 0; + } + } + + /* If we have a manual setting, then we stop reading + * /etc/resolv.conf */ + if (ltype == DNS_SERVER_SYSTEM) + m->read_resolv_conf = false; + if (ltype == DNS_SERVER_FALLBACK) + m->need_builtin_fallbacks = false; + + return 0; +} + +int config_parse_search_domains( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + Manager *m = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) + /* Empty assignment means clear the list */ + dns_search_domain_unlink_all(m->search_domains); + else { + /* Otherwise, add to the list */ + r = manager_parse_search_domains_and_warn(m, rvalue); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse search domains string '%s', ignoring.", rvalue); + return 0; + } + } + + /* If we have a manual setting, then we stop reading + * /etc/resolv.conf */ + m->read_resolv_conf = false; + + return 0; +} + +int config_parse_dnssd_service_name( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + static const Specifier specifier_table[] = { + { 'a', specifier_architecture, NULL }, + { 'b', specifier_boot_id, NULL }, + { 'B', specifier_os_build_id, NULL }, + { 'H', specifier_hostname, NULL }, /* We will use specifier_dnssd_hostname(). */ + { 'm', specifier_machine_id, NULL }, + { 'o', specifier_os_id, NULL }, + { 'v', specifier_kernel_release, NULL }, + { 'w', specifier_os_version_id, NULL }, + { 'W', specifier_os_variant_id, NULL }, + {} + }; + DnssdService *s = ASSERT_PTR(userdata); + _cleanup_free_ char *name = NULL; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + s->name_template = mfree(s->name_template); + return 0; + } + + r = specifier_printf(rvalue, DNS_LABEL_MAX, specifier_table, NULL, NULL, &name); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid service instance name template '%s', ignoring assignment: %m", rvalue); + return 0; + } + + if (!dns_service_name_is_valid(name)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, + "Service instance name template '%s' renders to invalid name '%s'. Ignoring assignment.", + rvalue, name); + return 0; + } + + return free_and_strdup_warn(&s->name_template, rvalue); +} + +int config_parse_dnssd_service_type( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + DnssdService *s = ASSERT_PTR(userdata); + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + s->type = mfree(s->type); + return 0; + } + + if (!dnssd_srv_type_is_valid(rvalue)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Service type is invalid. Ignoring."); + return 0; + } + + r = free_and_strdup(&s->type, rvalue); + if (r < 0) + return log_oom(); + + return 0; +} + +int config_parse_dnssd_txt( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + DnssdService *s = ASSERT_PTR(userdata); + DnsTxtItem *last = NULL; + + assert(filename); + assert(lvalue); + assert(rvalue); + + if (isempty(rvalue)) { + /* Flush out collected items */ + s->txt_data_items = dnssd_txtdata_free_all(s->txt_data_items); + return 0; + } + + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + for (;;) { + _cleanup_free_ char *word = NULL, *key = NULL, *value = NULL; + _cleanup_free_ void *decoded = NULL; + size_t length = 0; + DnsTxtItem *i; + int r; + + r = extract_first_word(&rvalue, &word, NULL, + EXTRACT_UNQUOTE|EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_RELAX); + if (r == 0) + break; + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue); + return 0; + } + + r = split_pair(word, "=", &key, &value); + if (r == -ENOMEM) + return log_oom(); + if (r == -EINVAL) + key = TAKE_PTR(word); + + if (!ascii_is_valid(key)) { + log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid key, ignoring: %s", key); + continue; + } + + switch (ltype) { + + case DNS_TXT_ITEM_DATA: + if (value) { + r = unbase64mem(value, strlen(value), &decoded, &length); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Invalid base64 encoding, ignoring: %s", value); + continue; + } + } + + r = dnssd_txt_item_new_from_data(key, decoded, length, &i); + if (r < 0) + return log_oom(); + break; + + case DNS_TXT_ITEM_TEXT: + r = dnssd_txt_item_new_from_string(key, value, &i); + if (r < 0) + return log_oom(); + break; + + default: + assert_not_reached(); + } + + LIST_INSERT_AFTER(items, txt_data->txts, last, i); + last = i; + } + + if (txt_data->txts) { + LIST_PREPEND(items, s->txt_data_items, txt_data); + TAKE_PTR(txt_data); + } + + return 0; +} + +int config_parse_dns_stub_listener_extra( + const char *unit, + const char *filename, + unsigned line, + const char *section, + unsigned section_line, + const char *lvalue, + int ltype, + const char *rvalue, + void *data, + void *userdata) { + + _cleanup_free_ DnsStubListenerExtra *stub = NULL; + Manager *m = userdata; + const char *p; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(data); + + if (isempty(rvalue)) { + m->dns_extra_stub_listeners = ordered_set_free(m->dns_extra_stub_listeners); + return 0; + } + + r = dns_stub_listener_extra_new(m, &stub); + if (r < 0) + return log_oom(); + + p = startswith(rvalue, "udp:"); + if (p) + stub->mode = DNS_STUB_LISTENER_UDP; + else { + p = startswith(rvalue, "tcp:"); + if (p) + stub->mode = DNS_STUB_LISTENER_TCP; + else { + stub->mode = DNS_STUB_LISTENER_YES; + p = rvalue; + } + } + + r = in_addr_port_ifindex_name_from_string_auto(p, &stub->family, &stub->address, &stub->port, NULL, NULL); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to parse address in %s=%s, ignoring assignment: %m", + lvalue, rvalue); + return 0; + } + + r = ordered_set_ensure_put(&m->dns_extra_stub_listeners, &dns_stub_listener_extra_hash_ops, stub); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) { + log_syntax(unit, LOG_WARNING, filename, line, r, + "Failed to store %s=%s, ignoring assignment: %m", lvalue, rvalue); + return 0; + } + + TAKE_PTR(stub); + + return 0; +} + +int manager_parse_config_file(Manager *m) { + int r; + + assert(m); + + r = config_parse_many_nulstr( + PKGSYSCONFDIR "/resolved.conf", + CONF_PATHS_NULSTR("systemd/resolved.conf.d"), + "Resolve\0", + config_item_perf_lookup, resolved_gperf_lookup, + CONFIG_PARSE_WARN, + m, + NULL); + if (r < 0) + return r; + + if (m->need_builtin_fallbacks) { + r = manager_parse_dns_server_string_and_warn(m, DNS_SERVER_FALLBACK, DNS_SERVERS); + if (r < 0) + return r; + } + +#if !HAVE_OPENSSL_OR_GCRYPT + if (m->dnssec_mode != DNSSEC_NO) { + log_warning("DNSSEC option cannot be enabled or set to allow-downgrade when systemd-resolved is built without a cryptographic library. Turning off DNSSEC support."); + m->dnssec_mode = DNSSEC_NO; + } +#endif + +#if !ENABLE_DNS_OVER_TLS + if (m->dns_over_tls_mode != DNS_OVER_TLS_NO) { + log_warning("DNS-over-TLS option cannot be enabled or set to opportunistic when systemd-resolved is built without DNS-over-TLS support. Turning off DNS-over-TLS support."); + m->dns_over_tls_mode = DNS_OVER_TLS_NO; + } +#endif + return 0; + +} diff --git a/src/resolve/resolved-conf.h b/src/resolve/resolved-conf.h new file mode 100644 index 0000000..07ce259 --- /dev/null +++ b/src/resolve/resolved-conf.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "conf-parser.h" + +#include "resolved-dns-server.h" + +int manager_parse_config_file(Manager *m); + +int manager_parse_search_domains_and_warn(Manager *m, const char *string); +int manager_parse_dns_server_string_and_warn(Manager *m, DnsServerType type, const char *string); + +const struct ConfigPerfItem* resolved_gperf_lookup(const char *key, GPERF_LEN_TYPE length); +const struct ConfigPerfItem* resolved_dnssd_gperf_lookup(const char *key, GPERF_LEN_TYPE length); + +CONFIG_PARSER_PROTOTYPE(config_parse_dns_servers); +CONFIG_PARSER_PROTOTYPE(config_parse_search_domains); +CONFIG_PARSER_PROTOTYPE(config_parse_dns_stub_listener_mode); +CONFIG_PARSER_PROTOTYPE(config_parse_dnssd_service_name); +CONFIG_PARSER_PROTOTYPE(config_parse_dnssd_service_type); +CONFIG_PARSER_PROTOTYPE(config_parse_dnssd_txt); +CONFIG_PARSER_PROTOTYPE(config_parse_dns_stub_listener_extra); diff --git a/src/resolve/resolved-def.h b/src/resolve/resolved-def.h new file mode 100644 index 0000000..36092af --- /dev/null +++ b/src/resolve/resolved-def.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <inttypes.h> + +#include "time-util.h" + +/* Input + Output: The various protocols we can use */ +#define SD_RESOLVED_DNS (UINT64_C(1) << 0) +#define SD_RESOLVED_LLMNR_IPV4 (UINT64_C(1) << 1) +#define SD_RESOLVED_LLMNR_IPV6 (UINT64_C(1) << 2) +#define SD_RESOLVED_MDNS_IPV4 (UINT64_C(1) << 3) +#define SD_RESOLVED_MDNS_IPV6 (UINT64_C(1) << 4) + +/* Input: Don't follow CNAMEs/DNAMEs */ +#define SD_RESOLVED_NO_CNAME (UINT64_C(1) << 5) + +/* Input: When doing service (SRV) resolving, don't resolve associated mDNS-style TXT records */ +#define SD_RESOLVED_NO_TXT (UINT64_C(1) << 6) + +/* Input: When doing service (SRV) resolving, don't resolve A/AAA RR for included hostname */ +#define SD_RESOLVED_NO_ADDRESS (UINT64_C(1) << 7) + +/* Input: Don't apply search domain logic to request */ +#define SD_RESOLVED_NO_SEARCH (UINT64_C(1) << 8) + +/* Output: Result is authenticated */ +#define SD_RESOLVED_AUTHENTICATED (UINT64_C(1) << 9) + +/* Input: Don't DNSSEC validate request */ +#define SD_RESOLVED_NO_VALIDATE (UINT64_C(1) << 10) + +/* Input: Don't answer request from locally synthesized records (which includes /etc/hosts) */ +#define SD_RESOLVED_NO_SYNTHESIZE (UINT64_C(1) << 11) + +/* Input: Don't answer request from cache */ +#define SD_RESOLVED_NO_CACHE (UINT64_C(1) << 12) + +/* Input: Don't answer request from locally registered public LLMNR/mDNS RRs */ +#define SD_RESOLVED_NO_ZONE (UINT64_C(1) << 13) + +/* Input: Don't answer request from locally configured trust anchors. */ +#define SD_RESOLVED_NO_TRUST_ANCHOR (UINT64_C(1) << 14) + +/* Input: Don't go to network for this request */ +#define SD_RESOLVED_NO_NETWORK (UINT64_C(1) << 15) + +/* Input: Require that request is answered from a "primary" answer, i.e. not from RRs acquired as + * side-effect of a previous transaction */ +#define SD_RESOLVED_REQUIRE_PRIMARY (UINT64_C(1) << 16) + +/* Input: If reply is answered from cache, the TTLs will be adjusted by age of cache entry */ +#define SD_RESOLVED_CLAMP_TTL (UINT64_C(1) << 17) + +/* Output: Result was only sent via encrypted channels, or never left this system */ +#define SD_RESOLVED_CONFIDENTIAL (UINT64_C(1) << 18) + +/* Output: Result was (at least partially) synthesized locally */ +#define SD_RESOLVED_SYNTHETIC (UINT64_C(1) << 19) + +/* Output: Result was (at least partially) answered from cache */ +#define SD_RESOLVED_FROM_CACHE (UINT64_C(1) << 20) + +/* Output: Result was (at least partially) answered from local zone */ +#define SD_RESOLVED_FROM_ZONE (UINT64_C(1) << 21) + +/* Output: Result was (at least partially) answered from trust anchor */ +#define SD_RESOLVED_FROM_TRUST_ANCHOR (UINT64_C(1) << 22) + +/* Output: Result was (at least partially) answered from network */ +#define SD_RESOLVED_FROM_NETWORK (UINT64_C(1) << 23) + +#define SD_RESOLVED_LLMNR (SD_RESOLVED_LLMNR_IPV4|SD_RESOLVED_LLMNR_IPV6) +#define SD_RESOLVED_MDNS (SD_RESOLVED_MDNS_IPV4|SD_RESOLVED_MDNS_IPV6) +#define SD_RESOLVED_PROTOCOLS_ALL (SD_RESOLVED_MDNS|SD_RESOLVED_LLMNR|SD_RESOLVED_DNS) + +#define SD_RESOLVED_FROM_MASK (SD_RESOLVED_FROM_CACHE|SD_RESOLVED_FROM_ZONE|SD_RESOLVED_FROM_TRUST_ANCHOR|SD_RESOLVED_FROM_NETWORK) + +#define SD_RESOLVED_QUERY_TIMEOUT_USEC (120 * USEC_PER_SEC) diff --git a/src/resolve/resolved-dns-answer.c b/src/resolve/resolved-dns-answer.c new file mode 100644 index 0000000..3d42b0d --- /dev/null +++ b/src/resolve/resolved-dns-answer.c @@ -0,0 +1,850 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <stdio.h> + +#include "alloc-util.h" +#include "dns-domain.h" +#include "random-util.h" +#include "resolved-dns-answer.h" +#include "resolved-dns-dnssec.h" +#include "string-util.h" + +static DnsAnswerItem *dns_answer_item_free(DnsAnswerItem *item) { + if (!item) + return NULL; + + dns_resource_record_unref(item->rr); + dns_resource_record_unref(item->rrsig); + + return mfree(item); +} + +DEFINE_PRIVATE_TRIVIAL_REF_UNREF_FUNC(DnsAnswerItem, dns_answer_item, dns_answer_item_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsAnswerItem*, dns_answer_item_unref); + +static void dns_answer_item_hash_func(const DnsAnswerItem *a, struct siphash *state) { + assert(a); + assert(state); + + siphash24_compress(&a->ifindex, sizeof(a->ifindex), state); + + dns_resource_record_hash_func(a->rr, state); +} + +static int dns_answer_item_compare_func(const DnsAnswerItem *a, const DnsAnswerItem *b) { + int r; + + assert(a); + assert(b); + + r = CMP(a->ifindex, b->ifindex); + if (r != 0) + return r; + + return dns_resource_record_compare_func(a->rr, b->rr); +} + +DEFINE_PRIVATE_HASH_OPS_WITH_KEY_DESTRUCTOR( + dns_answer_item_hash_ops, + DnsAnswerItem, + dns_answer_item_hash_func, + dns_answer_item_compare_func, + dns_answer_item_unref); + +static int dns_answer_reserve_internal(DnsAnswer *a, size_t n) { + size_t m; + + assert(a); + assert(a->items); + + m = ordered_set_size(a->items); + assert(m <= UINT16_MAX); /* We can only place 64K RRs in an answer at max */ + + n = saturate_add(m, n, UINT16_MAX); + + /* Higher multipliers give slightly higher efficiency through hash collisions, but the gains + * quickly drop off after 2. */ + return ordered_set_reserve(a->items, n * 2); +} + +DnsAnswer *dns_answer_new(size_t n) { + _cleanup_ordered_set_free_ OrderedSet *s = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *a = NULL; + + if (n > UINT16_MAX) + n = UINT16_MAX; + + s = ordered_set_new(&dns_answer_item_hash_ops); + if (!s) + return NULL; + + a = new(DnsAnswer, 1); + if (!a) + return NULL; + + *a = (DnsAnswer) { + .n_ref = 1, + .items = TAKE_PTR(s), + }; + + if (dns_answer_reserve_internal(a, n) < 0) + return NULL; + + return TAKE_PTR(a); +} + +static DnsAnswer *dns_answer_free(DnsAnswer *a) { + assert(a); + + ordered_set_free(a->items); + return mfree(a); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsAnswer, dns_answer, dns_answer_free); + +static int dns_answer_add_raw( + DnsAnswer *a, + DnsResourceRecord *rr, + int ifindex, + DnsAnswerFlags flags, + DnsResourceRecord *rrsig) { + + _cleanup_(dns_answer_item_unrefp) DnsAnswerItem *item = NULL; + int r; + + assert(rr); + + if (!a) + return -ENOSPC; + + if (dns_answer_size(a) >= UINT16_MAX) + return -ENOSPC; + + item = new(DnsAnswerItem, 1); + if (!item) + return -ENOMEM; + + *item = (DnsAnswerItem) { + .n_ref = 1, + .rr = dns_resource_record_ref(rr), + .ifindex = ifindex, + .flags = flags, + .rrsig = dns_resource_record_ref(rrsig), + }; + + r = ordered_set_put(a->items, item); + if (r < 0) + return r; + + TAKE_PTR(item); + return 1; +} + +static int dns_answer_add_raw_all(DnsAnswer *a, DnsAnswer *source) { + DnsAnswerItem *item; + int r; + + DNS_ANSWER_FOREACH_ITEM(item, source) { + r = dns_answer_add_raw( + a, + item->rr, + item->ifindex, + item->flags, + item->rrsig); + if (r < 0) + return r; + } + + return 0; +} + +int dns_answer_add( + DnsAnswer *a, + DnsResourceRecord *rr, + int ifindex, + DnsAnswerFlags flags, + DnsResourceRecord *rrsig) { + + DnsAnswerItem tmp, *exist; + + assert(rr); + + if (!a) + return -ENOSPC; + if (a->n_ref > 1) + return -EBUSY; + + tmp = (DnsAnswerItem) { + .rr = rr, + .ifindex = ifindex, + }; + + exist = ordered_set_get(a->items, &tmp); + if (exist) { + /* There's already an RR of the same RRset in place! Let's see if the TTLs more or less + * match. We don't really care if they match precisely, but we do care whether one is 0 and + * the other is not. See RFC 2181, Section 5.2. */ + if ((rr->ttl == 0) != (exist->rr->ttl == 0)) + return -EINVAL; + + /* Entry already exists, keep the entry with the higher TTL. */ + if (rr->ttl > exist->rr->ttl) { + DNS_RR_REPLACE(exist->rr, dns_resource_record_ref(rr)); + + /* Update RRSIG and RR at the same time */ + if (rrsig) + DNS_RR_REPLACE(exist->rrsig, dns_resource_record_ref(rrsig)); + } + + exist->flags |= flags; + + if (rr->key->type == DNS_TYPE_RRSIG) { + /* If the rr is RRSIG, then move the rr to the end. */ + assert_se(ordered_set_remove(a->items, exist) == exist); + assert_se(ordered_set_put(a->items, exist) == 1); + } + return 0; + } + + return dns_answer_add_raw(a, rr, ifindex, flags, rrsig); +} + +static int dns_answer_add_all(DnsAnswer *a, DnsAnswer *b) { + DnsAnswerItem *item; + int r; + + DNS_ANSWER_FOREACH_ITEM(item, b) { + r = dns_answer_add(a, item->rr, item->ifindex, item->flags, item->rrsig); + if (r < 0) + return r; + } + + return 0; +} + +int dns_answer_add_extend( + DnsAnswer **a, + DnsResourceRecord *rr, + int ifindex, + DnsAnswerFlags flags, + DnsResourceRecord *rrsig) { + + int r; + + assert(a); + assert(rr); + + r = dns_answer_reserve_or_clone(a, 1); + if (r < 0) + return r; + + return dns_answer_add(*a, rr, ifindex, flags, rrsig); +} + +int dns_answer_add_soa(DnsAnswer *a, const char *name, uint32_t ttl, int ifindex) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *soa = NULL; + + soa = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_SOA, name); + if (!soa) + return -ENOMEM; + + soa->ttl = ttl; + + soa->soa.mname = strdup(name); + if (!soa->soa.mname) + return -ENOMEM; + + soa->soa.rname = strjoin("root.", name); + if (!soa->soa.rname) + return -ENOMEM; + + soa->soa.serial = 1; + soa->soa.refresh = 1; + soa->soa.retry = 1; + soa->soa.expire = 1; + soa->soa.minimum = ttl; + + return dns_answer_add(a, soa, ifindex, DNS_ANSWER_AUTHENTICATED, NULL); +} + +int dns_answer_match_key(DnsAnswer *a, const DnsResourceKey *key, DnsAnswerFlags *ret_flags) { + DnsAnswerFlags flags = 0, i_flags; + DnsResourceRecord *i; + bool found = false; + int r; + + assert(key); + + DNS_ANSWER_FOREACH_FLAGS(i, i_flags, a) { + r = dns_resource_key_match_rr(key, i, NULL); + if (r < 0) + return r; + if (r == 0) + continue; + + if (!ret_flags) + return 1; + + if (found) + flags &= i_flags; + else { + flags = i_flags; + found = true; + } + } + + if (ret_flags) + *ret_flags = flags; + + return found; +} + +bool dns_answer_contains_nsec_or_nsec3(DnsAnswer *a) { + DnsResourceRecord *i; + + DNS_ANSWER_FOREACH(i, a) + if (IN_SET(i->key->type, DNS_TYPE_NSEC, DNS_TYPE_NSEC3)) + return true; + + return false; +} + +int dns_answer_contains_zone_nsec3(DnsAnswer *answer, const char *zone) { + DnsResourceRecord *rr; + int r; + + /* Checks whether the specified answer contains at least one NSEC3 RR in the specified zone */ + + DNS_ANSWER_FOREACH(rr, answer) { + const char *p; + + if (rr->key->type != DNS_TYPE_NSEC3) + continue; + + p = dns_resource_key_name(rr->key); + r = dns_name_parent(&p); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dns_name_equal(p, zone); + if (r != 0) + return r; + } + + return false; +} + +bool dns_answer_contains(DnsAnswer *answer, DnsResourceRecord *rr) { + DnsResourceRecord *i; + + DNS_ANSWER_FOREACH(i, answer) + if (dns_resource_record_equal(i, rr)) + return true; + + return false; +} + +int dns_answer_find_soa( + DnsAnswer *a, + const DnsResourceKey *key, + DnsResourceRecord **ret, + DnsAnswerFlags *ret_flags) { + + DnsResourceRecord *rr, *soa = NULL; + DnsAnswerFlags rr_flags, soa_flags = 0; + int r; + + assert(key); + + /* For a SOA record we can never find a matching SOA record */ + if (key->type == DNS_TYPE_SOA) + goto not_found; + + DNS_ANSWER_FOREACH_FLAGS(rr, rr_flags, a) { + r = dns_resource_key_match_soa(key, rr->key); + if (r < 0) + return r; + if (r > 0) { + + if (soa) { + r = dns_name_endswith(dns_resource_key_name(rr->key), dns_resource_key_name(soa->key)); + if (r < 0) + return r; + if (r > 0) + continue; + } + + soa = rr; + soa_flags = rr_flags; + } + } + + if (!soa) + goto not_found; + + if (ret) + *ret = soa; + if (ret_flags) + *ret_flags = soa_flags; + + return 1; + +not_found: + if (ret) + *ret = NULL; + if (ret_flags) + *ret_flags = 0; + + return 0; +} + +int dns_answer_find_cname_or_dname( + DnsAnswer *a, + const DnsResourceKey *key, + DnsResourceRecord **ret, + DnsAnswerFlags *ret_flags) { + + DnsResourceRecord *rr; + DnsAnswerFlags rr_flags; + int r; + + assert(key); + + /* For a {C,D}NAME record we can never find a matching {C,D}NAME record */ + if (!dns_type_may_redirect(key->type)) + return 0; + + DNS_ANSWER_FOREACH_FLAGS(rr, rr_flags, a) { + r = dns_resource_key_match_cname_or_dname(key, rr->key, NULL); + if (r < 0) + return r; + if (r > 0) { + if (ret) + *ret = rr; + if (ret_flags) + *ret_flags = rr_flags; + return 1; + } + } + + if (ret) + *ret = NULL; + if (ret_flags) + *ret_flags = 0; + + return 0; +} + +int dns_answer_merge(DnsAnswer *a, DnsAnswer *b, DnsAnswer **ret) { + _cleanup_(dns_answer_unrefp) DnsAnswer *k = NULL; + int r; + + assert(ret); + + if (a == b) { + *ret = dns_answer_ref(a); + return 0; + } + + if (dns_answer_size(a) <= 0) { + *ret = dns_answer_ref(b); + return 0; + } + + if (dns_answer_size(b) <= 0) { + *ret = dns_answer_ref(a); + return 0; + } + + k = dns_answer_new(dns_answer_size(a) + dns_answer_size(b)); + if (!k) + return -ENOMEM; + + r = dns_answer_add_raw_all(k, a); + if (r < 0) + return r; + + r = dns_answer_add_all(k, b); + if (r < 0) + return r; + + *ret = TAKE_PTR(k); + + return 0; +} + +int dns_answer_extend(DnsAnswer **a, DnsAnswer *b) { + DnsAnswer *merged; + int r; + + assert(a); + + r = dns_answer_merge(*a, b, &merged); + if (r < 0) + return r; + + DNS_ANSWER_REPLACE(*a, merged); + return 0; +} + +int dns_answer_remove_by_key(DnsAnswer **a, const DnsResourceKey *key) { + DnsAnswerItem *item; + bool found = false; + int r; + + assert(a); + assert(key); + + /* Remove all entries matching the specified key from *a */ + + DNS_ANSWER_FOREACH_ITEM(item, *a) { + r = dns_resource_key_equal(item->rr->key, key); + if (r < 0) + return r; + if (r > 0) { + dns_answer_item_unref(ordered_set_remove((*a)->items, item)); + found = true; + } + } + + if (!found) + return 0; + + if (dns_answer_isempty(*a)) + *a = dns_answer_unref(*a); /* Return NULL for the empty answer */ + + return 1; +} + +int dns_answer_remove_by_rr(DnsAnswer **a, DnsResourceRecord *rr) { + _unused_ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr_ref = dns_resource_record_ref(rr); + DnsAnswerItem *item; + bool found = false; + int r; + + assert(a); + assert(rr); + + /* Remove all entries matching the specified RR from *a */ + + DNS_ANSWER_FOREACH_ITEM(item, *a) { + r = dns_resource_record_equal(item->rr, rr); + if (r < 0) + return r; + if (r > 0) { + dns_answer_item_unref(ordered_set_remove((*a)->items, item)); + found = true; + } + } + + if (!found) + return 0; + + if (dns_answer_isempty(*a)) + *a = dns_answer_unref(*a); /* Return NULL for the empty answer */ + + return 1; +} + +int dns_answer_remove_by_answer_keys(DnsAnswer **a, DnsAnswer *b) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *prev = NULL; + DnsAnswerItem *item; + int r; + + /* Removes all items from '*a' that have a matching key in 'b' */ + + DNS_ANSWER_FOREACH_ITEM(item, b) { + + if (prev && dns_resource_key_equal(item->rr->key, prev)) /* Skip this one, we already looked at it */ + continue; + + r = dns_answer_remove_by_key(a, item->rr->key); + if (r < 0) + return r; + if (!*a) + return 0; /* a is already empty. */ + + /* Let's remember this entry's RR key, to optimize the loop a bit: if we have an RRset with + * more than one item then we don't need to remove the key multiple times */ + DNS_RESOURCE_KEY_REPLACE(prev, dns_resource_key_ref(item->rr->key)); + } + + return 0; +} + +int dns_answer_copy_by_key( + DnsAnswer **a, + DnsAnswer *source, + const DnsResourceKey *key, + DnsAnswerFlags or_flags, + DnsResourceRecord *rrsig) { + + DnsAnswerItem *item; + int r; + + assert(a); + assert(key); + + /* Copy all RRs matching the specified key from source into *a */ + + DNS_ANSWER_FOREACH_ITEM(item, source) { + + r = dns_resource_key_equal(item->rr->key, key); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dns_answer_add_extend(a, item->rr, item->ifindex, item->flags|or_flags, rrsig ?: item->rrsig); + if (r < 0) + return r; + } + + return 0; +} + +int dns_answer_move_by_key( + DnsAnswer **to, + DnsAnswer **from, + const DnsResourceKey *key, + DnsAnswerFlags or_flags, + DnsResourceRecord *rrsig) { + + int r; + + assert(to); + assert(from); + assert(key); + + r = dns_answer_copy_by_key(to, *from, key, or_flags, rrsig); + if (r < 0) + return r; + + return dns_answer_remove_by_key(from, key); +} + +void dns_answer_order_by_scope(DnsAnswer *a, bool prefer_link_local) { + _cleanup_free_ DnsAnswerItem **items = NULL; + DnsAnswerItem **p, *item; + size_t n; + + n = dns_answer_size(a); + if (n <= 1) + return; + + /* RFC 4795, Section 2.6 suggests we should order entries + * depending on whether the sender is a link-local address. */ + + p = items = new(DnsAnswerItem*, n); + if (!items) + return (void) log_oom(); + + /* Order preferred address records and other records to the beginning of the array */ + DNS_ANSWER_FOREACH_ITEM(item, a) + if (dns_resource_record_is_link_local_address(item->rr) == prefer_link_local) + *p++ = dns_answer_item_ref(item); + + /* Order address records that are not preferred to the end of the array */ + DNS_ANSWER_FOREACH_ITEM(item, a) + if (dns_resource_record_is_link_local_address(item->rr) != prefer_link_local) + *p++ = dns_answer_item_ref(item); + + + assert((size_t) (p - items) == n); + + ordered_set_clear(a->items); + for (size_t i = 0; i < n; i++) + assert_se(ordered_set_put(a->items, items[i]) >= 0); +} + +int dns_answer_reserve(DnsAnswer **a, size_t n_free) { + assert(a); + + if (n_free <= 0) + return 0; + + if (!*a) { + DnsAnswer *n; + + n = dns_answer_new(n_free); + if (!n) + return -ENOMEM; + + *a = n; + return 0; + } + + if ((*a)->n_ref > 1) + return -EBUSY; + + return dns_answer_reserve_internal(*a, n_free); +} + +int dns_answer_reserve_or_clone(DnsAnswer **a, size_t n_free) { + _cleanup_(dns_answer_unrefp) DnsAnswer *n = NULL; + size_t ns; + int r; + + assert(a); + + r = dns_answer_reserve(a, n_free); + if (r != -EBUSY) + return r; + + ns = dns_answer_size(*a); + assert(ns <= UINT16_MAX); /* Maximum number of RRs we can stick into a DNS packet section */ + + ns = saturate_add(ns, n_free, UINT16_MAX); + + n = dns_answer_new(ns); + if (!n) + return -ENOMEM; + + r = dns_answer_add_raw_all(n, *a); + if (r < 0) + return r; + + DNS_ANSWER_REPLACE(*a, TAKE_PTR(n)); + return 0; +} + +/* + * This function is not used in the code base, but is useful when debugging. Do not delete. + */ +void dns_answer_dump(DnsAnswer *answer, FILE *f) { + DnsAnswerItem *item; + + if (!f) + f = stdout; + + DNS_ANSWER_FOREACH_ITEM(item, answer) { + const char *t; + + fputc('\t', f); + + t = dns_resource_record_to_string(item->rr); + if (!t) { + log_oom(); + continue; + } + + fputs(t, f); + fputs("\t;", f); + fprintf(f, " ttl=%" PRIu32, item->rr->ttl); + + if (item->ifindex != 0) + fprintf(f, " ifindex=%i", item->ifindex); + if (item->rrsig) + fputs(" rrsig", f); + if (item->flags & DNS_ANSWER_AUTHENTICATED) + fputs(" authenticated", f); + if (item->flags & DNS_ANSWER_CACHEABLE) + fputs(" cacheable", f); + if (item->flags & DNS_ANSWER_SHARED_OWNER) + fputs(" shared-owner", f); + if (item->flags & DNS_ANSWER_CACHE_FLUSH) + fputs(" cache-flush", f); + if (item->flags & DNS_ANSWER_GOODBYE) + fputs(" goodbye", f); + if (item->flags & DNS_ANSWER_SECTION_ANSWER) + fputs(" section-answer", f); + if (item->flags & DNS_ANSWER_SECTION_AUTHORITY) + fputs(" section-authority", f); + if (item->flags & DNS_ANSWER_SECTION_ADDITIONAL) + fputs(" section-additional", f); + + fputc('\n', f); + } +} + +int dns_answer_has_dname_for_cname(DnsAnswer *a, DnsResourceRecord *cname) { + DnsResourceRecord *rr; + int r; + + assert(cname); + + /* Checks whether the answer contains a DNAME record that indicates that the specified CNAME record is + * synthesized from it */ + + if (cname->key->type != DNS_TYPE_CNAME) + return 0; + + DNS_ANSWER_FOREACH(rr, a) { + _cleanup_free_ char *n = NULL; + + if (rr->key->type != DNS_TYPE_DNAME) + continue; + if (rr->key->class != cname->key->class) + continue; + + r = dns_name_change_suffix(cname->cname.name, rr->dname.name, dns_resource_key_name(rr->key), &n); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dns_name_equal(n, dns_resource_key_name(cname->key)); + if (r < 0) + return r; + if (r > 0) + return 1; + } + + return 0; +} + +void dns_answer_randomize(DnsAnswer *a) { + _cleanup_free_ DnsAnswerItem **items = NULL; + DnsAnswerItem **p, *item; + size_t n; + + /* Permutes the answer list randomly (Knuth shuffle) */ + + n = dns_answer_size(a); + if (n <= 1) + return; + + p = items = new(DnsAnswerItem*, n); + if (!items) + return (void) log_oom(); + + DNS_ANSWER_FOREACH_ITEM(item, a) + *p++ = dns_answer_item_ref(item); + + assert((size_t) (p - items) == n); + + for (size_t i = 0; i < n; i++) { + size_t k; + + k = random_u64_range(n); + if (k == i) + continue; + + SWAP_TWO(items[i], items[k]); + } + + ordered_set_clear(a->items); + for (size_t i = 0; i < n; i++) + assert_se(ordered_set_put(a->items, items[i]) >= 0); +} + +uint32_t dns_answer_min_ttl(DnsAnswer *a) { + uint32_t ttl = UINT32_MAX; + DnsResourceRecord *rr; + + /* Return the smallest TTL of all RRs in this answer */ + + DNS_ANSWER_FOREACH(rr, a) { + /* Don't consider OPT (where the TTL field is used for other purposes than an actual TTL) */ + + if (dns_type_is_pseudo(rr->key->type) || + dns_class_is_pseudo(rr->key->class)) + continue; + + ttl = MIN(ttl, rr->ttl); + } + + return ttl; +} diff --git a/src/resolve/resolved-dns-answer.h b/src/resolve/resolved-dns-answer.h new file mode 100644 index 0000000..93afea3 --- /dev/null +++ b/src/resolve/resolved-dns-answer.h @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct DnsAnswer DnsAnswer; +typedef struct DnsAnswerItem DnsAnswerItem; + +#include "macro.h" +#include "ordered-set.h" +#include "resolved-dns-rr.h" + +/* A simple array of resource records. We keep track of the originating ifindex for each RR where that makes + * sense, so that we can qualify A and AAAA RRs referring to a local link with the right ifindex. + * + * Note that we usually encode the empty DnsAnswer object as a simple NULL. */ + +typedef enum DnsAnswerFlags { + DNS_ANSWER_AUTHENTICATED = 1 << 0, /* Item has been authenticated */ + DNS_ANSWER_CACHEABLE = 1 << 1, /* Item is subject to caching */ + DNS_ANSWER_SHARED_OWNER = 1 << 2, /* For mDNS: RRset may be owner by multiple peers */ + DNS_ANSWER_CACHE_FLUSH = 1 << 3, /* For mDNS: sets cache-flush bit in the rrclass of response records */ + DNS_ANSWER_GOODBYE = 1 << 4, /* For mDNS: item is subject to disappear */ + DNS_ANSWER_SECTION_ANSWER = 1 << 5, /* When parsing: RR originates from answer section */ + DNS_ANSWER_SECTION_AUTHORITY = 1 << 6, /* When parsing: RR originates from authority section */ + DNS_ANSWER_SECTION_ADDITIONAL = 1 << 7, /* When parsing: RR originates from additional section */ + + DNS_ANSWER_MASK_SECTIONS = DNS_ANSWER_SECTION_ANSWER| + DNS_ANSWER_SECTION_AUTHORITY| + DNS_ANSWER_SECTION_ADDITIONAL, +} DnsAnswerFlags; + +struct DnsAnswerItem { + unsigned n_ref; + DnsResourceRecord *rr; + DnsResourceRecord *rrsig; /* Optionally, also store RRSIG RR that successfully validates this item */ + int ifindex; + DnsAnswerFlags flags; +}; + +struct DnsAnswer { + unsigned n_ref; + OrderedSet *items; +}; + +DnsAnswer *dns_answer_new(size_t n); +DnsAnswer *dns_answer_ref(DnsAnswer *a); +DnsAnswer *dns_answer_unref(DnsAnswer *a); + +#define DNS_ANSWER_REPLACE(a, b) \ + do { \ + typeof(a)* _a = &(a); \ + typeof(b) _b = (b); \ + dns_answer_unref(*_a); \ + *_a = _b; \ + } while(0) + +int dns_answer_add(DnsAnswer *a, DnsResourceRecord *rr, int ifindex, DnsAnswerFlags flags, DnsResourceRecord *rrsig); +int dns_answer_add_extend(DnsAnswer **a, DnsResourceRecord *rr, int ifindex, DnsAnswerFlags flags, DnsResourceRecord *rrsig); +int dns_answer_add_soa(DnsAnswer *a, const char *name, uint32_t ttl, int ifindex); + +int dns_answer_match_key(DnsAnswer *a, const DnsResourceKey *key, DnsAnswerFlags *ret_flags); +bool dns_answer_contains_nsec_or_nsec3(DnsAnswer *a); +int dns_answer_contains_zone_nsec3(DnsAnswer *answer, const char *zone); +bool dns_answer_contains(DnsAnswer *answer, DnsResourceRecord *rr); + +int dns_answer_find_soa(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *ret_flags); +int dns_answer_find_cname_or_dname(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *ret_flags); + +int dns_answer_merge(DnsAnswer *a, DnsAnswer *b, DnsAnswer **ret); +int dns_answer_extend(DnsAnswer **a, DnsAnswer *b); + +void dns_answer_order_by_scope(DnsAnswer *a, bool prefer_link_local); + +int dns_answer_reserve(DnsAnswer **a, size_t n_free); +int dns_answer_reserve_or_clone(DnsAnswer **a, size_t n_free); + +int dns_answer_remove_by_key(DnsAnswer **a, const DnsResourceKey *key); +int dns_answer_remove_by_rr(DnsAnswer **a, DnsResourceRecord *rr); +int dns_answer_remove_by_answer_keys(DnsAnswer **a, DnsAnswer *b); + +int dns_answer_copy_by_key(DnsAnswer **a, DnsAnswer *source, const DnsResourceKey *key, DnsAnswerFlags or_flags, DnsResourceRecord *rrsig); +int dns_answer_move_by_key(DnsAnswer **to, DnsAnswer **from, const DnsResourceKey *key, DnsAnswerFlags or_flags, DnsResourceRecord *rrsig); + +int dns_answer_has_dname_for_cname(DnsAnswer *a, DnsResourceRecord *cname); + +static inline size_t dns_answer_size(DnsAnswer *a) { + return a ? ordered_set_size(a->items) : 0; +} + +static inline bool dns_answer_isempty(DnsAnswer *a) { + return dns_answer_size(a) <= 0; +} + +void dns_answer_dump(DnsAnswer *answer, FILE *f); + +void dns_answer_randomize(DnsAnswer *a); + +uint32_t dns_answer_min_ttl(DnsAnswer *a); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsAnswer*, dns_answer_unref); + +typedef struct DnsAnswerIterator { + Iterator iterator; + DnsAnswer *answer; + DnsAnswerItem *item; +} DnsAnswerIterator; + +#define _DNS_ANSWER_FOREACH(kk, a, i) \ + for (DnsAnswerIterator i = { .iterator = ITERATOR_FIRST, .answer = (a) }; \ + i.answer && \ + ordered_set_iterate(i.answer->items, &i.iterator, (void**) &(i.item)) && \ + (kk = i.item->rr, true); ) + +#define DNS_ANSWER_FOREACH(rr, a) _DNS_ANSWER_FOREACH(rr, a, UNIQ_T(i, UNIQ)) + +#define _DNS_ANSWER_FOREACH_IFINDEX(kk, ifi, a, i) \ + for (DnsAnswerIterator i = { .iterator = ITERATOR_FIRST, .answer = (a) }; \ + i.answer && \ + ordered_set_iterate(i.answer->items, &i.iterator, (void**) &(i.item)) && \ + (kk = i.item->rr, ifi = i.item->ifindex, true); ) + +#define DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, a) _DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, a, UNIQ_T(i, UNIQ)) + +#define _DNS_ANSWER_FOREACH_FLAGS(kk, fl, a, i) \ + for (DnsAnswerIterator i = { .iterator = ITERATOR_FIRST, .answer = (a) }; \ + i.answer && \ + ordered_set_iterate(i.answer->items, &i.iterator, (void**) &(i.item)) && \ + (kk = i.item->rr, fl = i.item->flags, true); ) + +#define DNS_ANSWER_FOREACH_FLAGS(rr, flags, a) _DNS_ANSWER_FOREACH_FLAGS(rr, flags, a, UNIQ_T(i, UNIQ)) + +#define _DNS_ANSWER_FOREACH_ITEM(it, a, i) \ + for (DnsAnswerIterator i = { .iterator = ITERATOR_FIRST, .answer = (a) }; \ + i.answer && \ + ordered_set_iterate(i.answer->items, &i.iterator, (void**) &(i.item)) && \ + (it = i.item, true); ) + +#define DNS_ANSWER_FOREACH_ITEM(item, a) _DNS_ANSWER_FOREACH_ITEM(item, a, UNIQ_T(i, UNIQ)) diff --git a/src/resolve/resolved-dns-cache.c b/src/resolve/resolved-dns-cache.c new file mode 100644 index 0000000..d9e1053 --- /dev/null +++ b/src/resolve/resolved-dns-cache.c @@ -0,0 +1,1371 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> + +#include "af-list.h" +#include "alloc-util.h" +#include "dns-domain.h" +#include "format-util.h" +#include "resolved-dns-answer.h" +#include "resolved-dns-cache.h" +#include "resolved-dns-packet.h" +#include "string-util.h" + +/* Never cache more than 4K entries. RFC 1536, Section 5 suggests to + * leave DNS caches unbounded, but that's crazy. */ +#define CACHE_MAX 4096 + +/* We never keep any item longer than 2h in our cache */ +#define CACHE_TTL_MAX_USEC (2 * USEC_PER_HOUR) + +/* How long to cache strange rcodes, i.e. rcodes != SUCCESS and != NXDOMAIN (specifically: that's only SERVFAIL for + * now) */ +#define CACHE_TTL_STRANGE_RCODE_USEC (10 * USEC_PER_SEC) + +#define CACHEABLE_QUERY_FLAGS (SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL) + +typedef enum DnsCacheItemType DnsCacheItemType; +typedef struct DnsCacheItem DnsCacheItem; + +enum DnsCacheItemType { + DNS_CACHE_POSITIVE, + DNS_CACHE_NODATA, + DNS_CACHE_NXDOMAIN, + DNS_CACHE_RCODE, /* "strange" RCODE (effective only SERVFAIL for now) */ +}; + +struct DnsCacheItem { + DnsCacheItemType type; + int rcode; + DnsResourceKey *key; /* The key for this item, i.e. the lookup key */ + DnsResourceRecord *rr; /* The RR for this item, i.e. the lookup value for positive queries */ + DnsAnswer *answer; /* The full validated answer, if this is an RRset acquired via a "primary" lookup */ + DnsPacket *full_packet; /* The full packet this information was acquired with */ + + usec_t until; + uint64_t query_flags; /* SD_RESOLVED_AUTHENTICATED and/or SD_RESOLVED_CONFIDENTIAL */ + DnssecResult dnssec_result; + + int ifindex; + int owner_family; + union in_addr_union owner_address; + + unsigned prioq_idx; + LIST_FIELDS(DnsCacheItem, by_key); + + bool shared_owner; +}; + +/* Returns true if this is a cache item created as result of an explicit lookup, or created as "side-effect" + * of another request. "Primary" entries will carry the full answer data (with NSEC, …) that can aso prove + * wildcard expansion, non-existence and such, while entries that were created as "side-effect" just contain + * immediate RR data for the specified RR key, but nothing else. */ +#define DNS_CACHE_ITEM_IS_PRIMARY(item) (!!(item)->answer) + +static const char *dns_cache_item_type_to_string(DnsCacheItem *item) { + assert(item); + + switch (item->type) { + + case DNS_CACHE_POSITIVE: + return "POSITIVE"; + + case DNS_CACHE_NODATA: + return "NODATA"; + + case DNS_CACHE_NXDOMAIN: + return "NXDOMAIN"; + + case DNS_CACHE_RCODE: + return dns_rcode_to_string(item->rcode); + } + + return NULL; +} + +static DnsCacheItem* dns_cache_item_free(DnsCacheItem *i) { + if (!i) + return NULL; + + dns_resource_record_unref(i->rr); + dns_resource_key_unref(i->key); + dns_answer_unref(i->answer); + dns_packet_unref(i->full_packet); + return mfree(i); +} +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsCacheItem*, dns_cache_item_free); + +static void dns_cache_item_unlink_and_free(DnsCache *c, DnsCacheItem *i) { + DnsCacheItem *first; + + assert(c); + + if (!i) + return; + + first = hashmap_get(c->by_key, i->key); + LIST_REMOVE(by_key, first, i); + + if (first) + assert_se(hashmap_replace(c->by_key, first->key, first) >= 0); + else + hashmap_remove(c->by_key, i->key); + + prioq_remove(c->by_expiry, i, &i->prioq_idx); + + dns_cache_item_free(i); +} + +static bool dns_cache_remove_by_rr(DnsCache *c, DnsResourceRecord *rr) { + DnsCacheItem *first; + int r; + + first = hashmap_get(c->by_key, rr->key); + LIST_FOREACH(by_key, i, first) { + r = dns_resource_record_equal(i->rr, rr); + if (r < 0) + return r; + if (r > 0) { + dns_cache_item_unlink_and_free(c, i); + return true; + } + } + + return false; +} + +static bool dns_cache_remove_by_key(DnsCache *c, DnsResourceKey *key) { + DnsCacheItem *first; + + assert(c); + assert(key); + + first = hashmap_remove(c->by_key, key); + if (!first) + return false; + + LIST_FOREACH(by_key, i, first) { + prioq_remove(c->by_expiry, i, &i->prioq_idx); + dns_cache_item_free(i); + } + + return true; +} + +void dns_cache_flush(DnsCache *c) { + DnsResourceKey *key; + + assert(c); + + while ((key = hashmap_first_key(c->by_key))) + dns_cache_remove_by_key(c, key); + + assert(hashmap_size(c->by_key) == 0); + assert(prioq_size(c->by_expiry) == 0); + + c->by_key = hashmap_free(c->by_key); + c->by_expiry = prioq_free(c->by_expiry); +} + +static void dns_cache_make_space(DnsCache *c, unsigned add) { + assert(c); + + if (add <= 0) + return; + + /* Makes space for n new entries. Note that we actually allow + * the cache to grow beyond CACHE_MAX, but only when we shall + * add more RRs to the cache than CACHE_MAX at once. In that + * case the cache will be emptied completely otherwise. */ + + for (;;) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + DnsCacheItem *i; + + if (prioq_size(c->by_expiry) <= 0) + break; + + if (prioq_size(c->by_expiry) + add < CACHE_MAX) + break; + + i = prioq_peek(c->by_expiry); + assert(i); + + /* Take an extra reference to the key so that it + * doesn't go away in the middle of the remove call */ + key = dns_resource_key_ref(i->key); + dns_cache_remove_by_key(c, key); + } +} + +void dns_cache_prune(DnsCache *c) { + usec_t t = 0; + + assert(c); + + /* Remove all entries that are past their TTL */ + + for (;;) { + DnsCacheItem *i; + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + i = prioq_peek(c->by_expiry); + if (!i) + break; + + if (t <= 0) + t = now(CLOCK_BOOTTIME); + + if (i->until > t) + break; + + /* Depending whether this is an mDNS shared entry + * either remove only this one RR or the whole RRset */ + log_debug("Removing %scache entry for %s (expired "USEC_FMT"s ago)", + i->shared_owner ? "shared " : "", + dns_resource_key_to_string(i->key, key_str, sizeof key_str), + (t - i->until) / USEC_PER_SEC); + + if (i->shared_owner) + dns_cache_item_unlink_and_free(c, i); + else { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + + /* Take an extra reference to the key so that it + * doesn't go away in the middle of the remove call */ + key = dns_resource_key_ref(i->key); + dns_cache_remove_by_key(c, key); + } + } +} + +static int dns_cache_item_prioq_compare_func(const void *a, const void *b) { + const DnsCacheItem *x = a, *y = b; + + return CMP(x->until, y->until); +} + +static int dns_cache_init(DnsCache *c) { + int r; + + assert(c); + + r = prioq_ensure_allocated(&c->by_expiry, dns_cache_item_prioq_compare_func); + if (r < 0) + return r; + + r = hashmap_ensure_allocated(&c->by_key, &dns_resource_key_hash_ops); + if (r < 0) + return r; + + return r; +} + +static int dns_cache_link_item(DnsCache *c, DnsCacheItem *i) { + DnsCacheItem *first; + int r; + + assert(c); + assert(i); + + r = prioq_put(c->by_expiry, i, &i->prioq_idx); + if (r < 0) + return r; + + first = hashmap_get(c->by_key, i->key); + if (first) { + _unused_ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *k = NULL; + + /* Keep a reference to the original key, while we manipulate the list. */ + k = dns_resource_key_ref(first->key); + + /* Now, try to reduce the number of keys we keep */ + dns_resource_key_reduce(&first->key, &i->key); + + if (first->rr) + dns_resource_key_reduce(&first->rr->key, &i->key); + if (i->rr) + dns_resource_key_reduce(&i->rr->key, &i->key); + + LIST_PREPEND(by_key, first, i); + assert_se(hashmap_replace(c->by_key, first->key, first) >= 0); + } else { + r = hashmap_put(c->by_key, i->key, i); + if (r < 0) { + prioq_remove(c->by_expiry, i, &i->prioq_idx); + return r; + } + } + + return 0; +} + +static DnsCacheItem* dns_cache_get(DnsCache *c, DnsResourceRecord *rr) { + assert(c); + assert(rr); + + LIST_FOREACH(by_key, i, (DnsCacheItem*) hashmap_get(c->by_key, rr->key)) + if (i->rr && dns_resource_record_equal(i->rr, rr) > 0) + return i; + + return NULL; +} + +static usec_t calculate_until( + DnsResourceRecord *rr, + uint32_t min_ttl, + uint32_t nsec_ttl, + usec_t timestamp, + bool use_soa_minimum) { + + uint32_t ttl; + usec_t u; + + assert(rr); + + ttl = MIN(min_ttl, nsec_ttl); + if (rr->key->type == DNS_TYPE_SOA && use_soa_minimum) { + /* If this is a SOA RR, and it is requested, clamp to the SOA's minimum field. This is used + * when we do negative caching, to determine the TTL for the negative caching entry. See RFC + * 2308, Section 5. */ + + if (ttl > rr->soa.minimum) + ttl = rr->soa.minimum; + } + + u = ttl * USEC_PER_SEC; + if (u > CACHE_TTL_MAX_USEC) + u = CACHE_TTL_MAX_USEC; + + if (rr->expiry != USEC_INFINITY) { + usec_t left; + + /* Make use of the DNSSEC RRSIG expiry info, if we have it */ + + left = LESS_BY(rr->expiry, now(CLOCK_REALTIME)); + if (u > left) + u = left; + } + + return timestamp + u; +} + +static void dns_cache_item_update_positive( + DnsCache *c, + DnsCacheItem *i, + DnsResourceRecord *rr, + DnsAnswer *answer, + DnsPacket *full_packet, + uint32_t min_ttl, + uint64_t query_flags, + bool shared_owner, + DnssecResult dnssec_result, + usec_t timestamp, + int ifindex, + int owner_family, + const union in_addr_union *owner_address) { + + assert(c); + assert(i); + assert(rr); + assert(owner_address); + + i->type = DNS_CACHE_POSITIVE; + + if (!i->by_key_prev) + /* We are the first item in the list, we need to + * update the key used in the hashmap */ + + assert_se(hashmap_replace(c->by_key, rr->key, i) >= 0); + + DNS_RR_REPLACE(i->rr, dns_resource_record_ref(rr)); + + DNS_RESOURCE_KEY_REPLACE(i->key, dns_resource_key_ref(rr->key)); + + DNS_ANSWER_REPLACE(i->answer, dns_answer_ref(answer)); + + DNS_PACKET_REPLACE(i->full_packet, dns_packet_ref(full_packet)); + + i->until = calculate_until(rr, min_ttl, UINT32_MAX, timestamp, false); + i->query_flags = query_flags & CACHEABLE_QUERY_FLAGS; + i->shared_owner = shared_owner; + i->dnssec_result = dnssec_result; + + i->ifindex = ifindex; + + i->owner_family = owner_family; + i->owner_address = *owner_address; + + prioq_reshuffle(c->by_expiry, i, &i->prioq_idx); +} + +static int dns_cache_put_positive( + DnsCache *c, + DnsProtocol protocol, + DnsResourceRecord *rr, + DnsAnswer *answer, + DnsPacket *full_packet, + uint64_t query_flags, + bool shared_owner, + DnssecResult dnssec_result, + usec_t timestamp, + int ifindex, + int owner_family, + const union in_addr_union *owner_address) { + + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + DnsCacheItem *existing; + uint32_t min_ttl; + int r; + + assert(c); + assert(rr); + assert(owner_address); + + /* Never cache pseudo RRs */ + if (dns_class_is_pseudo(rr->key->class)) + return 0; + if (dns_type_is_pseudo(rr->key->type)) + return 0; + + /* Determine the minimal TTL of all RRs in the answer plus the one by the main RR we are supposed to + * cache. Since we cache whole answers to questions we should never return answers where only some + * RRs are still valid, hence find the lowest here */ + min_ttl = MIN(dns_answer_min_ttl(answer), rr->ttl); + + /* New TTL is 0? Delete this specific entry... */ + if (min_ttl <= 0) { + r = dns_cache_remove_by_rr(c, rr); + log_debug("%s: %s", + r > 0 ? "Removed zero TTL entry from cache" : "Not caching zero TTL cache entry", + dns_resource_key_to_string(rr->key, key_str, sizeof key_str)); + return 0; + } + + /* Entry exists already? Update TTL, timestamp and owner */ + existing = dns_cache_get(c, rr); + if (existing) { + dns_cache_item_update_positive( + c, + existing, + rr, + answer, + full_packet, + min_ttl, + query_flags, + shared_owner, + dnssec_result, + timestamp, + ifindex, + owner_family, + owner_address); + return 0; + } + + /* Do not cache mDNS goodbye packet. */ + if (protocol == DNS_PROTOCOL_MDNS && rr->ttl <= 1) + return 0; + + /* Otherwise, add the new RR */ + r = dns_cache_init(c); + if (r < 0) + return r; + + dns_cache_make_space(c, 1); + + _cleanup_(dns_cache_item_freep) DnsCacheItem *i = new(DnsCacheItem, 1); + if (!i) + return -ENOMEM; + + *i = (DnsCacheItem) { + .type = DNS_CACHE_POSITIVE, + .key = dns_resource_key_ref(rr->key), + .rr = dns_resource_record_ref(rr), + .answer = dns_answer_ref(answer), + .full_packet = dns_packet_ref(full_packet), + .until = calculate_until(rr, min_ttl, UINT32_MAX, timestamp, false), + .query_flags = query_flags & CACHEABLE_QUERY_FLAGS, + .shared_owner = shared_owner, + .dnssec_result = dnssec_result, + .ifindex = ifindex, + .owner_family = owner_family, + .owner_address = *owner_address, + .prioq_idx = PRIOQ_IDX_NULL, + }; + + r = dns_cache_link_item(c, i); + if (r < 0) + return r; + + log_debug("Added positive %s %s%s cache entry for %s "USEC_FMT"s on %s/%s/%s", + FLAGS_SET(i->query_flags, SD_RESOLVED_AUTHENTICATED) ? "authenticated" : "unauthenticated", + FLAGS_SET(i->query_flags, SD_RESOLVED_CONFIDENTIAL) ? "confidential" : "non-confidential", + i->shared_owner ? " shared" : "", + dns_resource_key_to_string(i->key, key_str, sizeof key_str), + (i->until - timestamp) / USEC_PER_SEC, + i->ifindex == 0 ? "*" : FORMAT_IFNAME(i->ifindex), + af_to_name_short(i->owner_family), + IN_ADDR_TO_STRING(i->owner_family, &i->owner_address)); + + TAKE_PTR(i); + return 0; +} + +static int dns_cache_put_negative( + DnsCache *c, + DnsResourceKey *key, + int rcode, + DnsAnswer *answer, + DnsPacket *full_packet, + uint64_t query_flags, + DnssecResult dnssec_result, + uint32_t nsec_ttl, + usec_t timestamp, + DnsResourceRecord *soa, + int owner_family, + const union in_addr_union *owner_address) { + + _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL; + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + int r; + + assert(c); + assert(key); + assert(owner_address); + + /* Never cache pseudo RR keys. DNS_TYPE_ANY is particularly + * important to filter out as we use this as a pseudo-type for + * NXDOMAIN entries */ + if (dns_class_is_pseudo(key->class)) + return 0; + if (dns_type_is_pseudo(key->type)) + return 0; + + if (IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN)) { + if (!soa) + return 0; + + /* For negative replies, check if we have a TTL of a SOA */ + if (nsec_ttl <= 0 || soa->soa.minimum <= 0 || soa->ttl <= 0) { + log_debug("Not caching negative entry with zero SOA/NSEC/NSEC3 TTL: %s", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + return 0; + } + } else if (rcode != DNS_RCODE_SERVFAIL) + return 0; + + r = dns_cache_init(c); + if (r < 0) + return r; + + dns_cache_make_space(c, 1); + + i = new(DnsCacheItem, 1); + if (!i) + return -ENOMEM; + + *i = (DnsCacheItem) { + .type = + rcode == DNS_RCODE_SUCCESS ? DNS_CACHE_NODATA : + rcode == DNS_RCODE_NXDOMAIN ? DNS_CACHE_NXDOMAIN : DNS_CACHE_RCODE, + .query_flags = query_flags & CACHEABLE_QUERY_FLAGS, + .dnssec_result = dnssec_result, + .owner_family = owner_family, + .owner_address = *owner_address, + .prioq_idx = PRIOQ_IDX_NULL, + .rcode = rcode, + .answer = dns_answer_ref(answer), + .full_packet = dns_packet_ref(full_packet), + }; + + /* Determine how long to cache this entry. In case we have some RRs in the answer use the lowest TTL + * of any of them. Typically that's the SOA's TTL, which is OK, but could possibly be lower because + * of some other RR. Let's better take the lowest option here than a needlessly high one */ + i->until = + i->type == DNS_CACHE_RCODE ? timestamp + CACHE_TTL_STRANGE_RCODE_USEC : + calculate_until(soa, dns_answer_min_ttl(answer), nsec_ttl, timestamp, true); + + if (i->type == DNS_CACHE_NXDOMAIN) { + /* NXDOMAIN entries should apply equally to all types, so we use ANY as + * a pseudo type for this purpose here. */ + i->key = dns_resource_key_new(key->class, DNS_TYPE_ANY, dns_resource_key_name(key)); + if (!i->key) + return -ENOMEM; + + /* Make sure to remove any previous entry for this + * specific ANY key. (For non-ANY keys the cache data + * is already cleared by the caller.) Note that we + * don't bother removing positive or NODATA cache + * items in this case, because it would either be slow + * or require explicit indexing by name */ + dns_cache_remove_by_key(c, key); + } else + i->key = dns_resource_key_ref(key); + + r = dns_cache_link_item(c, i); + if (r < 0) + return r; + + log_debug("Added %s cache entry for %s "USEC_FMT"s", + dns_cache_item_type_to_string(i), + dns_resource_key_to_string(i->key, key_str, sizeof key_str), + (i->until - timestamp) / USEC_PER_SEC); + + i = NULL; + return 0; +} + +static void dns_cache_remove_previous( + DnsCache *c, + DnsResourceKey *key, + DnsAnswer *answer) { + + DnsResourceRecord *rr; + DnsAnswerFlags flags; + + assert(c); + + /* First, if we were passed a key (i.e. on LLMNR/DNS, but + * not on mDNS), delete all matching old RRs, so that we only + * keep complete by_key in place. */ + if (key) + dns_cache_remove_by_key(c, key); + + /* Second, flush all entries matching the answer, unless this + * is an RR that is explicitly marked to be "shared" between + * peers (i.e. mDNS RRs without the flush-cache bit set). */ + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + if ((flags & DNS_ANSWER_CACHEABLE) == 0) + continue; + + if (flags & DNS_ANSWER_SHARED_OWNER) + continue; + + dns_cache_remove_by_key(c, rr->key); + } +} + +static bool rr_eligible(DnsResourceRecord *rr) { + assert(rr); + + /* When we see an NSEC/NSEC3 RR, we'll only cache it if it is from the lower zone, not the upper zone, since + * that's where the interesting bits are (with exception of DS RRs). Of course, this way we cannot derive DS + * existence from any cached NSEC/NSEC3, but that should be fine. */ + + switch (rr->key->type) { + + case DNS_TYPE_NSEC: + return !bitmap_isset(rr->nsec.types, DNS_TYPE_NS) || + bitmap_isset(rr->nsec.types, DNS_TYPE_SOA); + + case DNS_TYPE_NSEC3: + return !bitmap_isset(rr->nsec3.types, DNS_TYPE_NS) || + bitmap_isset(rr->nsec3.types, DNS_TYPE_SOA); + + default: + return true; + } +} + +int dns_cache_put( + DnsCache *c, + DnsCacheMode cache_mode, + DnsProtocol protocol, + DnsResourceKey *key, + int rcode, + DnsAnswer *answer, + DnsPacket *full_packet, + uint64_t query_flags, + DnssecResult dnssec_result, + uint32_t nsec_ttl, + int owner_family, + const union in_addr_union *owner_address) { + + DnsResourceRecord *soa = NULL; + bool weird_rcode = false; + DnsAnswerItem *item; + DnsAnswerFlags flags; + unsigned cache_keys; + usec_t timestamp; + int r; + + assert(c); + assert(owner_address); + + dns_cache_remove_previous(c, key, answer); + + /* We only care for positive replies and NXDOMAINs, on all other replies we will simply flush the respective + * entries, and that's it. (Well, with one further exception: since some DNS zones (akamai!) return SERVFAIL + * consistently for some lookups, and forwarders tend to propagate that we'll cache that too, but only for a + * short time.) */ + + if (IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN)) { + if (dns_answer_isempty(answer)) { + if (key) { + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + log_debug("Not caching negative entry without a SOA record: %s", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + } + + return 0; + } + + } else { + /* Only cache SERVFAIL as "weird" rcode for now. We can add more later, should that turn out to be + * beneficial. */ + if (rcode != DNS_RCODE_SERVFAIL) + return 0; + + weird_rcode = true; + } + + cache_keys = dns_answer_size(answer); + if (key) + cache_keys++; + + /* Make some space for our new entries */ + dns_cache_make_space(c, cache_keys); + + timestamp = now(CLOCK_BOOTTIME); + + /* Second, add in positive entries for all contained RRs */ + DNS_ANSWER_FOREACH_ITEM(item, answer) { + int primary = false; + + if (!FLAGS_SET(item->flags, DNS_ANSWER_CACHEABLE) || + !rr_eligible(item->rr)) + continue; + + if (key) { + /* We store the auxiliary RRs and packet data in the cache only if they were in + * direct response to the original query. If we cache an RR we also received, and + * that is just auxiliary information we can't use the data, hence don't. */ + + primary = dns_resource_key_match_rr(key, item->rr, NULL); + if (primary < 0) + return primary; + if (primary == 0) { + primary = dns_resource_key_match_cname_or_dname(key, item->rr->key, NULL); + if (primary < 0) + return primary; + } + } + + if (!primary) { + DnsCacheItem *first; + + /* Do not replace existing cache items for primary lookups with non-primary + * data. After all the primary lookup data is a lot more useful. */ + first = hashmap_get(c->by_key, item->rr->key); + if (first && DNS_CACHE_ITEM_IS_PRIMARY(first)) + return 0; + } + + r = dns_cache_put_positive( + c, + protocol, + item->rr, + primary ? answer : NULL, + primary ? full_packet : NULL, + ((item->flags & DNS_ANSWER_AUTHENTICATED) ? SD_RESOLVED_AUTHENTICATED : 0) | + (query_flags & SD_RESOLVED_CONFIDENTIAL), + item->flags & DNS_ANSWER_SHARED_OWNER, + dnssec_result, + timestamp, + item->ifindex, + owner_family, + owner_address); + if (r < 0) + goto fail; + } + + if (!key) /* mDNS doesn't know negative caching, really */ + return 0; + + /* Third, add in negative entries if the key has no RR */ + r = dns_answer_match_key(answer, key, NULL); + if (r < 0) + goto fail; + if (r > 0) + return 0; + + /* But not if it has a matching CNAME/DNAME (the negative caching will be done on the canonical name, + * not on the alias) */ + r = dns_answer_find_cname_or_dname(answer, key, NULL, NULL); + if (r < 0) + goto fail; + if (r > 0) + return 0; + + /* See https://tools.ietf.org/html/rfc2308, which say that a matching SOA record in the packet is used to + * enable negative caching. We apply one exception though: if we are about to cache a weird rcode we do so + * regardless of a SOA. */ + r = dns_answer_find_soa(answer, key, &soa, &flags); + if (r < 0) + goto fail; + if (r == 0 && !weird_rcode) + return 0; + if (r > 0) { + /* Refuse using the SOA data if it is unsigned, but the key is signed */ + if (FLAGS_SET(query_flags, SD_RESOLVED_AUTHENTICATED) && + (flags & DNS_ANSWER_AUTHENTICATED) == 0) + return 0; + } + + if (cache_mode == DNS_CACHE_MODE_NO_NEGATIVE) { + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + log_debug("Not caching negative entry for: %s, cache mode set to no-negative", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + return 0; + } + + r = dns_cache_put_negative( + c, + key, + rcode, + answer, + full_packet, + query_flags, + dnssec_result, + nsec_ttl, + timestamp, + soa, + owner_family, owner_address); + if (r < 0) + goto fail; + + return 0; + +fail: + /* Adding all RRs failed. Let's clean up what we already + * added, just in case */ + + if (key) + dns_cache_remove_by_key(c, key); + + DNS_ANSWER_FOREACH_ITEM(item, answer) { + if ((item->flags & DNS_ANSWER_CACHEABLE) == 0) + continue; + + dns_cache_remove_by_key(c, item->rr->key); + } + + return r; +} + +static DnsCacheItem *dns_cache_get_by_key_follow_cname_dname_nsec(DnsCache *c, DnsResourceKey *k) { + DnsCacheItem *i; + const char *n; + int r; + + assert(c); + assert(k); + + /* If we hit some OOM error, or suchlike, we don't care too + * much, after all this is just a cache */ + + i = hashmap_get(c->by_key, k); + if (i) + return i; + + n = dns_resource_key_name(k); + + /* Check if we have an NXDOMAIN cache item for the name, notice that we use + * the pseudo-type ANY for NXDOMAIN cache items. */ + i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_ANY, n)); + if (i && i->type == DNS_CACHE_NXDOMAIN) + return i; + + if (dns_type_may_redirect(k->type)) { + /* Check if we have a CNAME record instead */ + i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_CNAME, n)); + if (i && i->type != DNS_CACHE_NODATA) + return i; + + /* OK, let's look for cached DNAME records. */ + for (;;) { + if (isempty(n)) + return NULL; + + i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_DNAME, n)); + if (i && i->type != DNS_CACHE_NODATA) + return i; + + /* Jump one label ahead */ + r = dns_name_parent(&n); + if (r <= 0) + return NULL; + } + } + + if (k->type != DNS_TYPE_NSEC) { + /* Check if we have an NSEC record instead for the name. */ + i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_NSEC, n)); + if (i) + return i; + } + + return NULL; +} + +static int answer_add_clamp_ttl( + DnsAnswer **answer, + DnsResourceRecord *rr, + int ifindex, + DnsAnswerFlags answer_flags, + DnsResourceRecord *rrsig, + uint64_t query_flags, + usec_t until, + usec_t current) { + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *patched = NULL, *patched_rrsig = NULL; + int r; + + assert(answer); + assert(rr); + + if (FLAGS_SET(query_flags, SD_RESOLVED_CLAMP_TTL)) { + uint32_t left_ttl; + + assert(current > 0); + + /* Let's determine how much time is left for this cache entry. Note that we round down, but + * clamp this to be 1s at minimum, since we usually want records to remain cached better too + * short a time than too long a time, but otoh don't want to return 0 ever, since that has + * special semantics in various contexts — in particular in mDNS */ + + left_ttl = MAX(1U, LESS_BY(until, current) / USEC_PER_SEC); + + patched = dns_resource_record_ref(rr); + + r = dns_resource_record_clamp_ttl(&patched, left_ttl); + if (r < 0) + return r; + + rr = patched; + + if (rrsig) { + patched_rrsig = dns_resource_record_ref(rrsig); + r = dns_resource_record_clamp_ttl(&patched_rrsig, left_ttl); + if (r < 0) + return r; + + rrsig = patched_rrsig; + } + } + + r = dns_answer_add_extend(answer, rr, ifindex, answer_flags, rrsig); + if (r < 0) + return r; + + return 0; +} + +int dns_cache_lookup( + DnsCache *c, + DnsResourceKey *key, + uint64_t query_flags, + int *ret_rcode, + DnsAnswer **ret_answer, + DnsPacket **ret_full_packet, + uint64_t *ret_query_flags, + DnssecResult *ret_dnssec_result) { + + _cleanup_(dns_packet_unrefp) DnsPacket *full_packet = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + unsigned n = 0; + int r; + bool nxdomain = false; + DnsCacheItem *first, *nsec = NULL; + bool have_authenticated = false, have_non_authenticated = false, have_confidential = false, have_non_confidential = false; + usec_t current = 0; + int found_rcode = -1; + DnssecResult dnssec_result = -1; + int have_dnssec_result = -1; + + assert(c); + assert(key); + + if (key->type == DNS_TYPE_ANY || key->class == DNS_CLASS_ANY) { + /* If we have ANY lookups we don't use the cache, so that the caller refreshes via the + * network. */ + + log_debug("Ignoring cache for ANY lookup: %s", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + goto miss; + } + + first = dns_cache_get_by_key_follow_cname_dname_nsec(c, key); + if (!first) { + /* If one question cannot be answered we need to refresh */ + + log_debug("Cache miss for %s", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + goto miss; + } + + if (FLAGS_SET(query_flags, SD_RESOLVED_CLAMP_TTL)) { + /* 'current' is always passed to answer_add_clamp_ttl(), but is only used conditionally. + * We'll do the same assert there to make sure that it was initialized properly. */ + current = now(CLOCK_BOOTTIME); + assert(current > 0); + } + + LIST_FOREACH(by_key, j, first) { + /* If the caller doesn't allow us to answer questions from cache data learned from + * "side-effect", skip this entry. */ + if (FLAGS_SET(query_flags, SD_RESOLVED_REQUIRE_PRIMARY) && + !DNS_CACHE_ITEM_IS_PRIMARY(j)) { + log_debug("Primary answer was requested for cache lookup for %s, which we don't have.", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + + goto miss; + } + + if (j->type == DNS_CACHE_NXDOMAIN) + nxdomain = true; + else if (j->type == DNS_CACHE_RCODE) + found_rcode = j->rcode; + else if (j->rr) { + if (j->rr->key->type == DNS_TYPE_NSEC) + nsec = j; + + n++; + } + + if (FLAGS_SET(j->query_flags, SD_RESOLVED_AUTHENTICATED)) + have_authenticated = true; + else + have_non_authenticated = true; + + if (FLAGS_SET(j->query_flags, SD_RESOLVED_CONFIDENTIAL)) + have_confidential = true; + else + have_non_confidential = true; + + if (j->dnssec_result < 0) { + have_dnssec_result = false; /* an entry without dnssec result? then invalidate things for good */ + dnssec_result = _DNSSEC_RESULT_INVALID; + } else if (have_dnssec_result < 0) { + have_dnssec_result = true; /* So far no result seen, let's pick this one up */ + dnssec_result = j->dnssec_result; + } else if (have_dnssec_result > 0 && j->dnssec_result != dnssec_result) { + have_dnssec_result = false; /* conflicting result seen? then invalidate for good */ + dnssec_result = _DNSSEC_RESULT_INVALID; + } + + /* Append the answer RRs to our answer. Ideally we have the answer object, which we + * preferably use. But if the cached entry was generated as "side-effect" of a reply, + * i.e. from validated auxiliary records rather than from the main reply, then we use the + * individual RRs only instead. */ + if (j->answer) { + + /* Minor optimization, if the full answer object of this and the previous RR is the + * same, don't bother adding it again. Typically we store a full RRset here, hence + * that should be the case. */ + if (!j->by_key_prev || j->answer != j->by_key_prev->answer) { + DnsAnswerItem *item; + + DNS_ANSWER_FOREACH_ITEM(item, j->answer) { + r = answer_add_clamp_ttl( + &answer, + item->rr, + item->ifindex, + item->flags, + item->rrsig, + query_flags, + j->until, + current); + if (r < 0) + return r; + } + } + + } else if (j->rr) { + r = answer_add_clamp_ttl( + &answer, + j->rr, + j->ifindex, + FLAGS_SET(j->query_flags, SD_RESOLVED_AUTHENTICATED) ? DNS_ANSWER_AUTHENTICATED : 0, + NULL, + query_flags, + j->until, + current); + if (r < 0) + return r; + } + + /* We'll return any packet we have for this. Typically all cache entries for the same key + * should come from the same packet anyway, hence it doesn't really matter which packet we + * return here, they should all resolve to the same anyway. */ + if (!full_packet && j->full_packet) + full_packet = dns_packet_ref(j->full_packet); + } + + if (found_rcode >= 0) { + log_debug("RCODE %s cache hit for %s", + FORMAT_DNS_RCODE(found_rcode), + dns_resource_key_to_string(key, key_str, sizeof(key_str))); + + if (ret_rcode) + *ret_rcode = found_rcode; + if (ret_answer) + *ret_answer = TAKE_PTR(answer); + if (ret_full_packet) + *ret_full_packet = TAKE_PTR(full_packet); + if (ret_query_flags) + *ret_query_flags = 0; + if (ret_dnssec_result) + *ret_dnssec_result = dnssec_result; + + c->n_hit++; + return 1; + } + + if (nsec && !IN_SET(key->type, DNS_TYPE_NSEC, DNS_TYPE_DS)) { + /* Note that we won't derive information for DS RRs from an NSEC, because we only cache NSEC + * RRs from the lower-zone of a zone cut, but the DS RRs are on the upper zone. */ + + log_debug("NSEC NODATA cache hit for %s", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + + /* We only found an NSEC record that matches our name. If it says the type doesn't exist + * report NODATA. Otherwise report a cache miss. */ + + if (ret_rcode) + *ret_rcode = DNS_RCODE_SUCCESS; + if (ret_answer) + *ret_answer = TAKE_PTR(answer); + if (ret_full_packet) + *ret_full_packet = TAKE_PTR(full_packet); + if (ret_query_flags) + *ret_query_flags = nsec->query_flags; + if (ret_dnssec_result) + *ret_dnssec_result = nsec->dnssec_result; + + if (!bitmap_isset(nsec->rr->nsec.types, key->type) && + !bitmap_isset(nsec->rr->nsec.types, DNS_TYPE_CNAME) && + !bitmap_isset(nsec->rr->nsec.types, DNS_TYPE_DNAME)) { + c->n_hit++; + return 1; + } + + c->n_miss++; + return 0; + } + + log_debug("%s cache hit for %s", + n > 0 ? "Positive" : + nxdomain ? "NXDOMAIN" : "NODATA", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + + if (n <= 0) { + c->n_hit++; + + if (ret_rcode) + *ret_rcode = nxdomain ? DNS_RCODE_NXDOMAIN : DNS_RCODE_SUCCESS; + if (ret_answer) + *ret_answer = TAKE_PTR(answer); + if (ret_full_packet) + *ret_full_packet = TAKE_PTR(full_packet); + if (ret_query_flags) + *ret_query_flags = + ((have_authenticated && !have_non_authenticated) ? SD_RESOLVED_AUTHENTICATED : 0) | + ((have_confidential && !have_non_confidential) ? SD_RESOLVED_CONFIDENTIAL : 0); + if (ret_dnssec_result) + *ret_dnssec_result = dnssec_result; + + return 1; + } + + c->n_hit++; + + if (ret_rcode) + *ret_rcode = DNS_RCODE_SUCCESS; + if (ret_answer) + *ret_answer = TAKE_PTR(answer); + if (ret_full_packet) + *ret_full_packet = TAKE_PTR(full_packet); + if (ret_query_flags) + *ret_query_flags = + ((have_authenticated && !have_non_authenticated) ? SD_RESOLVED_AUTHENTICATED : 0) | + ((have_confidential && !have_non_confidential) ? SD_RESOLVED_CONFIDENTIAL : 0); + if (ret_dnssec_result) + *ret_dnssec_result = dnssec_result; + + return n; + +miss: + if (ret_rcode) + *ret_rcode = DNS_RCODE_SUCCESS; + if (ret_answer) + *ret_answer = NULL; + if (ret_full_packet) + *ret_full_packet = NULL; + if (ret_query_flags) + *ret_query_flags = 0; + if (ret_dnssec_result) + *ret_dnssec_result = _DNSSEC_RESULT_INVALID; + + c->n_miss++; + return 0; +} + +int dns_cache_check_conflicts(DnsCache *cache, DnsResourceRecord *rr, int owner_family, const union in_addr_union *owner_address) { + DnsCacheItem *first; + bool same_owner = true; + + assert(cache); + assert(rr); + + dns_cache_prune(cache); + + /* See if there's a cache entry for the same key. If there + * isn't there's no conflict */ + first = hashmap_get(cache->by_key, rr->key); + if (!first) + return 0; + + /* See if the RR key is owned by the same owner, if so, there + * isn't a conflict either */ + LIST_FOREACH(by_key, i, first) { + if (i->owner_family != owner_family || + !in_addr_equal(owner_family, &i->owner_address, owner_address)) { + same_owner = false; + break; + } + } + if (same_owner) + return 0; + + /* See if there's the exact same RR in the cache. If yes, then + * there's no conflict. */ + if (dns_cache_get(cache, rr)) + return 0; + + /* There's a conflict */ + return 1; +} + +int dns_cache_export_shared_to_packet(DnsCache *cache, DnsPacket *p, usec_t ts, unsigned max_rr) { + unsigned ancount = 0; + DnsCacheItem *i; + int r; + + assert(cache); + assert(p); + assert(p->protocol == DNS_PROTOCOL_MDNS); + + HASHMAP_FOREACH(i, cache->by_key) + LIST_FOREACH(by_key, j, i) { + if (!j->rr) + continue; + + if (!j->shared_owner) + continue; + + /* Ignore cached goodby packet. See on_mdns_packet() and RFC 6762 section 10.1. */ + if (j->rr->ttl <= 1) + continue; + + /* RFC6762 7.1: Don't append records with less than half the TTL remaining + * as known answers. */ + if (usec_sub_unsigned(j->until, ts) < j->rr->ttl * USEC_PER_SEC / 2) + continue; + + if (max_rr > 0 && ancount >= max_rr) { + DNS_PACKET_HEADER(p)->ancount = htobe16(ancount); + ancount = 0; + + r = dns_packet_new_query(&p->more, p->protocol, 0, true); + if (r < 0) + return r; + + p = p->more; + + max_rr = UINT_MAX; + } + + r = dns_packet_append_rr(p, j->rr, 0, NULL, NULL); + if (r == -EMSGSIZE) { + if (max_rr == 0) + /* If max_rr == 0, do not allocate more packets. */ + goto finalize; + + /* If we're unable to stuff all known answers into the given packet, allocate + * a new one, push the RR into that one and link it to the current one. */ + + DNS_PACKET_HEADER(p)->ancount = htobe16(ancount); + ancount = 0; + + r = dns_packet_new_query(&p->more, p->protocol, 0, true); + if (r < 0) + return r; + + /* continue with new packet */ + p = p->more; + r = dns_packet_append_rr(p, j->rr, 0, NULL, NULL); + } + + if (r < 0) + return r; + + ancount++; + } + +finalize: + DNS_PACKET_HEADER(p)->ancount = htobe16(ancount); + + return 0; +} + +void dns_cache_dump(DnsCache *cache, FILE *f) { + DnsCacheItem *i; + + if (!cache) + return; + + if (!f) + f = stdout; + + HASHMAP_FOREACH(i, cache->by_key) + LIST_FOREACH(by_key, j, i) { + + fputc('\t', f); + + if (j->rr) { + const char *t; + t = dns_resource_record_to_string(j->rr); + if (!t) { + log_oom(); + continue; + } + + fputs(t, f); + fputc('\n', f); + } else { + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + fputs(dns_resource_key_to_string(j->key, key_str, sizeof key_str), f); + fputs(" -- ", f); + fputs(dns_cache_item_type_to_string(j), f); + fputc('\n', f); + } + } +} + +bool dns_cache_is_empty(DnsCache *cache) { + if (!cache) + return true; + + return hashmap_isempty(cache->by_key); +} + +unsigned dns_cache_size(DnsCache *cache) { + if (!cache) + return 0; + + return hashmap_size(cache->by_key); +} diff --git a/src/resolve/resolved-dns-cache.h b/src/resolve/resolved-dns-cache.h new file mode 100644 index 0000000..42e744a --- /dev/null +++ b/src/resolve/resolved-dns-cache.h @@ -0,0 +1,57 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "hashmap.h" +#include "list.h" +#include "prioq.h" +#include "resolve-util.h" +#include "resolved-dns-dnssec.h" +#include "time-util.h" + +typedef struct DnsCache { + Hashmap *by_key; + Prioq *by_expiry; + unsigned n_hit; + unsigned n_miss; +} DnsCache; + +#include "resolved-dns-answer.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-question.h" +#include "resolved-dns-rr.h" + +void dns_cache_flush(DnsCache *c); +void dns_cache_prune(DnsCache *c); + +int dns_cache_put( + DnsCache *c, + DnsCacheMode cache_mode, + DnsProtocol protocol, + DnsResourceKey *key, + int rcode, + DnsAnswer *answer, + DnsPacket *full_packet, + uint64_t query_flags, + DnssecResult dnssec_result, + uint32_t nsec_ttl, + int owner_family, + const union in_addr_union *owner_address); + +int dns_cache_lookup( + DnsCache *c, + DnsResourceKey *key, + uint64_t query_flags, + int *ret_rcode, + DnsAnswer **ret_answer, + DnsPacket **ret_full_packet, + uint64_t *ret_query_flags, + DnssecResult *ret_dnssec_result); + +int dns_cache_check_conflicts(DnsCache *cache, DnsResourceRecord *rr, int owner_family, const union in_addr_union *owner_address); + +void dns_cache_dump(DnsCache *cache, FILE *f); +bool dns_cache_is_empty(DnsCache *cache); + +unsigned dns_cache_size(DnsCache *cache); + +int dns_cache_export_shared_to_packet(DnsCache *cache, DnsPacket *p, usec_t ts, unsigned max_rr); diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c new file mode 100644 index 0000000..f63cd9b --- /dev/null +++ b/src/resolve/resolved-dns-dnssec.c @@ -0,0 +1,2583 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "fileio.h" +#include "gcrypt-util.h" +#include "hexdecoct.h" +#include "memory-util.h" +#include "openssl-util.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-packet.h" +#include "sort-util.h" +#include "string-table.h" + +#if PREFER_OPENSSL +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(RSA*, RSA_free, NULL); +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(EC_KEY*, EC_KEY_free, NULL); +# pragma GCC diagnostic pop +#endif + +#define VERIFY_RRS_MAX 256 +#define MAX_KEY_SIZE (32*1024) + +/* Permit a maximum clock skew of 1h 10min. This should be enough to deal with DST confusion */ +#define SKEW_MAX (1*USEC_PER_HOUR + 10*USEC_PER_MINUTE) + +/* Maximum number of NSEC3 iterations we'll do. RFC5155 says 2500 shall be the maximum useful value */ +#define NSEC3_ITERATIONS_MAX 2500 + +/* + * The DNSSEC Chain of trust: + * + * Normal RRs are protected via RRSIG RRs in combination with DNSKEY RRs, all in the same zone + * DNSKEY RRs are either protected like normal RRs, or via a DS from a zone "higher" up the tree + * DS RRs are protected like normal RRs + * + * Example chain: + * Normal RR → RRSIG/DNSKEY+ → DS → RRSIG/DNSKEY+ → DS → ... → DS → RRSIG/DNSKEY+ → DS + */ + +uint16_t dnssec_keytag(DnsResourceRecord *dnskey, bool mask_revoke) { + const uint8_t *p; + uint32_t sum, f; + + /* The algorithm from RFC 4034, Appendix B. */ + + assert(dnskey); + assert(dnskey->key->type == DNS_TYPE_DNSKEY); + + f = (uint32_t) dnskey->dnskey.flags; + + if (mask_revoke) + f &= ~DNSKEY_FLAG_REVOKE; + + sum = f + ((((uint32_t) dnskey->dnskey.protocol) << 8) + (uint32_t) dnskey->dnskey.algorithm); + + p = dnskey->dnskey.key; + + for (size_t i = 0; i < dnskey->dnskey.key_size; i++) + sum += (i & 1) == 0 ? (uint32_t) p[i] << 8 : (uint32_t) p[i]; + + sum += (sum >> 16) & UINT32_C(0xFFFF); + + return sum & UINT32_C(0xFFFF); +} + +#if HAVE_OPENSSL_OR_GCRYPT + +static int rr_compare(DnsResourceRecord * const *a, DnsResourceRecord * const *b) { + const DnsResourceRecord *x = *a, *y = *b; + size_t m; + int r; + + /* Let's order the RRs according to RFC 4034, Section 6.3 */ + + assert(x); + assert(x->wire_format); + assert(y); + assert(y->wire_format); + + m = MIN(DNS_RESOURCE_RECORD_RDATA_SIZE(x), DNS_RESOURCE_RECORD_RDATA_SIZE(y)); + + r = memcmp(DNS_RESOURCE_RECORD_RDATA(x), DNS_RESOURCE_RECORD_RDATA(y), m); + if (r != 0) + return r; + + return CMP(DNS_RESOURCE_RECORD_RDATA_SIZE(x), DNS_RESOURCE_RECORD_RDATA_SIZE(y)); +} + +static int dnssec_rsa_verify_raw( + hash_algorithm_t hash_algorithm, + const void *signature, size_t signature_size, + const void *data, size_t data_size, + const void *exponent, size_t exponent_size, + const void *modulus, size_t modulus_size) { + int r; + +#if PREFER_OPENSSL +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" + _cleanup_(RSA_freep) RSA *rpubkey = NULL; + _cleanup_(EVP_PKEY_freep) EVP_PKEY *epubkey = NULL; + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *ctx = NULL; + _cleanup_(BN_freep) BIGNUM *e = NULL, *m = NULL; + + assert(hash_algorithm); + + e = BN_bin2bn(exponent, exponent_size, NULL); + if (!e) + return -EIO; + + m = BN_bin2bn(modulus, modulus_size, NULL); + if (!m) + return -EIO; + + rpubkey = RSA_new(); + if (!rpubkey) + return -ENOMEM; + + if (RSA_set0_key(rpubkey, m, e, NULL) <= 0) + return -EIO; + e = m = NULL; + + assert((size_t) RSA_size(rpubkey) == signature_size); + + epubkey = EVP_PKEY_new(); + if (!epubkey) + return -ENOMEM; + + if (EVP_PKEY_assign_RSA(epubkey, RSAPublicKey_dup(rpubkey)) <= 0) + return -EIO; + + ctx = EVP_PKEY_CTX_new(epubkey, NULL); + if (!ctx) + return -ENOMEM; + + if (EVP_PKEY_verify_init(ctx) <= 0) + return -EIO; + + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0) + return -EIO; + + if (EVP_PKEY_CTX_set_signature_md(ctx, hash_algorithm) <= 0) + return -EIO; + + r = EVP_PKEY_verify(ctx, signature, signature_size, data, data_size); + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), + "Signature verification failed: 0x%lx", ERR_get_error()); + +# pragma GCC diagnostic pop +#else + gcry_sexp_t public_key_sexp = NULL, data_sexp = NULL, signature_sexp = NULL; + gcry_mpi_t n = NULL, e = NULL, s = NULL; + gcry_error_t ge; + + assert(hash_algorithm); + + ge = gcry_mpi_scan(&s, GCRYMPI_FMT_USG, signature, signature_size, NULL); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_mpi_scan(&e, GCRYMPI_FMT_USG, exponent, exponent_size, NULL); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_mpi_scan(&n, GCRYMPI_FMT_USG, modulus, modulus_size, NULL); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&signature_sexp, + NULL, + "(sig-val (rsa (s %m)))", + s); + + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&data_sexp, + NULL, + "(data (flags pkcs1) (hash %s %b))", + hash_algorithm, + (int) data_size, + data); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&public_key_sexp, + NULL, + "(public-key (rsa (n %m) (e %m)))", + n, + e); + if (ge != 0) { + r = -EIO; + goto finish; + } + + ge = gcry_pk_verify(signature_sexp, data_sexp, public_key_sexp); + if (gpg_err_code(ge) == GPG_ERR_BAD_SIGNATURE) + r = 0; + else if (ge != 0) + r = log_debug_errno(SYNTHETIC_ERRNO(EIO), + "RSA signature check failed: %s", gpg_strerror(ge)); + else + r = 1; + +finish: + if (e) + gcry_mpi_release(e); + if (n) + gcry_mpi_release(n); + if (s) + gcry_mpi_release(s); + + if (public_key_sexp) + gcry_sexp_release(public_key_sexp); + if (signature_sexp) + gcry_sexp_release(signature_sexp); + if (data_sexp) + gcry_sexp_release(data_sexp); +#endif + return r; +} + +static int dnssec_rsa_verify( + hash_algorithm_t hash_algorithm, + const void *hash, size_t hash_size, + DnsResourceRecord *rrsig, + DnsResourceRecord *dnskey) { + + size_t exponent_size, modulus_size; + void *exponent, *modulus; + + assert(hash_algorithm); + assert(hash); + assert(hash_size > 0); + assert(rrsig); + assert(dnskey); + + if (*(uint8_t*) dnskey->dnskey.key == 0) { + /* exponent is > 255 bytes long */ + + exponent = (uint8_t*) dnskey->dnskey.key + 3; + exponent_size = + ((size_t) (((uint8_t*) dnskey->dnskey.key)[1]) << 8) | + ((size_t) ((uint8_t*) dnskey->dnskey.key)[2]); + + if (exponent_size < 256) + return -EINVAL; + + if (3 + exponent_size >= dnskey->dnskey.key_size) + return -EINVAL; + + modulus = (uint8_t*) dnskey->dnskey.key + 3 + exponent_size; + modulus_size = dnskey->dnskey.key_size - 3 - exponent_size; + + } else { + /* exponent is <= 255 bytes long */ + + exponent = (uint8_t*) dnskey->dnskey.key + 1; + exponent_size = (size_t) ((uint8_t*) dnskey->dnskey.key)[0]; + + if (exponent_size <= 0) + return -EINVAL; + + if (1 + exponent_size >= dnskey->dnskey.key_size) + return -EINVAL; + + modulus = (uint8_t*) dnskey->dnskey.key + 1 + exponent_size; + modulus_size = dnskey->dnskey.key_size - 1 - exponent_size; + } + + return dnssec_rsa_verify_raw( + hash_algorithm, + rrsig->rrsig.signature, rrsig->rrsig.signature_size, + hash, hash_size, + exponent, exponent_size, + modulus, modulus_size); +} + +static int dnssec_ecdsa_verify_raw( + hash_algorithm_t hash_algorithm, + elliptic_curve_t curve, + const void *signature_r, size_t signature_r_size, + const void *signature_s, size_t signature_s_size, + const void *data, size_t data_size, + const void *key, size_t key_size) { + int k; + +#if PREFER_OPENSSL +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" + _cleanup_(EC_GROUP_freep) EC_GROUP *ec_group = NULL; + _cleanup_(EC_POINT_freep) EC_POINT *p = NULL; + _cleanup_(EC_KEY_freep) EC_KEY *eckey = NULL; + _cleanup_(BN_CTX_freep) BN_CTX *bctx = NULL; + _cleanup_(BN_freep) BIGNUM *r = NULL, *s = NULL; + _cleanup_(ECDSA_SIG_freep) ECDSA_SIG *sig = NULL; + + assert(hash_algorithm); + + ec_group = EC_GROUP_new_by_curve_name(curve); + if (!ec_group) + return -ENOMEM; + + p = EC_POINT_new(ec_group); + if (!p) + return -ENOMEM; + + bctx = BN_CTX_new(); + if (!bctx) + return -ENOMEM; + + if (EC_POINT_oct2point(ec_group, p, key, key_size, bctx) <= 0) + return -EIO; + + eckey = EC_KEY_new(); + if (!eckey) + return -ENOMEM; + + if (EC_KEY_set_group(eckey, ec_group) <= 0) + return -EIO; + + if (EC_KEY_set_public_key(eckey, p) <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), + "EC_POINT_bn2point failed: 0x%lx", ERR_get_error()); + + assert(EC_KEY_check_key(eckey) == 1); + + r = BN_bin2bn(signature_r, signature_r_size, NULL); + if (!r) + return -EIO; + + s = BN_bin2bn(signature_s, signature_s_size, NULL); + if (!s) + return -EIO; + + /* TODO: We should eventually use use the EVP API once it supports ECDSA signature verification */ + + sig = ECDSA_SIG_new(); + if (!sig) + return -ENOMEM; + + if (ECDSA_SIG_set0(sig, r, s) <= 0) + return -EIO; + r = s = NULL; + + k = ECDSA_do_verify(data, data_size, sig, eckey); + if (k < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), + "Signature verification failed: 0x%lx", ERR_get_error()); + +# pragma GCC diagnostic pop +#else + gcry_sexp_t public_key_sexp = NULL, data_sexp = NULL, signature_sexp = NULL; + gcry_mpi_t q = NULL, r = NULL, s = NULL; + gcry_error_t ge; + + assert(hash_algorithm); + + ge = gcry_mpi_scan(&r, GCRYMPI_FMT_USG, signature_r, signature_r_size, NULL); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_mpi_scan(&s, GCRYMPI_FMT_USG, signature_s, signature_s_size, NULL); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_mpi_scan(&q, GCRYMPI_FMT_USG, key, key_size, NULL); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&signature_sexp, + NULL, + "(sig-val (ecdsa (r %m) (s %m)))", + r, + s); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&data_sexp, + NULL, + "(data (flags rfc6979) (hash %s %b))", + hash_algorithm, + (int) data_size, + data); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&public_key_sexp, + NULL, + "(public-key (ecc (curve %s) (q %m)))", + curve, + q); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_pk_verify(signature_sexp, data_sexp, public_key_sexp); + if (gpg_err_code(ge) == GPG_ERR_BAD_SIGNATURE) + k = 0; + else if (ge != 0) { + log_debug("ECDSA signature check failed: %s", gpg_strerror(ge)); + k = -EIO; + } else + k = 1; +finish: + if (r) + gcry_mpi_release(r); + if (s) + gcry_mpi_release(s); + if (q) + gcry_mpi_release(q); + + if (public_key_sexp) + gcry_sexp_release(public_key_sexp); + if (signature_sexp) + gcry_sexp_release(signature_sexp); + if (data_sexp) + gcry_sexp_release(data_sexp); +#endif + return k; +} + +static int dnssec_ecdsa_verify( + hash_algorithm_t hash_algorithm, + int algorithm, + const void *hash, size_t hash_size, + DnsResourceRecord *rrsig, + DnsResourceRecord *dnskey) { + + elliptic_curve_t curve; + size_t key_size; + uint8_t *q; + + assert(hash); + assert(hash_size); + assert(rrsig); + assert(dnskey); + + if (algorithm == DNSSEC_ALGORITHM_ECDSAP256SHA256) { + curve = OPENSSL_OR_GCRYPT(NID_X9_62_prime256v1, "NIST P-256"); /* NIST P-256 */ + key_size = 32; + } else if (algorithm == DNSSEC_ALGORITHM_ECDSAP384SHA384) { + curve = OPENSSL_OR_GCRYPT(NID_secp384r1, "NIST P-384"); /* NIST P-384 */ + key_size = 48; + } else + return -EOPNOTSUPP; + + if (dnskey->dnskey.key_size != key_size * 2) + return -EINVAL; + + if (rrsig->rrsig.signature_size != key_size * 2) + return -EINVAL; + + q = newa(uint8_t, key_size*2 + 1); + q[0] = 0x04; /* Prepend 0x04 to indicate an uncompressed key */ + memcpy(q+1, dnskey->dnskey.key, key_size*2); + + return dnssec_ecdsa_verify_raw( + hash_algorithm, + curve, + rrsig->rrsig.signature, key_size, + (uint8_t*) rrsig->rrsig.signature + key_size, key_size, + hash, hash_size, + q, key_size*2+1); +} + +static int dnssec_eddsa_verify_raw( + elliptic_curve_t curve, + const uint8_t *signature, size_t signature_size, + const uint8_t *data, size_t data_size, + const uint8_t *key, size_t key_size) { + +#if PREFER_OPENSSL + _cleanup_(EVP_PKEY_freep) EVP_PKEY *evkey = NULL; + _cleanup_(EVP_PKEY_CTX_freep) EVP_PKEY_CTX *pctx = NULL; + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *ctx = NULL; + int r; + + assert(curve == NID_ED25519); + assert(signature_size == key_size * 2); + + uint8_t *q = newa(uint8_t, signature_size + 1); + q[0] = 0x04; /* Prepend 0x04 to indicate an uncompressed key */ + memcpy(q+1, signature, signature_size); + + evkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, key, key_size); + if (!evkey) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), + "EVP_PKEY_new_raw_public_key failed: 0x%lx", ERR_get_error()); + + pctx = EVP_PKEY_CTX_new(evkey, NULL); + if (!pctx) + return -ENOMEM; + + ctx = EVP_MD_CTX_new(); + if (!ctx) + return -ENOMEM; + + /* This prevents EVP_DigestVerifyInit from managing pctx and complicating our free logic. */ + EVP_MD_CTX_set_pkey_ctx(ctx, pctx); + + /* One might be tempted to use EVP_PKEY_verify_init, but see Ed25519(7ssl). */ + if (EVP_DigestVerifyInit(ctx, &pctx, NULL, NULL, evkey) <= 0) + return -EIO; + + r = EVP_DigestVerify(ctx, signature, signature_size, data, data_size); + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EIO), + "Signature verification failed: 0x%lx", ERR_get_error()); + + return r; + +#elif GCRYPT_VERSION_NUMBER >= 0x010600 + gcry_sexp_t public_key_sexp = NULL, data_sexp = NULL, signature_sexp = NULL; + gcry_error_t ge; + int k; + + assert(signature_size == key_size * 2); + + ge = gcry_sexp_build(&signature_sexp, + NULL, + "(sig-val (eddsa (r %b) (s %b)))", + (int) key_size, + signature, + (int) key_size, + signature + key_size); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&data_sexp, + NULL, + "(data (flags eddsa) (hash-algo sha512) (value %b))", + (int) data_size, + data); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_sexp_build(&public_key_sexp, + NULL, + "(public-key (ecc (curve %s) (flags eddsa) (q %b)))", + curve, + (int) key_size, + key); + if (ge != 0) { + k = -EIO; + goto finish; + } + + ge = gcry_pk_verify(signature_sexp, data_sexp, public_key_sexp); + if (gpg_err_code(ge) == GPG_ERR_BAD_SIGNATURE) + k = 0; + else if (ge != 0) + k = log_debug_errno(SYNTHETIC_ERRNO(EIO), + "EdDSA signature check failed: %s", gpg_strerror(ge)); + else + k = 1; +finish: + if (public_key_sexp) + gcry_sexp_release(public_key_sexp); + if (signature_sexp) + gcry_sexp_release(signature_sexp); + if (data_sexp) + gcry_sexp_release(data_sexp); + + return k; +#else + return -EOPNOTSUPP; +#endif +} + +static int dnssec_eddsa_verify( + int algorithm, + const void *data, size_t data_size, + DnsResourceRecord *rrsig, + DnsResourceRecord *dnskey) { + elliptic_curve_t curve; + size_t key_size; + + if (algorithm == DNSSEC_ALGORITHM_ED25519) { + curve = OPENSSL_OR_GCRYPT(NID_ED25519, "Ed25519"); + key_size = 32; + } else + return -EOPNOTSUPP; + + if (dnskey->dnskey.key_size != key_size) + return -EINVAL; + + if (rrsig->rrsig.signature_size != key_size * 2) + return -EINVAL; + + return dnssec_eddsa_verify_raw( + curve, + rrsig->rrsig.signature, rrsig->rrsig.signature_size, + data, data_size, + dnskey->dnskey.key, key_size); +} + +static int md_add_uint8(hash_context_t ctx, uint8_t v) { +#if PREFER_OPENSSL + return EVP_DigestUpdate(ctx, &v, sizeof(v)); +#else + gcry_md_write(ctx, &v, sizeof(v)); + return 0; +#endif +} + +static int md_add_uint16(hash_context_t ctx, uint16_t v) { + v = htobe16(v); +#if PREFER_OPENSSL + return EVP_DigestUpdate(ctx, &v, sizeof(v)); +#else + gcry_md_write(ctx, &v, sizeof(v)); + return 0; +#endif +} + +static void fwrite_uint8(FILE *fp, uint8_t v) { + fwrite(&v, sizeof(v), 1, fp); +} + +static void fwrite_uint16(FILE *fp, uint16_t v) { + v = htobe16(v); + fwrite(&v, sizeof(v), 1, fp); +} + +static void fwrite_uint32(FILE *fp, uint32_t v) { + v = htobe32(v); + fwrite(&v, sizeof(v), 1, fp); +} + +static int dnssec_rrsig_prepare(DnsResourceRecord *rrsig) { + int n_key_labels, n_signer_labels; + const char *name; + int r; + + /* Checks whether the specified RRSIG RR is somewhat valid, and initializes the .n_skip_labels_source + * and .n_skip_labels_signer fields so that we can use them later on. */ + + assert(rrsig); + assert(rrsig->key->type == DNS_TYPE_RRSIG); + + /* Check if this RRSIG RR is already prepared */ + if (rrsig->n_skip_labels_source != UINT8_MAX) + return 0; + + if (rrsig->rrsig.inception > rrsig->rrsig.expiration) + return -EINVAL; + + name = dns_resource_key_name(rrsig->key); + + n_key_labels = dns_name_count_labels(name); + if (n_key_labels < 0) + return n_key_labels; + if (rrsig->rrsig.labels > n_key_labels) + return -EINVAL; + + n_signer_labels = dns_name_count_labels(rrsig->rrsig.signer); + if (n_signer_labels < 0) + return n_signer_labels; + if (n_signer_labels > rrsig->rrsig.labels) + return -EINVAL; + + r = dns_name_skip(name, n_key_labels - n_signer_labels, &name); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + /* Check if the signer is really a suffix of us */ + r = dns_name_equal(name, rrsig->rrsig.signer); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + assert(n_key_labels < UINT8_MAX); /* UINT8_MAX/-1 means unsigned. */ + rrsig->n_skip_labels_source = n_key_labels - rrsig->rrsig.labels; + rrsig->n_skip_labels_signer = n_key_labels - n_signer_labels; + + return 0; +} + +static int dnssec_rrsig_expired(DnsResourceRecord *rrsig, usec_t realtime) { + usec_t expiration, inception, skew; + + assert(rrsig); + assert(rrsig->key->type == DNS_TYPE_RRSIG); + + if (realtime == USEC_INFINITY) + realtime = now(CLOCK_REALTIME); + + expiration = rrsig->rrsig.expiration * USEC_PER_SEC; + inception = rrsig->rrsig.inception * USEC_PER_SEC; + + /* Consider inverted validity intervals as expired */ + if (inception > expiration) + return true; + + /* Permit a certain amount of clock skew of 10% of the valid + * time range. This takes inspiration from unbound's + * resolver. */ + skew = (expiration - inception) / 10; + if (skew > SKEW_MAX) + skew = SKEW_MAX; + + if (inception < skew) + inception = 0; + else + inception -= skew; + + if (expiration + skew < expiration) + expiration = USEC_INFINITY; + else + expiration += skew; + + return realtime < inception || realtime > expiration; +} + +static hash_md_t algorithm_to_implementation_id(uint8_t algorithm) { + + /* Translates a DNSSEC signature algorithm into an openssl/gcrypt digest identifier. + * + * Note that we implement all algorithms listed as "Must implement" and "Recommended to Implement" in + * RFC6944. We don't implement any algorithms that are listed as "Optional" or "Must Not Implement". + * Specifically, we do not implement RSAMD5, DSASHA1, DH, DSA-NSEC3-SHA1, and GOST-ECC. */ + + switch (algorithm) { + + case DNSSEC_ALGORITHM_RSASHA1: + case DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1: + return OPENSSL_OR_GCRYPT(EVP_sha1(), GCRY_MD_SHA1); + + case DNSSEC_ALGORITHM_RSASHA256: + case DNSSEC_ALGORITHM_ECDSAP256SHA256: + return OPENSSL_OR_GCRYPT(EVP_sha256(), GCRY_MD_SHA256); + + case DNSSEC_ALGORITHM_ECDSAP384SHA384: + return OPENSSL_OR_GCRYPT(EVP_sha384(), GCRY_MD_SHA384); + + case DNSSEC_ALGORITHM_RSASHA512: + return OPENSSL_OR_GCRYPT(EVP_sha512(), GCRY_MD_SHA512); + + default: + return OPENSSL_OR_GCRYPT(NULL, -EOPNOTSUPP); + } +} + +static void dnssec_fix_rrset_ttl( + DnsResourceRecord *list[], + unsigned n, + DnsResourceRecord *rrsig) { + + assert(list); + assert(n > 0); + assert(rrsig); + + for (unsigned k = 0; k < n; k++) { + DnsResourceRecord *rr = list[k]; + + /* Pick the TTL as the minimum of the RR's TTL, the + * RR's original TTL according to the RRSIG and the + * RRSIG's own TTL, see RFC 4035, Section 5.3.3 */ + rr->ttl = MIN3(rr->ttl, rrsig->rrsig.original_ttl, rrsig->ttl); + rr->expiry = rrsig->rrsig.expiration * USEC_PER_SEC; + + /* Copy over information about the signer and wildcard source of synthesis */ + rr->n_skip_labels_source = rrsig->n_skip_labels_source; + rr->n_skip_labels_signer = rrsig->n_skip_labels_signer; + } + + rrsig->expiry = rrsig->rrsig.expiration * USEC_PER_SEC; +} + +static int dnssec_rrset_serialize_sig( + DnsResourceRecord *rrsig, + const char *source, + DnsResourceRecord **list, + size_t list_len, + bool wildcard, + char **ret_sig_data, + size_t *ret_sig_size) { + + _cleanup_free_ char *sig_data = NULL; + size_t sig_size = 0; + _cleanup_fclose_ FILE *f = NULL; + uint8_t wire_format_name[DNS_WIRE_FORMAT_HOSTNAME_MAX]; + DnsResourceRecord *rr; + int r; + + assert(rrsig); + assert(source); + assert(list || list_len == 0); + assert(ret_sig_data); + assert(ret_sig_size); + + f = open_memstream_unlocked(&sig_data, &sig_size); + if (!f) + return -ENOMEM; + + fwrite_uint16(f, rrsig->rrsig.type_covered); + fwrite_uint8(f, rrsig->rrsig.algorithm); + fwrite_uint8(f, rrsig->rrsig.labels); + fwrite_uint32(f, rrsig->rrsig.original_ttl); + fwrite_uint32(f, rrsig->rrsig.expiration); + fwrite_uint32(f, rrsig->rrsig.inception); + fwrite_uint16(f, rrsig->rrsig.key_tag); + + r = dns_name_to_wire_format(rrsig->rrsig.signer, wire_format_name, sizeof(wire_format_name), true); + if (r < 0) + return r; + fwrite(wire_format_name, 1, r, f); + + /* Convert the source of synthesis into wire format */ + r = dns_name_to_wire_format(source, wire_format_name, sizeof(wire_format_name), true); + if (r < 0) + return r; + + for (size_t k = 0; k < list_len; k++) { + size_t l; + + rr = list[k]; + + /* Hash the source of synthesis. If this is a wildcard, then prefix it with the *. label */ + if (wildcard) + fwrite((uint8_t[]) { 1, '*'}, sizeof(uint8_t), 2, f); + fwrite(wire_format_name, 1, r, f); + + fwrite_uint16(f, rr->key->type); + fwrite_uint16(f, rr->key->class); + fwrite_uint32(f, rrsig->rrsig.original_ttl); + + l = DNS_RESOURCE_RECORD_RDATA_SIZE(rr); + assert(l <= 0xFFFF); + + fwrite_uint16(f, (uint16_t) l); + fwrite(DNS_RESOURCE_RECORD_RDATA(rr), 1, l, f); + } + + r = fflush_and_check(f); + f = safe_fclose(f); /* sig_data may be reallocated when f is closed. */ + if (r < 0) + return r; + + *ret_sig_data = TAKE_PTR(sig_data); + *ret_sig_size = sig_size; + return 0; +} + +static int dnssec_rrset_verify_sig( + DnsResourceRecord *rrsig, + DnsResourceRecord *dnskey, + const char *sig_data, + size_t sig_size) { + + assert(rrsig); + assert(dnskey); + assert(sig_data); + assert(sig_size > 0); + + hash_md_t md_algorithm; + +#if PREFER_OPENSSL + uint8_t hash[EVP_MAX_MD_SIZE]; + unsigned hash_size; +#else + _cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL; + void *hash; + size_t hash_size; + + initialize_libgcrypt(false); +#endif + + switch (rrsig->rrsig.algorithm) { + case DNSSEC_ALGORITHM_ED25519: +#if PREFER_OPENSSL || GCRYPT_VERSION_NUMBER >= 0x010600 + return dnssec_eddsa_verify( + rrsig->rrsig.algorithm, + sig_data, sig_size, + rrsig, + dnskey); +#endif + case DNSSEC_ALGORITHM_ED448: + return -EOPNOTSUPP; + default: + /* OK, the RRs are now in canonical order. Let's calculate the digest */ + md_algorithm = algorithm_to_implementation_id(rrsig->rrsig.algorithm); +#if PREFER_OPENSSL + if (!md_algorithm) + return -EOPNOTSUPP; + + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + if (!ctx) + return -ENOMEM; + + if (EVP_DigestInit_ex(ctx, md_algorithm, NULL) <= 0) + return -EIO; + + if (EVP_DigestUpdate(ctx, sig_data, sig_size) <= 0) + return -EIO; + + if (EVP_DigestFinal_ex(ctx, hash, &hash_size) <= 0) + return -EIO; + + assert(hash_size > 0); + +#else + if (md_algorithm < 0) + return md_algorithm; + + gcry_error_t err = gcry_md_open(&md, md_algorithm, 0); + if (gcry_err_code(err) != GPG_ERR_NO_ERROR || !md) + return -EIO; + + hash_size = gcry_md_get_algo_dlen(md_algorithm); + assert(hash_size > 0); + + gcry_md_write(md, sig_data, sig_size); + + hash = gcry_md_read(md, 0); + if (!hash) + return -EIO; +#endif + } + + switch (rrsig->rrsig.algorithm) { + + case DNSSEC_ALGORITHM_RSASHA1: + case DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1: + case DNSSEC_ALGORITHM_RSASHA256: + case DNSSEC_ALGORITHM_RSASHA512: + return dnssec_rsa_verify( + OPENSSL_OR_GCRYPT(md_algorithm, gcry_md_algo_name(md_algorithm)), + hash, hash_size, + rrsig, + dnskey); + + case DNSSEC_ALGORITHM_ECDSAP256SHA256: + case DNSSEC_ALGORITHM_ECDSAP384SHA384: + return dnssec_ecdsa_verify( + OPENSSL_OR_GCRYPT(md_algorithm, gcry_md_algo_name(md_algorithm)), + rrsig->rrsig.algorithm, + hash, hash_size, + rrsig, + dnskey); + + default: + assert_not_reached(); + } +} + +int dnssec_verify_rrset( + DnsAnswer *a, + const DnsResourceKey *key, + DnsResourceRecord *rrsig, + DnsResourceRecord *dnskey, + usec_t realtime, + DnssecResult *result) { + + DnsResourceRecord **list, *rr; + const char *source, *name; + _cleanup_free_ char *sig_data = NULL; + size_t sig_size = 0; /* avoid false maybe-uninitialized warning */ + size_t n = 0; + bool wildcard; + int r; + + assert(key); + assert(rrsig); + assert(dnskey); + assert(result); + assert(rrsig->key->type == DNS_TYPE_RRSIG); + assert(dnskey->key->type == DNS_TYPE_DNSKEY); + + /* Verifies that the RRSet matches the specified "key" in "a", + * using the signature "rrsig" and the key "dnskey". It's + * assumed that RRSIG and DNSKEY match. */ + + r = dnssec_rrsig_prepare(rrsig); + if (r == -EINVAL) { + *result = DNSSEC_INVALID; + return r; + } + if (r < 0) + return r; + + r = dnssec_rrsig_expired(rrsig, realtime); + if (r < 0) + return r; + if (r > 0) { + *result = DNSSEC_SIGNATURE_EXPIRED; + return 0; + } + + name = dns_resource_key_name(key); + + /* Some keys may only appear signed in the zone apex, and are invalid anywhere else. (SOA, NS...) */ + if (dns_type_apex_only(rrsig->rrsig.type_covered)) { + r = dns_name_equal(rrsig->rrsig.signer, name); + if (r < 0) + return r; + if (r == 0) { + *result = DNSSEC_INVALID; + return 0; + } + } + + /* OTOH DS RRs may not appear in the zone apex, but are valid everywhere else. */ + if (rrsig->rrsig.type_covered == DNS_TYPE_DS) { + r = dns_name_equal(rrsig->rrsig.signer, name); + if (r < 0) + return r; + if (r > 0) { + *result = DNSSEC_INVALID; + return 0; + } + } + + /* Determine the "Source of Synthesis" and whether this is a wildcard RRSIG */ + r = dns_name_suffix(name, rrsig->rrsig.labels, &source); + if (r < 0) + return r; + if (r > 0 && !dns_type_may_wildcard(rrsig->rrsig.type_covered)) { + /* We refuse to validate NSEC3 or SOA RRs that are synthesized from wildcards */ + *result = DNSSEC_INVALID; + return 0; + } + if (r == 1) { + /* If we stripped a single label, then let's see if that maybe was "*". If so, we are not really + * synthesized from a wildcard, we are the wildcard itself. Treat that like a normal name. */ + r = dns_name_startswith(name, "*"); + if (r < 0) + return r; + if (r > 0) + source = name; + + wildcard = r == 0; + } else + wildcard = r > 0; + + /* Collect all relevant RRs in a single array, so that we can look at the RRset */ + list = newa(DnsResourceRecord *, dns_answer_size(a)); + + DNS_ANSWER_FOREACH(rr, a) { + r = dns_resource_key_equal(key, rr->key); + if (r < 0) + return r; + if (r == 0) + continue; + + /* We need the wire format for ordering, and digest calculation */ + r = dns_resource_record_to_wire_format(rr, true); + if (r < 0) + return r; + + list[n++] = rr; + + if (n > VERIFY_RRS_MAX) + return -E2BIG; + } + + if (n <= 0) + return -ENODATA; + + /* Bring the RRs into canonical order */ + typesafe_qsort(list, n, rr_compare); + + r = dnssec_rrset_serialize_sig(rrsig, source, list, n, wildcard, + &sig_data, &sig_size); + if (r < 0) + return r; + + r = dnssec_rrset_verify_sig(rrsig, dnskey, sig_data, sig_size); + if (r == -EOPNOTSUPP) { + *result = DNSSEC_UNSUPPORTED_ALGORITHM; + return 0; + } + if (r < 0) + return r; + + /* Now, fix the ttl, expiry, and remember the synthesizing source and the signer */ + if (r > 0) + dnssec_fix_rrset_ttl(list, n, rrsig); + + if (r == 0) + *result = DNSSEC_INVALID; + else if (wildcard) + *result = DNSSEC_VALIDATED_WILDCARD; + else + *result = DNSSEC_VALIDATED; + + return 0; +} + +int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, bool revoked_ok) { + + assert(rrsig); + assert(dnskey); + + /* Checks if the specified DNSKEY RR matches the key used for + * the signature in the specified RRSIG RR */ + + if (rrsig->key->type != DNS_TYPE_RRSIG) + return -EINVAL; + + if (dnskey->key->type != DNS_TYPE_DNSKEY) + return 0; + if (dnskey->key->class != rrsig->key->class) + return 0; + if ((dnskey->dnskey.flags & DNSKEY_FLAG_ZONE_KEY) == 0) + return 0; + if (!revoked_ok && (dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE)) + return 0; + if (dnskey->dnskey.protocol != 3) + return 0; + if (dnskey->dnskey.algorithm != rrsig->rrsig.algorithm) + return 0; + + if (dnssec_keytag(dnskey, false) != rrsig->rrsig.key_tag) + return 0; + + return dns_name_equal(dns_resource_key_name(dnskey->key), rrsig->rrsig.signer); +} + +int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig) { + assert(key); + assert(rrsig); + + /* Checks if the specified RRSIG RR protects the RRSet of the specified RR key. */ + + if (rrsig->key->type != DNS_TYPE_RRSIG) + return 0; + if (rrsig->key->class != key->class) + return 0; + if (rrsig->rrsig.type_covered != key->type) + return 0; + + return dns_name_equal(dns_resource_key_name(rrsig->key), dns_resource_key_name(key)); +} + +int dnssec_verify_rrset_search( + DnsAnswer *a, + const DnsResourceKey *key, + DnsAnswer *validated_dnskeys, + usec_t realtime, + DnssecResult *result, + DnsResourceRecord **ret_rrsig) { + + bool found_rrsig = false, found_invalid = false, found_expired_rrsig = false, found_unsupported_algorithm = false; + DnsResourceRecord *rrsig; + int r; + + assert(key); + assert(result); + + /* Verifies all RRs from "a" that match the key "key" against DNSKEYs in "validated_dnskeys" */ + + if (dns_answer_isempty(a)) + return -ENODATA; + + /* Iterate through each RRSIG RR. */ + DNS_ANSWER_FOREACH(rrsig, a) { + DnsResourceRecord *dnskey; + DnsAnswerFlags flags; + + /* Is this an RRSIG RR that applies to RRs matching our key? */ + r = dnssec_key_match_rrsig(key, rrsig); + if (r < 0) + return r; + if (r == 0) + continue; + + found_rrsig = true; + + /* Look for a matching key */ + DNS_ANSWER_FOREACH_FLAGS(dnskey, flags, validated_dnskeys) { + DnssecResult one_result; + + if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) + continue; + + /* Is this a DNSKEY RR that matches they key of our RRSIG? */ + r = dnssec_rrsig_match_dnskey(rrsig, dnskey, false); + if (r < 0) + return r; + if (r == 0) + continue; + + /* Take the time here, if it isn't set yet, so + * that we do all validations with the same + * time. */ + if (realtime == USEC_INFINITY) + realtime = now(CLOCK_REALTIME); + + /* Yay, we found a matching RRSIG with a matching + * DNSKEY, awesome. Now let's verify all entries of + * the RRSet against the RRSIG and DNSKEY + * combination. */ + + r = dnssec_verify_rrset(a, key, rrsig, dnskey, realtime, &one_result); + if (r < 0) + return r; + + switch (one_result) { + + case DNSSEC_VALIDATED: + case DNSSEC_VALIDATED_WILDCARD: + /* Yay, the RR has been validated, + * return immediately, but fix up the expiry */ + if (ret_rrsig) + *ret_rrsig = rrsig; + + *result = one_result; + return 0; + + case DNSSEC_INVALID: + /* If the signature is invalid, let's try another + key and/or signature. After all they + key_tags and stuff are not unique, and + might be shared by multiple keys. */ + found_invalid = true; + continue; + + case DNSSEC_UNSUPPORTED_ALGORITHM: + /* If the key algorithm is + unsupported, try another + RRSIG/DNSKEY pair, but remember we + encountered this, so that we can + return a proper error when we + encounter nothing better. */ + found_unsupported_algorithm = true; + continue; + + case DNSSEC_SIGNATURE_EXPIRED: + /* If the signature is expired, try + another one, but remember it, so + that we can return this */ + found_expired_rrsig = true; + continue; + + default: + assert_not_reached(); + } + } + } + + if (found_expired_rrsig) + *result = DNSSEC_SIGNATURE_EXPIRED; + else if (found_unsupported_algorithm) + *result = DNSSEC_UNSUPPORTED_ALGORITHM; + else if (found_invalid) + *result = DNSSEC_INVALID; + else if (found_rrsig) + *result = DNSSEC_MISSING_KEY; + else + *result = DNSSEC_NO_SIGNATURE; + + if (ret_rrsig) + *ret_rrsig = NULL; + + return 0; +} + +int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key) { + DnsResourceRecord *rr; + int r; + + /* Checks whether there's at least one RRSIG in 'a' that protects RRs of the specified key */ + + DNS_ANSWER_FOREACH(rr, a) { + r = dnssec_key_match_rrsig(key, rr); + if (r < 0) + return r; + if (r > 0) + return 1; + } + + return 0; +} + +static hash_md_t digest_to_hash_md(uint8_t algorithm) { + + /* Translates a DNSSEC digest algorithm into an openssl/gcrypt digest identifier */ + + switch (algorithm) { + + case DNSSEC_DIGEST_SHA1: + return OPENSSL_OR_GCRYPT(EVP_sha1(), GCRY_MD_SHA1); + + case DNSSEC_DIGEST_SHA256: + return OPENSSL_OR_GCRYPT(EVP_sha256(), GCRY_MD_SHA256); + + case DNSSEC_DIGEST_SHA384: + return OPENSSL_OR_GCRYPT(EVP_sha384(), GCRY_MD_SHA384); + + default: + return OPENSSL_OR_GCRYPT(NULL, -EOPNOTSUPP); + } +} + +int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke) { + uint8_t wire_format[DNS_WIRE_FORMAT_HOSTNAME_MAX]; + int r; + + assert(dnskey); + assert(ds); + + /* Implements DNSKEY verification by a DS, according to RFC 4035, section 5.2 */ + + if (dnskey->key->type != DNS_TYPE_DNSKEY) + return -EINVAL; + if (ds->key->type != DNS_TYPE_DS) + return -EINVAL; + if ((dnskey->dnskey.flags & DNSKEY_FLAG_ZONE_KEY) == 0) + return -EKEYREJECTED; + if (!mask_revoke && (dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE)) + return -EKEYREJECTED; + if (dnskey->dnskey.protocol != 3) + return -EKEYREJECTED; + + if (dnskey->dnskey.algorithm != ds->ds.algorithm) + return 0; + if (dnssec_keytag(dnskey, mask_revoke) != ds->ds.key_tag) + return 0; + + r = dns_name_to_wire_format(dns_resource_key_name(dnskey->key), wire_format, sizeof wire_format, true); + if (r < 0) + return r; + + hash_md_t md_algorithm = digest_to_hash_md(ds->ds.digest_type); + +#if PREFER_OPENSSL + if (!md_algorithm) + return -EOPNOTSUPP; + + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *ctx = NULL; + uint8_t result[EVP_MAX_MD_SIZE]; + + unsigned hash_size = EVP_MD_size(md_algorithm); + assert(hash_size > 0); + + if (ds->ds.digest_size != hash_size) + return 0; + + ctx = EVP_MD_CTX_new(); + if (!ctx) + return -ENOMEM; + + if (EVP_DigestInit_ex(ctx, md_algorithm, NULL) <= 0) + return -EIO; + + if (EVP_DigestUpdate(ctx, wire_format, r) <= 0) + return -EIO; + + if (mask_revoke) + md_add_uint16(ctx, dnskey->dnskey.flags & ~DNSKEY_FLAG_REVOKE); + else + md_add_uint16(ctx, dnskey->dnskey.flags); + + r = md_add_uint8(ctx, dnskey->dnskey.protocol); + if (r <= 0) + return r; + r = md_add_uint8(ctx, dnskey->dnskey.algorithm); + if (r <= 0) + return r; + if (EVP_DigestUpdate(ctx, dnskey->dnskey.key, dnskey->dnskey.key_size) <= 0) + return -EIO; + + if (EVP_DigestFinal_ex(ctx, result, NULL) <= 0) + return -EIO; + +#else + if (md_algorithm < 0) + return -EOPNOTSUPP; + + initialize_libgcrypt(false); + + _cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL; + + size_t hash_size = gcry_md_get_algo_dlen(md_algorithm); + assert(hash_size > 0); + + if (ds->ds.digest_size != hash_size) + return 0; + + gcry_error_t err = gcry_md_open(&md, md_algorithm, 0); + if (gcry_err_code(err) != GPG_ERR_NO_ERROR || !md) + return -EIO; + + gcry_md_write(md, wire_format, r); + if (mask_revoke) + md_add_uint16(md, dnskey->dnskey.flags & ~DNSKEY_FLAG_REVOKE); + else + md_add_uint16(md, dnskey->dnskey.flags); + md_add_uint8(md, dnskey->dnskey.protocol); + md_add_uint8(md, dnskey->dnskey.algorithm); + gcry_md_write(md, dnskey->dnskey.key, dnskey->dnskey.key_size); + + void *result = gcry_md_read(md, 0); + if (!result) + return -EIO; +#endif + + return memcmp(result, ds->ds.digest, ds->ds.digest_size) == 0; +} + +int dnssec_verify_dnskey_by_ds_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds) { + DnsResourceRecord *ds; + DnsAnswerFlags flags; + int r; + + assert(dnskey); + + if (dnskey->key->type != DNS_TYPE_DNSKEY) + return 0; + + DNS_ANSWER_FOREACH_FLAGS(ds, flags, validated_ds) { + + if ((flags & DNS_ANSWER_AUTHENTICATED) == 0) + continue; + + if (ds->key->type != DNS_TYPE_DS) + continue; + if (ds->key->class != dnskey->key->class) + continue; + + r = dns_name_equal(dns_resource_key_name(dnskey->key), dns_resource_key_name(ds->key)); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dnssec_verify_dnskey_by_ds(dnskey, ds, false); + if (IN_SET(r, -EKEYREJECTED, -EOPNOTSUPP)) + return 0; /* The DNSKEY is revoked or otherwise invalid, or we don't support the digest algorithm */ + if (r < 0) + return r; + if (r > 0) + return 1; + } + + return 0; +} + +static hash_md_t nsec3_hash_to_hash_md(uint8_t algorithm) { + + /* Translates a DNSSEC NSEC3 hash algorithm into an openssl/gcrypt digest identifier */ + + switch (algorithm) { + + case NSEC3_ALGORITHM_SHA1: + return OPENSSL_OR_GCRYPT(EVP_sha1(), GCRY_MD_SHA1); + + default: + return OPENSSL_OR_GCRYPT(NULL, -EOPNOTSUPP); + } +} + +int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret) { + uint8_t wire_format[DNS_WIRE_FORMAT_HOSTNAME_MAX]; + int r; + + assert(nsec3); + assert(name); + assert(ret); + + if (nsec3->key->type != DNS_TYPE_NSEC3) + return -EINVAL; + + if (nsec3->nsec3.iterations > NSEC3_ITERATIONS_MAX) + return log_debug_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Ignoring NSEC3 RR %s with excessive number of iterations.", + dns_resource_record_to_string(nsec3)); + + hash_md_t algorithm = nsec3_hash_to_hash_md(nsec3->nsec3.algorithm); +#if PREFER_OPENSSL + if (!algorithm) + return -EOPNOTSUPP; + + size_t hash_size = EVP_MD_size(algorithm); + assert(hash_size > 0); + + if (nsec3->nsec3.next_hashed_name_size != hash_size) + return -EINVAL; + + _cleanup_(EVP_MD_CTX_freep) EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + if (!ctx) + return -ENOMEM; + + if (EVP_DigestInit_ex(ctx, algorithm, NULL) <= 0) + return -EIO; + + r = dns_name_to_wire_format(name, wire_format, sizeof(wire_format), true); + if (r < 0) + return r; + + if (EVP_DigestUpdate(ctx, wire_format, r) <= 0) + return -EIO; + if (EVP_DigestUpdate(ctx, nsec3->nsec3.salt, nsec3->nsec3.salt_size) <= 0) + return -EIO; + + uint8_t result[EVP_MAX_MD_SIZE]; + if (EVP_DigestFinal_ex(ctx, result, NULL) <= 0) + return -EIO; + + for (unsigned k = 0; k < nsec3->nsec3.iterations; k++) { + if (EVP_DigestInit_ex(ctx, algorithm, NULL) <= 0) + return -EIO; + if (EVP_DigestUpdate(ctx, result, hash_size) <= 0) + return -EIO; + if (EVP_DigestUpdate(ctx, nsec3->nsec3.salt, nsec3->nsec3.salt_size) <= 0) + return -EIO; + + if (EVP_DigestFinal_ex(ctx, result, NULL) <= 0) + return -EIO; + } +#else + if (algorithm < 0) + return algorithm; + + initialize_libgcrypt(false); + + unsigned hash_size = gcry_md_get_algo_dlen(algorithm); + assert(hash_size > 0); + + if (nsec3->nsec3.next_hashed_name_size != hash_size) + return -EINVAL; + + r = dns_name_to_wire_format(name, wire_format, sizeof(wire_format), true); + if (r < 0) + return r; + + _cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL; + gcry_error_t err = gcry_md_open(&md, algorithm, 0); + if (gcry_err_code(err) != GPG_ERR_NO_ERROR || !md) + return -EIO; + + gcry_md_write(md, wire_format, r); + gcry_md_write(md, nsec3->nsec3.salt, nsec3->nsec3.salt_size); + + void *result = gcry_md_read(md, 0); + if (!result) + return -EIO; + + for (unsigned k = 0; k < nsec3->nsec3.iterations; k++) { + uint8_t tmp[hash_size]; + memcpy(tmp, result, hash_size); + + gcry_md_reset(md); + gcry_md_write(md, tmp, hash_size); + gcry_md_write(md, nsec3->nsec3.salt, nsec3->nsec3.salt_size); + + result = gcry_md_read(md, 0); + if (!result) + return -EIO; + } +#endif + + memcpy(ret, result, hash_size); + return (int) hash_size; +} + +static int nsec3_is_good(DnsResourceRecord *rr, DnsResourceRecord *nsec3) { + const char *a, *b; + int r; + + assert(rr); + + if (rr->key->type != DNS_TYPE_NSEC3) + return 0; + + /* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */ + if (!IN_SET(rr->nsec3.flags, 0, 1)) + return 0; + + /* Ignore NSEC3 RRs whose algorithm we don't know */ +#if PREFER_OPENSSL + if (!nsec3_hash_to_hash_md(rr->nsec3.algorithm)) + return 0; +#else + if (nsec3_hash_to_hash_md(rr->nsec3.algorithm) < 0) + return 0; +#endif + + /* Ignore NSEC3 RRs with an excessive number of required iterations */ + if (rr->nsec3.iterations > NSEC3_ITERATIONS_MAX) + return 0; + + /* Ignore NSEC3 RRs generated from wildcards. If these NSEC3 RRs weren't correctly signed we can't make this + * check (since rr->n_skip_labels_source is -1), but that's OK, as we won't trust them anyway in that case. */ + if (!IN_SET(rr->n_skip_labels_source, 0, UINT8_MAX)) + return 0; + /* Ignore NSEC3 RRs that are located anywhere else than one label below the zone */ + if (!IN_SET(rr->n_skip_labels_signer, 1, UINT8_MAX)) + return 0; + + if (!nsec3) + return 1; + + /* If a second NSEC3 RR is specified, also check if they are from the same zone. */ + + if (nsec3 == rr) /* Shortcut */ + return 1; + + if (rr->key->class != nsec3->key->class) + return 0; + if (rr->nsec3.algorithm != nsec3->nsec3.algorithm) + return 0; + if (rr->nsec3.iterations != nsec3->nsec3.iterations) + return 0; + if (rr->nsec3.salt_size != nsec3->nsec3.salt_size) + return 0; + if (memcmp_safe(rr->nsec3.salt, nsec3->nsec3.salt, rr->nsec3.salt_size) != 0) + return 0; + + a = dns_resource_key_name(rr->key); + r = dns_name_parent(&a); /* strip off hash */ + if (r <= 0) + return r; + + b = dns_resource_key_name(nsec3->key); + r = dns_name_parent(&b); /* strip off hash */ + if (r <= 0) + return r; + + /* Make sure both have the same parent */ + return dns_name_equal(a, b); +} + +static int nsec3_hashed_domain_format(const uint8_t *hashed, size_t hashed_size, const char *zone, char **ret) { + _cleanup_free_ char *l = NULL; + char *j; + + assert(hashed); + assert(hashed_size > 0); + assert(zone); + assert(ret); + + l = base32hexmem(hashed, hashed_size, false); + if (!l) + return -ENOMEM; + + j = strjoin(l, ".", zone); + if (!j) + return -ENOMEM; + + *ret = j; + return (int) hashed_size; +} + +static int nsec3_hashed_domain_make(DnsResourceRecord *nsec3, const char *domain, const char *zone, char **ret) { + uint8_t hashed[DNSSEC_HASH_SIZE_MAX]; + int hashed_size; + + assert(nsec3); + assert(domain); + assert(zone); + assert(ret); + + hashed_size = dnssec_nsec3_hash(nsec3, domain, hashed); + if (hashed_size < 0) + return hashed_size; + + return nsec3_hashed_domain_format(hashed, (size_t) hashed_size, zone, ret); +} + +/* See RFC 5155, Section 8 + * First try to find a NSEC3 record that matches our query precisely, if that fails, find the closest + * enclosure. Secondly, find a proof that there is no closer enclosure and either a proof that there + * is no wildcard domain as a direct descendant of the closest enclosure, or find an NSEC3 record that + * matches the wildcard domain. + * + * Based on this we can prove either the existence of the record in @key, or NXDOMAIN or NODATA, or + * that there is no proof either way. The latter is the case if a proof of non-existence of a given + * name uses an NSEC3 record with the opt-out bit set. Lastly, if we are given insufficient NSEC3 records + * to conclude anything we indicate this by returning NO_RR. */ +static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) { + _cleanup_free_ char *next_closer_domain = NULL, *wildcard_domain = NULL; + const char *zone, *p, *pp = NULL, *wildcard; + DnsResourceRecord *rr, *enclosure_rr, *zone_rr, *wildcard_rr = NULL; + DnsAnswerFlags flags; + int hashed_size, r; + bool a, no_closer = false, no_wildcard = false, optout = false; + + assert(key); + assert(result); + + /* First step, find the zone name and the NSEC3 parameters of the zone. + * it is sufficient to look for the longest common suffix we find with + * any NSEC3 RR in the response. Any NSEC3 record will do as all NSEC3 + * records from a given zone in a response must use the same + * parameters. */ + zone = dns_resource_key_name(key); + for (;;) { + DNS_ANSWER_FOREACH_FLAGS(zone_rr, flags, answer) { + r = nsec3_is_good(zone_rr, NULL); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dns_name_equal_skip(dns_resource_key_name(zone_rr->key), 1, zone); + if (r < 0) + return r; + if (r > 0) + goto found_zone; + } + + /* Strip one label from the front */ + r = dns_name_parent(&zone); + if (r < 0) + return r; + if (r == 0) + break; + } + + *result = DNSSEC_NSEC_NO_RR; + return 0; + +found_zone: + /* Second step, find the closest encloser NSEC3 RR in 'answer' that matches 'key' */ + p = dns_resource_key_name(key); + for (;;) { + _cleanup_free_ char *hashed_domain = NULL; + + hashed_size = nsec3_hashed_domain_make(zone_rr, p, zone, &hashed_domain); + if (hashed_size == -EOPNOTSUPP) { + *result = DNSSEC_NSEC_UNSUPPORTED_ALGORITHM; + return 0; + } + if (hashed_size < 0) + return hashed_size; + + DNS_ANSWER_FOREACH_FLAGS(enclosure_rr, flags, answer) { + + r = nsec3_is_good(enclosure_rr, zone_rr); + if (r < 0) + return r; + if (r == 0) + continue; + + if (enclosure_rr->nsec3.next_hashed_name_size != (size_t) hashed_size) + continue; + + r = dns_name_equal(dns_resource_key_name(enclosure_rr->key), hashed_domain); + if (r < 0) + return r; + if (r > 0) { + a = flags & DNS_ANSWER_AUTHENTICATED; + goto found_closest_encloser; + } + } + + /* We didn't find the closest encloser with this name, + * but let's remember this domain name, it might be + * the next closer name */ + + pp = p; + + /* Strip one label from the front */ + r = dns_name_parent(&p); + if (r < 0) + return r; + if (r == 0) + break; + } + + *result = DNSSEC_NSEC_NO_RR; + return 0; + +found_closest_encloser: + /* We found a closest encloser in 'p'; next closer is 'pp' */ + + if (!pp) { + /* We have an exact match! If we area looking for a DS RR, then we must insist that we got the NSEC3 RR + * from the parent. Otherwise the one from the child. Do so, by checking whether SOA and NS are + * appropriately set. */ + + if (key->type == DNS_TYPE_DS) { + if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_SOA)) + return -EBADMSG; + } else { + if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_NS) && + !bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_SOA)) + return -EBADMSG; + } + + /* No next closer NSEC3 RR. That means there's a direct NSEC3 RR for our key. */ + if (bitmap_isset(enclosure_rr->nsec3.types, key->type)) + *result = DNSSEC_NSEC_FOUND; + else if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_CNAME)) + *result = DNSSEC_NSEC_CNAME; + else + *result = DNSSEC_NSEC_NODATA; + + if (authenticated) + *authenticated = a; + if (ttl) + *ttl = enclosure_rr->ttl; + + return 0; + } + + /* Ensure this is not a DNAME domain, see RFC5155, section 8.3. */ + if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_DNAME)) + return -EBADMSG; + + /* Ensure that this data is from the delegated domain + * (i.e. originates from the "lower" DNS server), and isn't + * just glue records (i.e. doesn't originate from the "upper" + * DNS server). */ + if (bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_NS) && + !bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_SOA)) + return -EBADMSG; + + /* Prove that there is no next closer and whether or not there is a wildcard domain. */ + + wildcard = strjoina("*.", p); + r = nsec3_hashed_domain_make(enclosure_rr, wildcard, zone, &wildcard_domain); + if (r < 0) + return r; + if (r != hashed_size) + return -EBADMSG; + + r = nsec3_hashed_domain_make(enclosure_rr, pp, zone, &next_closer_domain); + if (r < 0) + return r; + if (r != hashed_size) + return -EBADMSG; + + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + _cleanup_free_ char *next_hashed_domain = NULL; + + r = nsec3_is_good(rr, zone_rr); + if (r < 0) + return r; + if (r == 0) + continue; + + r = nsec3_hashed_domain_format(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size, zone, &next_hashed_domain); + if (r < 0) + return r; + + r = dns_name_between(dns_resource_key_name(rr->key), next_closer_domain, next_hashed_domain); + if (r < 0) + return r; + if (r > 0) { + if (rr->nsec3.flags & 1) + optout = true; + + a = a && (flags & DNS_ANSWER_AUTHENTICATED); + + no_closer = true; + } + + r = dns_name_equal(dns_resource_key_name(rr->key), wildcard_domain); + if (r < 0) + return r; + if (r > 0) { + a = a && (flags & DNS_ANSWER_AUTHENTICATED); + + wildcard_rr = rr; + } + + r = dns_name_between(dns_resource_key_name(rr->key), wildcard_domain, next_hashed_domain); + if (r < 0) + return r; + if (r > 0) { + if (rr->nsec3.flags & 1) + /* This only makes sense if we have a wildcard delegation, which is + * very unlikely, see RFC 4592, Section 4.2, but we cannot rely on + * this not happening, so hence cannot simply conclude NXDOMAIN as + * we would wish */ + optout = true; + + a = a && (flags & DNS_ANSWER_AUTHENTICATED); + + no_wildcard = true; + } + } + + if (wildcard_rr && no_wildcard) + return -EBADMSG; + + if (!no_closer) { + *result = DNSSEC_NSEC_NO_RR; + return 0; + } + + if (wildcard_rr) { + /* A wildcard exists that matches our query. */ + if (optout) + /* This is not specified in any RFC to the best of my knowledge, but + * if the next closer enclosure is covered by an opt-out NSEC3 RR + * it means that we cannot prove that the source of synthesis is + * correct, as there may be a closer match. */ + *result = DNSSEC_NSEC_OPTOUT; + else if (bitmap_isset(wildcard_rr->nsec3.types, key->type)) + *result = DNSSEC_NSEC_FOUND; + else if (bitmap_isset(wildcard_rr->nsec3.types, DNS_TYPE_CNAME)) + *result = DNSSEC_NSEC_CNAME; + else + *result = DNSSEC_NSEC_NODATA; + } else { + if (optout) + /* The RFC only specifies that we have to care for optout for NODATA for + * DS records. However, children of an insecure opt-out delegation should + * also be considered opt-out, rather than verified NXDOMAIN. + * Note that we do not require a proof of wildcard non-existence if the + * next closer domain is covered by an opt-out, as that would not provide + * any additional information. */ + *result = DNSSEC_NSEC_OPTOUT; + else if (no_wildcard) + *result = DNSSEC_NSEC_NXDOMAIN; + else { + *result = DNSSEC_NSEC_NO_RR; + + return 0; + } + } + + if (authenticated) + *authenticated = a; + + if (ttl) + *ttl = enclosure_rr->ttl; + + return 0; +} + +static int dnssec_nsec_wildcard_equal(DnsResourceRecord *rr, const char *name) { + char label[DNS_LABEL_MAX]; + const char *n; + int r; + + assert(rr); + assert(rr->key->type == DNS_TYPE_NSEC); + + /* Checks whether the specified RR has a name beginning in "*.", and if the rest is a suffix of our name */ + + if (rr->n_skip_labels_source != 1) + return 0; + + n = dns_resource_key_name(rr->key); + r = dns_label_unescape(&n, label, sizeof label, 0); + if (r <= 0) + return r; + if (r != 1 || label[0] != '*') + return 0; + + return dns_name_endswith(name, n); +} + +static int dnssec_nsec_in_path(DnsResourceRecord *rr, const char *name) { + const char *nn, *common_suffix; + int r; + + assert(rr); + assert(rr->key->type == DNS_TYPE_NSEC); + + /* Checks whether the specified nsec RR indicates that name is an empty non-terminal (ENT) + * + * A couple of examples: + * + * NSEC bar → waldo.foo.bar: indicates that foo.bar exists and is an ENT + * NSEC waldo.foo.bar → yyy.zzz.xoo.bar: indicates that xoo.bar and zzz.xoo.bar exist and are ENTs + * NSEC yyy.zzz.xoo.bar → bar: indicates pretty much nothing about ENTs + */ + + /* First, determine parent of next domain. */ + nn = rr->nsec.next_domain_name; + r = dns_name_parent(&nn); + if (r <= 0) + return r; + + /* If the name we just determined is not equal or child of the name we are interested in, then we can't say + * anything at all. */ + r = dns_name_endswith(nn, name); + if (r <= 0) + return r; + + /* If the name we are interested in is not a prefix of the common suffix of the NSEC RR's owner and next domain names, then we can't say anything either. */ + r = dns_name_common_suffix(dns_resource_key_name(rr->key), rr->nsec.next_domain_name, &common_suffix); + if (r < 0) + return r; + + return dns_name_endswith(name, common_suffix); +} + +static int dnssec_nsec_from_parent_zone(DnsResourceRecord *rr, const char *name) { + int r; + + assert(rr); + assert(rr->key->type == DNS_TYPE_NSEC); + + /* Checks whether this NSEC originates to the parent zone or the child zone. */ + + r = dns_name_parent(&name); + if (r <= 0) + return r; + + r = dns_name_equal(name, dns_resource_key_name(rr->key)); + if (r <= 0) + return r; + + /* DNAME, and NS without SOA is an indication for a delegation. */ + if (bitmap_isset(rr->nsec.types, DNS_TYPE_DNAME)) + return 1; + + if (bitmap_isset(rr->nsec.types, DNS_TYPE_NS) && !bitmap_isset(rr->nsec.types, DNS_TYPE_SOA)) + return 1; + + return 0; +} + +static int dnssec_nsec_covers(DnsResourceRecord *rr, const char *name) { + const char *signer; + int r; + + assert(rr); + assert(rr->key->type == DNS_TYPE_NSEC); + + /* Checks whether the name is covered by this NSEC RR. This means, that the name is somewhere below the NSEC's + * signer name, and between the NSEC's two names. */ + + r = dns_resource_record_signer(rr, &signer); + if (r < 0) + return r; + + r = dns_name_endswith(name, signer); /* this NSEC isn't suitable the name is not in the signer's domain */ + if (r <= 0) + return r; + + return dns_name_between(dns_resource_key_name(rr->key), name, rr->nsec.next_domain_name); +} + +static int dnssec_nsec_generate_wildcard(DnsResourceRecord *rr, const char *name, char **wc) { + const char *common_suffix1, *common_suffix2, *signer; + int r, labels1, labels2; + + assert(rr); + assert(rr->key->type == DNS_TYPE_NSEC); + + /* Generates "Wildcard at the Closest Encloser" for the given name and NSEC RR. */ + + r = dns_resource_record_signer(rr, &signer); + if (r < 0) + return r; + + r = dns_name_endswith(name, signer); /* this NSEC isn't suitable the name is not in the signer's domain */ + if (r <= 0) + return r; + + r = dns_name_common_suffix(name, dns_resource_key_name(rr->key), &common_suffix1); + if (r < 0) + return r; + + r = dns_name_common_suffix(name, rr->nsec.next_domain_name, &common_suffix2); + if (r < 0) + return r; + + labels1 = dns_name_count_labels(common_suffix1); + if (labels1 < 0) + return labels1; + + labels2 = dns_name_count_labels(common_suffix2); + if (labels2 < 0) + return labels2; + + if (labels1 > labels2) + r = dns_name_concat("*", common_suffix1, 0, wc); + else + r = dns_name_concat("*", common_suffix2, 0, wc); + + if (r < 0) + return r; + + return 0; +} + +int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) { + bool have_nsec3 = false, covering_rr_authenticated = false, wildcard_rr_authenticated = false; + DnsResourceRecord *rr, *covering_rr = NULL, *wildcard_rr = NULL; + DnsAnswerFlags flags; + const char *name; + int r; + + assert(key); + assert(result); + + /* Look for any NSEC/NSEC3 RRs that say something about the specified key. */ + + name = dns_resource_key_name(key); + + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + + if (rr->key->class != key->class) + continue; + + have_nsec3 = have_nsec3 || (rr->key->type == DNS_TYPE_NSEC3); + + if (rr->key->type != DNS_TYPE_NSEC) + continue; + + /* The following checks only make sense for NSEC RRs that are not expanded from a wildcard */ + r = dns_resource_record_is_synthetic(rr); + if (r == -ENODATA) /* No signing RR known. */ + continue; + if (r < 0) + return r; + if (r > 0) + continue; + + /* Check if this is a direct match. If so, we have encountered a NODATA case */ + r = dns_name_equal(dns_resource_key_name(rr->key), name); + if (r < 0) + return r; + if (r == 0) { + /* If it's not a direct match, maybe it's a wild card match? */ + r = dnssec_nsec_wildcard_equal(rr, name); + if (r < 0) + return r; + } + if (r > 0) { + if (key->type == DNS_TYPE_DS) { + /* If we look for a DS RR and the server sent us the NSEC RR of the child zone + * we have a problem. For DS RRs we want the NSEC RR from the parent */ + if (bitmap_isset(rr->nsec.types, DNS_TYPE_SOA)) + continue; + } else { + /* For all RR types, ensure that if NS is set SOA is set too, so that we know + * we got the child's NSEC. */ + if (bitmap_isset(rr->nsec.types, DNS_TYPE_NS) && + !bitmap_isset(rr->nsec.types, DNS_TYPE_SOA)) + continue; + } + + if (bitmap_isset(rr->nsec.types, key->type)) + *result = DNSSEC_NSEC_FOUND; + else if (bitmap_isset(rr->nsec.types, DNS_TYPE_CNAME)) + *result = DNSSEC_NSEC_CNAME; + else + *result = DNSSEC_NSEC_NODATA; + + if (authenticated) + *authenticated = flags & DNS_ANSWER_AUTHENTICATED; + if (ttl) + *ttl = rr->ttl; + + return 0; + } + + /* Check if the name we are looking for is an empty non-terminal within the owner or next name + * of the NSEC RR. */ + r = dnssec_nsec_in_path(rr, name); + if (r < 0) + return r; + if (r > 0) { + *result = DNSSEC_NSEC_NODATA; + + if (authenticated) + *authenticated = flags & DNS_ANSWER_AUTHENTICATED; + if (ttl) + *ttl = rr->ttl; + + return 0; + } + + /* The following two "covering" checks, are not useful if the NSEC is from the parent */ + r = dnssec_nsec_from_parent_zone(rr, name); + if (r < 0) + return r; + if (r > 0) + continue; + + /* Check if this NSEC RR proves the absence of an explicit RR under this name */ + r = dnssec_nsec_covers(rr, name); + if (r < 0) + return r; + if (r > 0 && (!covering_rr || !covering_rr_authenticated)) { + covering_rr = rr; + covering_rr_authenticated = flags & DNS_ANSWER_AUTHENTICATED; + } + } + + if (covering_rr) { + _cleanup_free_ char *wc = NULL; + r = dnssec_nsec_generate_wildcard(covering_rr, name, &wc); + if (r < 0) + return r; + + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + + if (rr->key->class != key->class) + continue; + + if (rr->key->type != DNS_TYPE_NSEC) + continue; + + /* Check if this NSEC RR proves the nonexistence of the wildcard */ + r = dnssec_nsec_covers(rr, wc); + if (r < 0) + return r; + if (r > 0 && (!wildcard_rr || !wildcard_rr_authenticated)) { + wildcard_rr = rr; + wildcard_rr_authenticated = flags & DNS_ANSWER_AUTHENTICATED; + } + } + } + + if (covering_rr && wildcard_rr) { + /* If we could prove that neither the name itself, nor the wildcard at the closest encloser exists, we + * proved the NXDOMAIN case. */ + *result = DNSSEC_NSEC_NXDOMAIN; + + if (authenticated) + *authenticated = covering_rr_authenticated && wildcard_rr_authenticated; + if (ttl) + *ttl = MIN(covering_rr->ttl, wildcard_rr->ttl); + + return 0; + } + + /* OK, this was not sufficient. Let's see if NSEC3 can help. */ + if (have_nsec3) + return dnssec_test_nsec3(answer, key, result, authenticated, ttl); + + /* No appropriate NSEC RR found, report this. */ + *result = DNSSEC_NSEC_NO_RR; + return 0; +} + +static int dnssec_nsec_test_enclosed(DnsAnswer *answer, uint16_t type, const char *name, const char *zone, bool *authenticated) { + DnsResourceRecord *rr; + DnsAnswerFlags flags; + int r; + + assert(name); + assert(zone); + + /* Checks whether there's an NSEC/NSEC3 that proves that the specified 'name' is non-existing in the specified + * 'zone'. The 'zone' must be a suffix of the 'name'. */ + + DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) { + bool found = false; + + if (rr->key->type != type && type != DNS_TYPE_ANY) + continue; + + switch (rr->key->type) { + + case DNS_TYPE_NSEC: + + /* We only care for NSEC RRs from the indicated zone */ + r = dns_resource_record_is_signer(rr, zone); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dns_name_between(dns_resource_key_name(rr->key), name, rr->nsec.next_domain_name); + if (r < 0) + return r; + + found = r > 0; + break; + + case DNS_TYPE_NSEC3: { + _cleanup_free_ char *hashed_domain = NULL, *next_hashed_domain = NULL; + + /* We only care for NSEC3 RRs from the indicated zone */ + r = dns_resource_record_is_signer(rr, zone); + if (r < 0) + return r; + if (r == 0) + continue; + + r = nsec3_is_good(rr, NULL); + if (r < 0) + return r; + if (r == 0) + break; + + /* Format the domain we are testing with the NSEC3 RR's hash function */ + r = nsec3_hashed_domain_make( + rr, + name, + zone, + &hashed_domain); + if (r < 0) + return r; + if ((size_t) r != rr->nsec3.next_hashed_name_size) + break; + + /* Format the NSEC3's next hashed name as proper domain name */ + r = nsec3_hashed_domain_format( + rr->nsec3.next_hashed_name, + rr->nsec3.next_hashed_name_size, + zone, + &next_hashed_domain); + if (r < 0) + return r; + + r = dns_name_between(dns_resource_key_name(rr->key), hashed_domain, next_hashed_domain); + if (r < 0) + return r; + + found = r > 0; + break; + } + + default: + continue; + } + + if (found) { + if (authenticated) + *authenticated = flags & DNS_ANSWER_AUTHENTICATED; + return 1; + } + } + + return 0; +} + +static int dnssec_test_positive_wildcard_nsec3( + DnsAnswer *answer, + const char *name, + const char *source, + const char *zone, + bool *authenticated) { + + const char *next_closer = NULL; + int r; + + /* Run a positive NSEC3 wildcard proof. Specifically: + * + * A proof that the "next closer" of the generating wildcard does not exist. + * + * Note a key difference between the NSEC3 and NSEC versions of the proof. NSEC RRs don't have to exist for + * empty non-transients. NSEC3 RRs however have to. This means it's sufficient to check if the next closer name + * exists for the NSEC3 RR and we are done. + * + * To prove that a.b.c.d.e.f is rightfully synthesized from a wildcard *.d.e.f all we have to check is that + * c.d.e.f does not exist. */ + + for (;;) { + next_closer = name; + r = dns_name_parent(&name); + if (r <= 0) + return r; + + r = dns_name_equal(name, source); + if (r < 0) + return r; + if (r > 0) + break; + } + + return dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC3, next_closer, zone, authenticated); +} + +static int dnssec_test_positive_wildcard_nsec( + DnsAnswer *answer, + const char *name, + const char *source, + const char *zone, + bool *_authenticated) { + + bool authenticated = true; + int r; + + /* Run a positive NSEC wildcard proof. Specifically: + * + * A proof that there's neither a wildcard name nor a non-wildcard name that is a suffix of the name "name" and + * a prefix of the synthesizing source "source" in the zone "zone". + * + * See RFC 5155, Section 8.8 and RFC 4035, Section 5.3.4 + * + * Note that if we want to prove that a.b.c.d.e.f is rightfully synthesized from a wildcard *.d.e.f, then we + * have to prove that none of the following exist: + * + * 1) a.b.c.d.e.f + * 2) *.b.c.d.e.f + * 3) b.c.d.e.f + * 4) *.c.d.e.f + * 5) c.d.e.f + */ + + for (;;) { + _cleanup_free_ char *wc = NULL; + bool a = false; + + /* Check if there's an NSEC or NSEC3 RR that proves that the mame we determined is really non-existing, + * i.e between the owner name and the next name of an NSEC RR. */ + r = dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC, name, zone, &a); + if (r <= 0) + return r; + + authenticated = authenticated && a; + + /* Strip one label off */ + r = dns_name_parent(&name); + if (r <= 0) + return r; + + /* Did we reach the source of synthesis? */ + r = dns_name_equal(name, source); + if (r < 0) + return r; + if (r > 0) { + /* Successful exit */ + *_authenticated = authenticated; + return 1; + } + + /* Safety check, that the source of synthesis is still our suffix */ + r = dns_name_endswith(name, source); + if (r < 0) + return r; + if (r == 0) + return -EBADMSG; + + /* Replace the label we stripped off with an asterisk */ + wc = strjoin("*.", name); + if (!wc) + return -ENOMEM; + + /* And check if the proof holds for the asterisk name, too */ + r = dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC, wc, zone, &a); + if (r <= 0) + return r; + + authenticated = authenticated && a; + /* In the next iteration we'll check the non-asterisk-prefixed version */ + } +} + +int dnssec_test_positive_wildcard( + DnsAnswer *answer, + const char *name, + const char *source, + const char *zone, + bool *authenticated) { + + int r; + + assert(name); + assert(source); + assert(zone); + assert(authenticated); + + r = dns_answer_contains_zone_nsec3(answer, zone); + if (r < 0) + return r; + if (r > 0) + return dnssec_test_positive_wildcard_nsec3(answer, name, source, zone, authenticated); + else + return dnssec_test_positive_wildcard_nsec(answer, name, source, zone, authenticated); +} + +#else + +int dnssec_verify_rrset( + DnsAnswer *a, + const DnsResourceKey *key, + DnsResourceRecord *rrsig, + DnsResourceRecord *dnskey, + usec_t realtime, + DnssecResult *result) { + + return -EOPNOTSUPP; +} + +int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, bool revoked_ok) { + + return -EOPNOTSUPP; +} + +int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig) { + + return -EOPNOTSUPP; +} + +int dnssec_verify_rrset_search( + DnsAnswer *a, + const DnsResourceKey *key, + DnsAnswer *validated_dnskeys, + usec_t realtime, + DnssecResult *result, + DnsResourceRecord **ret_rrsig) { + + return -EOPNOTSUPP; +} + +int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key) { + + return -EOPNOTSUPP; +} + +int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke) { + + return -EOPNOTSUPP; +} + +int dnssec_verify_dnskey_by_ds_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds) { + + return -EOPNOTSUPP; +} + +int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret) { + + return -EOPNOTSUPP; +} + +int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) { + + return -EOPNOTSUPP; +} + +int dnssec_test_positive_wildcard( + DnsAnswer *answer, + const char *name, + const char *source, + const char *zone, + bool *authenticated) { + + return -EOPNOTSUPP; +} + +#endif + +static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = { + [DNSSEC_VALIDATED] = "validated", + [DNSSEC_VALIDATED_WILDCARD] = "validated-wildcard", + [DNSSEC_INVALID] = "invalid", + [DNSSEC_SIGNATURE_EXPIRED] = "signature-expired", + [DNSSEC_UNSUPPORTED_ALGORITHM] = "unsupported-algorithm", + [DNSSEC_NO_SIGNATURE] = "no-signature", + [DNSSEC_MISSING_KEY] = "missing-key", + [DNSSEC_UNSIGNED] = "unsigned", + [DNSSEC_FAILED_AUXILIARY] = "failed-auxiliary", + [DNSSEC_NSEC_MISMATCH] = "nsec-mismatch", + [DNSSEC_INCOMPATIBLE_SERVER] = "incompatible-server", +}; +DEFINE_STRING_TABLE_LOOKUP(dnssec_result, DnssecResult); + +static const char* const dnssec_verdict_table[_DNSSEC_VERDICT_MAX] = { + [DNSSEC_SECURE] = "secure", + [DNSSEC_INSECURE] = "insecure", + [DNSSEC_BOGUS] = "bogus", + [DNSSEC_INDETERMINATE] = "indeterminate", +}; +DEFINE_STRING_TABLE_LOOKUP(dnssec_verdict, DnssecVerdict); diff --git a/src/resolve/resolved-dns-dnssec.h b/src/resolve/resolved-dns-dnssec.h new file mode 100644 index 0000000..954bb3e --- /dev/null +++ b/src/resolve/resolved-dns-dnssec.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef enum DnssecResult DnssecResult; +typedef enum DnssecVerdict DnssecVerdict; + +#include "dns-domain.h" +#include "resolved-dns-answer.h" +#include "resolved-dns-rr.h" + +enum DnssecResult { + /* These five are returned by dnssec_verify_rrset() */ + DNSSEC_VALIDATED, + DNSSEC_VALIDATED_WILDCARD, /* Validated via a wildcard RRSIG, further NSEC/NSEC3 checks necessary */ + DNSSEC_INVALID, + DNSSEC_SIGNATURE_EXPIRED, + DNSSEC_UNSUPPORTED_ALGORITHM, + + /* These two are added by dnssec_verify_rrset_search() */ + DNSSEC_NO_SIGNATURE, + DNSSEC_MISSING_KEY, + + /* These two are added by the DnsTransaction logic */ + DNSSEC_UNSIGNED, + DNSSEC_FAILED_AUXILIARY, + DNSSEC_NSEC_MISMATCH, + DNSSEC_INCOMPATIBLE_SERVER, + + _DNSSEC_RESULT_MAX, + _DNSSEC_RESULT_INVALID = -EINVAL, +}; + +enum DnssecVerdict { + DNSSEC_SECURE, + DNSSEC_INSECURE, + DNSSEC_BOGUS, + DNSSEC_INDETERMINATE, + + _DNSSEC_VERDICT_MAX, + _DNSSEC_VERDICT_INVALID = -EINVAL, +}; + +#define DNSSEC_CANONICAL_HOSTNAME_MAX (DNS_HOSTNAME_MAX + 2) + +/* The longest digest we'll ever generate, of all digest algorithms we support */ +#define DNSSEC_HASH_SIZE_MAX (MAX(20, 32)) + +int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, bool revoked_ok); +int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig); + +int dnssec_verify_rrset(DnsAnswer *answer, const DnsResourceKey *key, DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, usec_t realtime, DnssecResult *result); +int dnssec_verify_rrset_search(DnsAnswer *answer, const DnsResourceKey *key, DnsAnswer *validated_dnskeys, usec_t realtime, DnssecResult *result, DnsResourceRecord **rrsig); + +int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke); +int dnssec_verify_dnskey_by_ds_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds); + +int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key); + +uint16_t dnssec_keytag(DnsResourceRecord *dnskey, bool mask_revoke); + +int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret); + +typedef enum DnssecNsecResult { + DNSSEC_NSEC_NO_RR, /* No suitable NSEC/NSEC3 RR found */ + DNSSEC_NSEC_CNAME, /* Didn't find what was asked for, but did find CNAME */ + DNSSEC_NSEC_UNSUPPORTED_ALGORITHM, + DNSSEC_NSEC_NXDOMAIN, + DNSSEC_NSEC_NODATA, + DNSSEC_NSEC_FOUND, + DNSSEC_NSEC_OPTOUT, +} DnssecNsecResult; + +int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl); + +int dnssec_test_positive_wildcard(DnsAnswer *a, const char *name, const char *source, const char *zone, bool *authenticated); + +const char* dnssec_result_to_string(DnssecResult m) _const_; +DnssecResult dnssec_result_from_string(const char *s) _pure_; + +const char* dnssec_verdict_to_string(DnssecVerdict m) _const_; +DnssecVerdict dnssec_verdict_from_string(const char *s) _pure_; diff --git a/src/resolve/resolved-dns-packet.c b/src/resolve/resolved-dns-packet.c new file mode 100644 index 0000000..ad0a34e --- /dev/null +++ b/src/resolve/resolved-dns-packet.c @@ -0,0 +1,2683 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if HAVE_GCRYPT +# include <gcrypt.h> +#endif + +#include "alloc-util.h" +#include "dns-domain.h" +#include "memory-util.h" +#include "resolved-dns-packet.h" +#include "set.h" +#include "stdio-util.h" +#include "string-table.h" +#include "strv.h" +#include "unaligned.h" +#include "utf8.h" +#include "util.h" + +#define EDNS0_OPT_DO (1<<15) + +assert_cc(DNS_PACKET_SIZE_START > DNS_PACKET_HEADER_SIZE); + +typedef struct DnsPacketRewinder { + DnsPacket *packet; + size_t saved_rindex; +} DnsPacketRewinder; + +static void rewind_dns_packet(DnsPacketRewinder *rewinder) { + if (rewinder->packet) + dns_packet_rewind(rewinder->packet, rewinder->saved_rindex); +} + +#define REWINDER_INIT(p) { \ + .packet = (p), \ + .saved_rindex = (p)->rindex, \ + } +#define CANCEL_REWINDER(rewinder) do { (rewinder).packet = NULL; } while (0) + +int dns_packet_new( + DnsPacket **ret, + DnsProtocol protocol, + size_t min_alloc_dsize, + size_t max_size) { + + DnsPacket *p; + size_t a; + + assert(ret); + assert(max_size >= DNS_PACKET_HEADER_SIZE); + + if (max_size > DNS_PACKET_SIZE_MAX) + max_size = DNS_PACKET_SIZE_MAX; + + /* The caller may not check what is going to be truly allocated, so do not allow to + * allocate a DNS packet bigger than DNS_PACKET_SIZE_MAX. + */ + if (min_alloc_dsize > DNS_PACKET_SIZE_MAX) + return log_error_errno(SYNTHETIC_ERRNO(EFBIG), + "Requested packet data size too big: %zu", + min_alloc_dsize); + + /* When dns_packet_new() is called with min_alloc_dsize == 0, allocate more than the + * absolute minimum (which is the dns packet header size), to avoid + * resizing immediately again after appending the first data to the packet. + */ + if (min_alloc_dsize < DNS_PACKET_HEADER_SIZE) + a = DNS_PACKET_SIZE_START; + else + a = min_alloc_dsize; + + /* round up to next page size */ + a = PAGE_ALIGN(ALIGN(sizeof(DnsPacket)) + a) - ALIGN(sizeof(DnsPacket)); + + /* make sure we never allocate more than useful */ + if (a > max_size) + a = max_size; + + p = malloc0(ALIGN(sizeof(DnsPacket)) + a); + if (!p) + return -ENOMEM; + + *p = (DnsPacket) { + .n_ref = 1, + .protocol = protocol, + .size = DNS_PACKET_HEADER_SIZE, + .rindex = DNS_PACKET_HEADER_SIZE, + .allocated = a, + .max_size = max_size, + .opt_start = SIZE_MAX, + .opt_size = SIZE_MAX, + }; + + *ret = p; + + return 0; +} + +void dns_packet_set_flags(DnsPacket *p, bool dnssec_checking_disabled, bool truncated) { + + DnsPacketHeader *h; + + assert(p); + + h = DNS_PACKET_HEADER(p); + + switch (p->protocol) { + case DNS_PROTOCOL_LLMNR: + assert(!truncated); + + h->flags = htobe16(DNS_PACKET_MAKE_FLAGS(0 /* qr */, + 0 /* opcode */, + 0 /* c */, + 0 /* tc */, + 0 /* t */, + 0 /* ra */, + 0 /* ad */, + 0 /* cd */, + 0 /* rcode */)); + break; + + case DNS_PROTOCOL_MDNS: + h->flags = htobe16(DNS_PACKET_MAKE_FLAGS(0 /* qr */, + 0 /* opcode */, + 0 /* aa */, + truncated /* tc */, + 0 /* rd (ask for recursion) */, + 0 /* ra */, + 0 /* ad */, + 0 /* cd */, + 0 /* rcode */)); + break; + + default: + assert(!truncated); + + h->flags = htobe16(DNS_PACKET_MAKE_FLAGS(0 /* qr */, + 0 /* opcode */, + 0 /* aa */, + 0 /* tc */, + 1 /* rd (ask for recursion) */, + 0 /* ra */, + 0 /* ad */, + dnssec_checking_disabled /* cd */, + 0 /* rcode */)); + } +} + +int dns_packet_new_query(DnsPacket **ret, DnsProtocol protocol, size_t min_alloc_dsize, bool dnssec_checking_disabled) { + DnsPacket *p; + int r; + + assert(ret); + + r = dns_packet_new(&p, protocol, min_alloc_dsize, DNS_PACKET_SIZE_MAX); + if (r < 0) + return r; + + /* Always set the TC bit to 0 initially. + * If there are multiple packets later, we'll update the bit shortly before sending. + */ + dns_packet_set_flags(p, dnssec_checking_disabled, false); + + *ret = p; + return 0; +} + +int dns_packet_dup(DnsPacket **ret, DnsPacket *p) { + DnsPacket *c; + int r; + + assert(ret); + assert(p); + + r = dns_packet_validate(p); + if (r < 0) + return r; + + c = malloc(ALIGN(sizeof(DnsPacket)) + p->size); + if (!c) + return -ENOMEM; + + *c = (DnsPacket) { + .n_ref = 1, + .protocol = p->protocol, + .size = p->size, + .rindex = DNS_PACKET_HEADER_SIZE, + .allocated = p->size, + .max_size = p->max_size, + .opt_start = SIZE_MAX, + .opt_size = SIZE_MAX, + }; + + memcpy(DNS_PACKET_DATA(c), DNS_PACKET_DATA(p), p->size); + + *ret = c; + return 0; +} + +DnsPacket *dns_packet_ref(DnsPacket *p) { + + if (!p) + return NULL; + + assert(!p->on_stack); + + assert(p->n_ref > 0); + p->n_ref++; + return p; +} + +static void dns_packet_free(DnsPacket *p) { + char *s; + + assert(p); + + dns_question_unref(p->question); + dns_answer_unref(p->answer); + dns_resource_record_unref(p->opt); + + while ((s = hashmap_steal_first_key(p->names))) + free(s); + hashmap_free(p->names); + + free(p->_data); + + if (!p->on_stack) + free(p); +} + +DnsPacket *dns_packet_unref(DnsPacket *p) { + if (!p) + return NULL; + + assert(p->n_ref > 0); + + dns_packet_unref(p->more); + + if (p->n_ref == 1) + dns_packet_free(p); + else + p->n_ref--; + + return NULL; +} + +int dns_packet_validate(DnsPacket *p) { + assert(p); + + if (p->size < DNS_PACKET_HEADER_SIZE) + return -EBADMSG; + + if (p->size > DNS_PACKET_SIZE_MAX) + return -EBADMSG; + + return 1; +} + +int dns_packet_validate_reply(DnsPacket *p) { + int r; + + assert(p); + + r = dns_packet_validate(p); + if (r < 0) + return r; + + if (DNS_PACKET_QR(p) != 1) + return 0; + + if (DNS_PACKET_OPCODE(p) != 0) + return -EBADMSG; + + switch (p->protocol) { + + case DNS_PROTOCOL_LLMNR: + /* RFC 4795, Section 2.1.1. says to discard all replies with QDCOUNT != 1 */ + if (DNS_PACKET_QDCOUNT(p) != 1) + return -EBADMSG; + + break; + + case DNS_PROTOCOL_MDNS: + /* RFC 6762, Section 18 */ + if (DNS_PACKET_RCODE(p) != 0) + return -EBADMSG; + + break; + + default: + break; + } + + return 1; +} + +int dns_packet_validate_query(DnsPacket *p) { + int r; + + assert(p); + + r = dns_packet_validate(p); + if (r < 0) + return r; + + if (DNS_PACKET_QR(p) != 0) + return 0; + + if (DNS_PACKET_OPCODE(p) != 0) + return -EBADMSG; + + switch (p->protocol) { + + case DNS_PROTOCOL_DNS: + if (DNS_PACKET_TC(p)) + return -EBADMSG; + + if (DNS_PACKET_QDCOUNT(p) != 1) + return -EBADMSG; + + if (DNS_PACKET_ANCOUNT(p) > 0) + return -EBADMSG; + + /* Note, in most cases, DNS query packet does not have authority section. But some query + * types, e.g. IXFR, have Authority sections. Hence, unlike the check for LLMNR, we do not + * check DNS_PACKET_NSCOUNT(p) here. */ + break; + + case DNS_PROTOCOL_LLMNR: + if (DNS_PACKET_TC(p)) + return -EBADMSG; + + /* RFC 4795, Section 2.1.1. says to discard all queries with QDCOUNT != 1 */ + if (DNS_PACKET_QDCOUNT(p) != 1) + return -EBADMSG; + + /* RFC 4795, Section 2.1.1. says to discard all queries with ANCOUNT != 0 */ + if (DNS_PACKET_ANCOUNT(p) > 0) + return -EBADMSG; + + /* RFC 4795, Section 2.1.1. says to discard all queries with NSCOUNT != 0 */ + if (DNS_PACKET_NSCOUNT(p) > 0) + return -EBADMSG; + + break; + + case DNS_PROTOCOL_MDNS: + /* Note, mDNS query may have truncation flag. So, unlike the check for DNS and LLMNR, + * we do not check DNS_PACKET_TC(p) here. */ + + /* RFC 6762, Section 18 specifies that messages with non-zero RCODE + * must be silently ignored, and that we must ignore the values of + * AA, RD, RA, AD, and CD bits. */ + if (DNS_PACKET_RCODE(p) != 0) + return -EBADMSG; + + break; + + default: + break; + } + + return 1; +} + +static int dns_packet_extend(DnsPacket *p, size_t add, void **ret, size_t *start) { + assert(p); + + if (p->size + add > p->allocated) { + size_t a, ms; + + a = PAGE_ALIGN((p->size + add) * 2); + + ms = dns_packet_size_max(p); + if (a > ms) + a = ms; + + if (p->size + add > a) + return -EMSGSIZE; + + if (p->_data) { + void *d; + + d = realloc(p->_data, a); + if (!d) + return -ENOMEM; + + p->_data = d; + } else { + p->_data = malloc(a); + if (!p->_data) + return -ENOMEM; + + memcpy(p->_data, (uint8_t*) p + ALIGN(sizeof(DnsPacket)), p->size); + memzero((uint8_t*) p->_data + p->size, a - p->size); + } + + p->allocated = a; + } + + if (start) + *start = p->size; + + if (ret) + *ret = (uint8_t*) DNS_PACKET_DATA(p) + p->size; + + p->size += add; + return 0; +} + +void dns_packet_truncate(DnsPacket *p, size_t sz) { + char *s; + void *n; + + assert(p); + + if (p->size <= sz) + return; + + HASHMAP_FOREACH_KEY(n, s, p->names) { + + if (PTR_TO_SIZE(n) < sz) + continue; + + hashmap_remove(p->names, s); + free(s); + } + + p->size = sz; +} + +int dns_packet_append_blob(DnsPacket *p, const void *d, size_t l, size_t *start) { + void *q; + int r; + + assert(p); + + r = dns_packet_extend(p, l, &q, start); + if (r < 0) + return r; + + memcpy_safe(q, d, l); + return 0; +} + +int dns_packet_append_uint8(DnsPacket *p, uint8_t v, size_t *start) { + void *d; + int r; + + assert(p); + + r = dns_packet_extend(p, sizeof(uint8_t), &d, start); + if (r < 0) + return r; + + ((uint8_t*) d)[0] = v; + + return 0; +} + +int dns_packet_append_uint16(DnsPacket *p, uint16_t v, size_t *start) { + void *d; + int r; + + assert(p); + + r = dns_packet_extend(p, sizeof(uint16_t), &d, start); + if (r < 0) + return r; + + unaligned_write_be16(d, v); + + return 0; +} + +int dns_packet_append_uint32(DnsPacket *p, uint32_t v, size_t *start) { + void *d; + int r; + + assert(p); + + r = dns_packet_extend(p, sizeof(uint32_t), &d, start); + if (r < 0) + return r; + + unaligned_write_be32(d, v); + + return 0; +} + +int dns_packet_append_string(DnsPacket *p, const char *s, size_t *start) { + assert(p); + assert(s); + + return dns_packet_append_raw_string(p, s, strlen(s), start); +} + +int dns_packet_append_raw_string(DnsPacket *p, const void *s, size_t size, size_t *start) { + void *d; + int r; + + assert(p); + assert(s || size == 0); + + if (size > 255) + return -E2BIG; + + r = dns_packet_extend(p, 1 + size, &d, start); + if (r < 0) + return r; + + ((uint8_t*) d)[0] = (uint8_t) size; + + memcpy_safe(((uint8_t*) d) + 1, s, size); + + return 0; +} + +int dns_packet_append_label(DnsPacket *p, const char *d, size_t l, bool canonical_candidate, size_t *start) { + uint8_t *w; + int r; + + /* Append a label to a packet. Optionally, does this in DNSSEC + * canonical form, if this label is marked as a candidate for + * it, and the canonical form logic is enabled for the + * packet */ + + assert(p); + assert(d); + + if (l > DNS_LABEL_MAX) + return -E2BIG; + + r = dns_packet_extend(p, 1 + l, (void**) &w, start); + if (r < 0) + return r; + + *(w++) = (uint8_t) l; + + if (p->canonical_form && canonical_candidate) + /* Generate in canonical form, as defined by DNSSEC + * RFC 4034, Section 6.2, i.e. all lower-case. */ + for (size_t i = 0; i < l; i++) + w[i] = (uint8_t) ascii_tolower(d[i]); + else + /* Otherwise, just copy the string unaltered. This is + * essential for DNS-SD, where the casing of labels + * matters and needs to be retained. */ + memcpy(w, d, l); + + return 0; +} + +int dns_packet_append_name( + DnsPacket *p, + const char *name, + bool allow_compression, + bool canonical_candidate, + size_t *start) { + + size_t saved_size; + int r; + + assert(p); + assert(name); + + if (p->refuse_compression) + allow_compression = false; + + saved_size = p->size; + + while (!dns_name_is_root(name)) { + const char *z = name; + char label[DNS_LABEL_MAX]; + size_t n = 0; + + if (allow_compression) + n = PTR_TO_SIZE(hashmap_get(p->names, name)); + if (n > 0) { + assert(n < p->size); + + if (n < 0x4000) { + r = dns_packet_append_uint16(p, 0xC000 | n, NULL); + if (r < 0) + goto fail; + + goto done; + } + } + + r = dns_label_unescape(&name, label, sizeof label, 0); + if (r < 0) + goto fail; + + r = dns_packet_append_label(p, label, r, canonical_candidate, &n); + if (r < 0) + goto fail; + + if (allow_compression) { + _cleanup_free_ char *s = NULL; + + s = strdup(z); + if (!s) { + r = -ENOMEM; + goto fail; + } + + r = hashmap_ensure_put(&p->names, &dns_name_hash_ops, s, SIZE_TO_PTR(n)); + if (r < 0) + goto fail; + + TAKE_PTR(s); + } + } + + r = dns_packet_append_uint8(p, 0, NULL); + if (r < 0) + return r; + +done: + if (start) + *start = saved_size; + + return 0; + +fail: + dns_packet_truncate(p, saved_size); + return r; +} + +int dns_packet_append_key(DnsPacket *p, const DnsResourceKey *k, const DnsAnswerFlags flags, size_t *start) { + size_t saved_size; + uint16_t class; + int r; + + assert(p); + assert(k); + + saved_size = p->size; + + r = dns_packet_append_name(p, dns_resource_key_name(k), true, true, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint16(p, k->type, NULL); + if (r < 0) + goto fail; + + class = flags & DNS_ANSWER_CACHE_FLUSH ? k->class | MDNS_RR_CACHE_FLUSH_OR_QU : k->class; + r = dns_packet_append_uint16(p, class, NULL); + if (r < 0) + goto fail; + + if (start) + *start = saved_size; + + return 0; + +fail: + dns_packet_truncate(p, saved_size); + return r; +} + +static int dns_packet_append_type_window(DnsPacket *p, uint8_t window, uint8_t length, const uint8_t *types, size_t *start) { + size_t saved_size; + int r; + + assert(p); + assert(types); + assert(length > 0); + + saved_size = p->size; + + r = dns_packet_append_uint8(p, window, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, length, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, types, length, NULL); + if (r < 0) + goto fail; + + if (start) + *start = saved_size; + + return 0; +fail: + dns_packet_truncate(p, saved_size); + return r; +} + +static int dns_packet_append_types(DnsPacket *p, Bitmap *types, size_t *start) { + uint8_t window = 0; + uint8_t entry = 0; + uint8_t bitmaps[32] = {}; + unsigned n; + size_t saved_size; + int r; + + assert(p); + + saved_size = p->size; + + BITMAP_FOREACH(n, types) { + assert(n <= 0xffff); + + if ((n >> 8) != window && bitmaps[entry / 8] != 0) { + r = dns_packet_append_type_window(p, window, entry / 8 + 1, bitmaps, NULL); + if (r < 0) + goto fail; + + zero(bitmaps); + } + + window = n >> 8; + entry = n & 255; + + bitmaps[entry / 8] |= 1 << (7 - (entry % 8)); + } + + if (bitmaps[entry / 8] != 0) { + r = dns_packet_append_type_window(p, window, entry / 8 + 1, bitmaps, NULL); + if (r < 0) + goto fail; + } + + if (start) + *start = saved_size; + + return 0; +fail: + dns_packet_truncate(p, saved_size); + return r; +} + +/* Append the OPT pseudo-RR described in RFC6891 */ +int dns_packet_append_opt( + DnsPacket *p, + uint16_t max_udp_size, + bool edns0_do, + bool include_rfc6975, + const char *nsid, + int rcode, + size_t *ret_start) { + + size_t saved_size; + int r; + + assert(p); + /* we must never advertise supported packet size smaller than the legacy max */ + assert(max_udp_size >= DNS_PACKET_UNICAST_SIZE_MAX); + assert(rcode >= 0); + assert(rcode <= _DNS_RCODE_MAX); + + if (p->opt_start != SIZE_MAX) + return -EBUSY; + + assert(p->opt_size == SIZE_MAX); + + saved_size = p->size; + + /* empty name */ + r = dns_packet_append_uint8(p, 0, NULL); + if (r < 0) + return r; + + /* type */ + r = dns_packet_append_uint16(p, DNS_TYPE_OPT, NULL); + if (r < 0) + goto fail; + + /* class: maximum udp packet that can be received */ + r = dns_packet_append_uint16(p, max_udp_size, NULL); + if (r < 0) + goto fail; + + /* extended RCODE and VERSION */ + r = dns_packet_append_uint16(p, ((uint16_t) rcode & 0x0FF0) << 4, NULL); + if (r < 0) + goto fail; + + /* flags: DNSSEC OK (DO), see RFC3225 */ + r = dns_packet_append_uint16(p, edns0_do ? EDNS0_OPT_DO : 0, NULL); + if (r < 0) + goto fail; + + if (edns0_do && include_rfc6975) { + /* If DO is on and this is requested, also append RFC6975 Algorithm data. This is supposed to + * be done on queries, not on replies, hencer callers should turn this off when finishing off + * replies. */ + + static const uint8_t rfc6975[] = { + + 0, 5, /* OPTION_CODE: DAU */ +#if PREFER_OPENSSL || (HAVE_GCRYPT && GCRYPT_VERSION_NUMBER >= 0x010600) + 0, 7, /* LIST_LENGTH */ +#else + 0, 6, /* LIST_LENGTH */ +#endif + DNSSEC_ALGORITHM_RSASHA1, + DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1, + DNSSEC_ALGORITHM_RSASHA256, + DNSSEC_ALGORITHM_RSASHA512, + DNSSEC_ALGORITHM_ECDSAP256SHA256, + DNSSEC_ALGORITHM_ECDSAP384SHA384, +#if PREFER_OPENSSL || (HAVE_GCRYPT && GCRYPT_VERSION_NUMBER >= 0x010600) + DNSSEC_ALGORITHM_ED25519, +#endif + + 0, 6, /* OPTION_CODE: DHU */ + 0, 3, /* LIST_LENGTH */ + DNSSEC_DIGEST_SHA1, + DNSSEC_DIGEST_SHA256, + DNSSEC_DIGEST_SHA384, + + 0, 7, /* OPTION_CODE: N3U */ + 0, 1, /* LIST_LENGTH */ + NSEC3_ALGORITHM_SHA1, + }; + + r = dns_packet_append_uint16(p, sizeof(rfc6975), NULL); /* RDLENGTH */ + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rfc6975, sizeof(rfc6975), NULL); /* the payload, as defined above */ + + } else if (nsid) { + + if (strlen(nsid) > UINT16_MAX - 4) { + r = -E2BIG; + goto fail; + } + + r = dns_packet_append_uint16(p, 4 + strlen(nsid), NULL); /* RDLENGTH */ + if (r < 0) + goto fail; + + r = dns_packet_append_uint16(p, 3, NULL); /* OPTION-CODE: NSID */ + if (r < 0) + goto fail; + + r = dns_packet_append_uint16(p, strlen(nsid), NULL); /* OPTION-LENGTH */ + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, nsid, strlen(nsid), NULL); + } else + r = dns_packet_append_uint16(p, 0, NULL); + if (r < 0) + goto fail; + + DNS_PACKET_HEADER(p)->arcount = htobe16(DNS_PACKET_ARCOUNT(p) + 1); + + p->opt_start = saved_size; + p->opt_size = p->size - saved_size; + + if (ret_start) + *ret_start = saved_size; + + return 0; + +fail: + dns_packet_truncate(p, saved_size); + return r; +} + +int dns_packet_truncate_opt(DnsPacket *p) { + assert(p); + + if (p->opt_start == SIZE_MAX) { + assert(p->opt_size == SIZE_MAX); + return 0; + } + + assert(p->opt_size != SIZE_MAX); + assert(DNS_PACKET_ARCOUNT(p) > 0); + + if (p->opt_start + p->opt_size != p->size) + return -EBUSY; + + dns_packet_truncate(p, p->opt_start); + DNS_PACKET_HEADER(p)->arcount = htobe16(DNS_PACKET_ARCOUNT(p) - 1); + p->opt_start = p->opt_size = SIZE_MAX; + + return 1; +} + +int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, const DnsAnswerFlags flags, size_t *start, size_t *rdata_start) { + + size_t saved_size, rdlength_offset, end, rdlength, rds; + uint32_t ttl; + int r; + + assert(p); + assert(rr); + + saved_size = p->size; + + r = dns_packet_append_key(p, rr->key, flags, NULL); + if (r < 0) + goto fail; + + ttl = flags & DNS_ANSWER_GOODBYE ? 0 : rr->ttl; + r = dns_packet_append_uint32(p, ttl, NULL); + if (r < 0) + goto fail; + + /* Initially we write 0 here */ + r = dns_packet_append_uint16(p, 0, &rdlength_offset); + if (r < 0) + goto fail; + + rds = p->size - saved_size; + + switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) { + + case DNS_TYPE_SRV: + r = dns_packet_append_uint16(p, rr->srv.priority, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint16(p, rr->srv.weight, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint16(p, rr->srv.port, NULL); + if (r < 0) + goto fail; + + /* RFC 2782 states "Unless and until permitted by future standards + * action, name compression is not to be used for this field." */ + r = dns_packet_append_name(p, rr->srv.name, false, true, NULL); + break; + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + r = dns_packet_append_name(p, rr->ptr.name, true, true, NULL); + break; + + case DNS_TYPE_HINFO: + r = dns_packet_append_string(p, rr->hinfo.cpu, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_string(p, rr->hinfo.os, NULL); + break; + + case DNS_TYPE_SPF: /* exactly the same as TXT */ + case DNS_TYPE_TXT: + + if (!rr->txt.items) { + /* RFC 6763, section 6.1 suggests to generate + * single empty string for an empty array. */ + + r = dns_packet_append_raw_string(p, NULL, 0, NULL); + if (r < 0) + goto fail; + } else + LIST_FOREACH(items, i, rr->txt.items) { + r = dns_packet_append_raw_string(p, i->data, i->length, NULL); + if (r < 0) + goto fail; + } + + r = 0; + break; + + case DNS_TYPE_A: + r = dns_packet_append_blob(p, &rr->a.in_addr, sizeof(struct in_addr), NULL); + break; + + case DNS_TYPE_AAAA: + r = dns_packet_append_blob(p, &rr->aaaa.in6_addr, sizeof(struct in6_addr), NULL); + break; + + case DNS_TYPE_SOA: + r = dns_packet_append_name(p, rr->soa.mname, true, true, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_name(p, rr->soa.rname, true, true, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->soa.serial, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->soa.refresh, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->soa.retry, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->soa.expire, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->soa.minimum, NULL); + break; + + case DNS_TYPE_MX: + r = dns_packet_append_uint16(p, rr->mx.priority, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_name(p, rr->mx.exchange, true, true, NULL); + break; + + case DNS_TYPE_LOC: + r = dns_packet_append_uint8(p, rr->loc.version, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->loc.size, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->loc.horiz_pre, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->loc.vert_pre, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->loc.latitude, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->loc.longitude, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->loc.altitude, NULL); + break; + + case DNS_TYPE_DS: + r = dns_packet_append_uint16(p, rr->ds.key_tag, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->ds.algorithm, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->ds.digest_type, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->ds.digest, rr->ds.digest_size, NULL); + break; + + case DNS_TYPE_SSHFP: + r = dns_packet_append_uint8(p, rr->sshfp.algorithm, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->sshfp.fptype, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->sshfp.fingerprint, rr->sshfp.fingerprint_size, NULL); + break; + + case DNS_TYPE_DNSKEY: + r = dns_packet_append_uint16(p, rr->dnskey.flags, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->dnskey.protocol, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->dnskey.algorithm, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->dnskey.key, rr->dnskey.key_size, NULL); + break; + + case DNS_TYPE_RRSIG: + r = dns_packet_append_uint16(p, rr->rrsig.type_covered, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->rrsig.algorithm, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->rrsig.labels, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->rrsig.original_ttl, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->rrsig.expiration, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint32(p, rr->rrsig.inception, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint16(p, rr->rrsig.key_tag, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_name(p, rr->rrsig.signer, false, true, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->rrsig.signature, rr->rrsig.signature_size, NULL); + break; + + case DNS_TYPE_NSEC: + r = dns_packet_append_name(p, rr->nsec.next_domain_name, false, false, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_types(p, rr->nsec.types, NULL); + if (r < 0) + goto fail; + + break; + + case DNS_TYPE_NSEC3: + r = dns_packet_append_uint8(p, rr->nsec3.algorithm, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->nsec3.flags, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint16(p, rr->nsec3.iterations, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->nsec3.salt_size, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->nsec3.salt, rr->nsec3.salt_size, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->nsec3.next_hashed_name_size, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_types(p, rr->nsec3.types, NULL); + if (r < 0) + goto fail; + + break; + + case DNS_TYPE_TLSA: + r = dns_packet_append_uint8(p, rr->tlsa.cert_usage, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->tlsa.selector, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_uint8(p, rr->tlsa.matching_type, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->tlsa.data, rr->tlsa.data_size, NULL); + break; + + case DNS_TYPE_CAA: + r = dns_packet_append_uint8(p, rr->caa.flags, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_string(p, rr->caa.tag, NULL); + if (r < 0) + goto fail; + + r = dns_packet_append_blob(p, rr->caa.value, rr->caa.value_size, NULL); + break; + + case DNS_TYPE_OPT: + case DNS_TYPE_OPENPGPKEY: + case _DNS_TYPE_INVALID: /* unparsable */ + default: + + r = dns_packet_append_blob(p, rr->generic.data, rr->generic.data_size, NULL); + break; + } + if (r < 0) + goto fail; + + /* Let's calculate the actual data size and update the field */ + rdlength = p->size - rdlength_offset - sizeof(uint16_t); + if (rdlength > 0xFFFF) { + r = -ENOSPC; + goto fail; + } + + end = p->size; + p->size = rdlength_offset; + r = dns_packet_append_uint16(p, rdlength, NULL); + if (r < 0) + goto fail; + p->size = end; + + if (start) + *start = saved_size; + + if (rdata_start) + *rdata_start = rds; + + return 0; + +fail: + dns_packet_truncate(p, saved_size); + return r; +} + +int dns_packet_append_question(DnsPacket *p, DnsQuestion *q) { + DnsResourceKey *key; + int r; + + assert(p); + + DNS_QUESTION_FOREACH(key, q) { + r = dns_packet_append_key(p, key, 0, NULL); + if (r < 0) + return r; + } + + return 0; +} + +int dns_packet_append_answer(DnsPacket *p, DnsAnswer *a, unsigned *completed) { + DnsResourceRecord *rr; + DnsAnswerFlags flags; + int r; + + assert(p); + + DNS_ANSWER_FOREACH_FLAGS(rr, flags, a) { + r = dns_packet_append_rr(p, rr, flags, NULL, NULL); + if (r < 0) + return r; + + if (completed) + (*completed)++; + } + + return 0; +} + +int dns_packet_read(DnsPacket *p, size_t sz, const void **ret, size_t *start) { + assert(p); + assert(p->rindex <= p->size); + + if (sz > p->size - p->rindex) + return -EMSGSIZE; + + if (ret) + *ret = (uint8_t*) DNS_PACKET_DATA(p) + p->rindex; + + if (start) + *start = p->rindex; + + p->rindex += sz; + return 0; +} + +void dns_packet_rewind(DnsPacket *p, size_t idx) { + assert(p); + assert(idx <= p->size); + assert(idx >= DNS_PACKET_HEADER_SIZE); + + p->rindex = idx; +} + +int dns_packet_read_blob(DnsPacket *p, void *d, size_t sz, size_t *start) { + const void *q; + int r; + + assert(p); + assert(d); + + r = dns_packet_read(p, sz, &q, start); + if (r < 0) + return r; + + memcpy(d, q, sz); + return 0; +} + +static int dns_packet_read_memdup( + DnsPacket *p, size_t size, + void **ret, size_t *ret_size, + size_t *ret_start) { + + const void *src; + size_t start; + int r; + + assert(p); + assert(ret); + + r = dns_packet_read(p, size, &src, &start); + if (r < 0) + return r; + + if (size <= 0) + *ret = NULL; + else { + void *copy; + + copy = memdup(src, size); + if (!copy) + return -ENOMEM; + + *ret = copy; + } + + if (ret_size) + *ret_size = size; + if (ret_start) + *ret_start = start; + + return 0; +} + +int dns_packet_read_uint8(DnsPacket *p, uint8_t *ret, size_t *start) { + const void *d; + int r; + + assert(p); + + r = dns_packet_read(p, sizeof(uint8_t), &d, start); + if (r < 0) + return r; + + *ret = ((uint8_t*) d)[0]; + return 0; +} + +int dns_packet_read_uint16(DnsPacket *p, uint16_t *ret, size_t *start) { + const void *d; + int r; + + assert(p); + + r = dns_packet_read(p, sizeof(uint16_t), &d, start); + if (r < 0) + return r; + + if (ret) + *ret = unaligned_read_be16(d); + + return 0; +} + +int dns_packet_read_uint32(DnsPacket *p, uint32_t *ret, size_t *start) { + const void *d; + int r; + + assert(p); + + r = dns_packet_read(p, sizeof(uint32_t), &d, start); + if (r < 0) + return r; + + *ret = unaligned_read_be32(d); + + return 0; +} + +int dns_packet_read_string(DnsPacket *p, char **ret, size_t *start) { + assert(p); + + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + const void *d; + char *t; + uint8_t c; + int r; + + r = dns_packet_read_uint8(p, &c, NULL); + if (r < 0) + return r; + + r = dns_packet_read(p, c, &d, NULL); + if (r < 0) + return r; + + if (memchr(d, 0, c)) + return -EBADMSG; + + t = memdup_suffix0(d, c); + if (!t) + return -ENOMEM; + + if (!utf8_is_valid(t)) { + free(t); + return -EBADMSG; + } + + *ret = t; + + if (start) + *start = rewinder.saved_rindex; + CANCEL_REWINDER(rewinder); + + return 0; +} + +int dns_packet_read_raw_string(DnsPacket *p, const void **ret, size_t *size, size_t *start) { + assert(p); + + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + uint8_t c; + int r; + + r = dns_packet_read_uint8(p, &c, NULL); + if (r < 0) + return r; + + r = dns_packet_read(p, c, ret, NULL); + if (r < 0) + return r; + + if (size) + *size = c; + if (start) + *start = rewinder.saved_rindex; + CANCEL_REWINDER(rewinder); + + return 0; +} + +int dns_packet_read_name( + DnsPacket *p, + char **ret, + bool allow_compression, + size_t *ret_start) { + + assert(p); + + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + size_t after_rindex = 0, jump_barrier = p->rindex; + _cleanup_free_ char *name = NULL; + bool first = true; + size_t n = 0; + int r; + + if (p->refuse_compression) + allow_compression = false; + + for (;;) { + uint8_t c, d; + + r = dns_packet_read_uint8(p, &c, NULL); + if (r < 0) + return r; + + if (c == 0) + /* End of name */ + break; + else if (c <= 63) { + const char *label; + + /* Literal label */ + r = dns_packet_read(p, c, (const void**) &label, NULL); + if (r < 0) + return r; + + if (!GREEDY_REALLOC(name, n + !first + DNS_LABEL_ESCAPED_MAX)) + return -ENOMEM; + + if (first) + first = false; + else + name[n++] = '.'; + + r = dns_label_escape(label, c, name + n, DNS_LABEL_ESCAPED_MAX); + if (r < 0) + return r; + + n += r; + continue; + } else if (allow_compression && FLAGS_SET(c, 0xc0)) { + uint16_t ptr; + + /* Pointer */ + r = dns_packet_read_uint8(p, &d, NULL); + if (r < 0) + return r; + + ptr = (uint16_t) (c & ~0xc0) << 8 | (uint16_t) d; + if (ptr < DNS_PACKET_HEADER_SIZE || ptr >= jump_barrier) + return -EBADMSG; + + if (after_rindex == 0) + after_rindex = p->rindex; + + /* Jumps are limited to a "prior occurrence" (RFC-1035 4.1.4) */ + jump_barrier = ptr; + p->rindex = ptr; + } else + return -EBADMSG; + } + + if (!GREEDY_REALLOC(name, n + 1)) + return -ENOMEM; + + name[n] = 0; + + if (after_rindex != 0) + p->rindex= after_rindex; + + if (ret) + *ret = TAKE_PTR(name); + if (ret_start) + *ret_start = rewinder.saved_rindex; + + CANCEL_REWINDER(rewinder); + + return 0; +} + +static int dns_packet_read_type_window(DnsPacket *p, Bitmap **types, size_t *start) { + assert(p); + assert(types); + + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + uint8_t window, length; + const uint8_t *bitmap; + uint8_t bit = 0; + bool found = false; + int r; + + r = bitmap_ensure_allocated(types); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &window, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &length, NULL); + if (r < 0) + return r; + + if (length == 0 || length > 32) + return -EBADMSG; + + r = dns_packet_read(p, length, (const void **)&bitmap, NULL); + if (r < 0) + return r; + + for (uint8_t i = 0; i < length; i++) { + uint8_t bitmask = 1 << 7; + + if (!bitmap[i]) { + found = false; + bit += 8; + continue; + } + + found = true; + + for (; bitmask; bit++, bitmask >>= 1) + if (bitmap[i] & bitmask) { + uint16_t n; + + n = (uint16_t) window << 8 | (uint16_t) bit; + + /* Ignore pseudo-types. see RFC4034 section 4.1.2 */ + if (dns_type_is_pseudo(n)) + continue; + + r = bitmap_set(*types, n); + if (r < 0) + return r; + } + } + + if (!found) + return -EBADMSG; + + if (start) + *start = rewinder.saved_rindex; + CANCEL_REWINDER(rewinder); + + return 0; +} + +static int dns_packet_read_type_windows(DnsPacket *p, Bitmap **types, size_t size, size_t *start) { + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + int r; + + while (p->rindex - rewinder.saved_rindex < size) { + r = dns_packet_read_type_window(p, types, NULL); + if (r < 0) + return r; + + assert(p->rindex >= rewinder.saved_rindex); + + /* don't read past end of current RR */ + if (p->rindex - rewinder.saved_rindex > size) + return -EBADMSG; + } + + if (p->rindex - rewinder.saved_rindex != size) + return -EBADMSG; + + if (start) + *start = rewinder.saved_rindex; + CANCEL_REWINDER(rewinder); + + return 0; +} + +int dns_packet_read_key( + DnsPacket *p, + DnsResourceKey **ret, + bool *ret_cache_flush_or_qu, + size_t *ret_start) { + + assert(p); + + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + _cleanup_free_ char *name = NULL; + bool cache_flush_or_qu = false; + uint16_t class, type; + int r; + + r = dns_packet_read_name(p, &name, true, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint16(p, &type, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint16(p, &class, NULL); + if (r < 0) + return r; + + if (p->protocol == DNS_PROTOCOL_MDNS) { + /* See RFC6762, sections 5.4 and 10.2 */ + + if (type != DNS_TYPE_OPT && (class & MDNS_RR_CACHE_FLUSH_OR_QU)) { + class &= ~MDNS_RR_CACHE_FLUSH_OR_QU; + cache_flush_or_qu = true; + } + } + + if (ret) { + DnsResourceKey *key; + + key = dns_resource_key_new_consume(class, type, name); + if (!key) + return -ENOMEM; + + TAKE_PTR(name); + *ret = key; + } + + if (ret_cache_flush_or_qu) + *ret_cache_flush_or_qu = cache_flush_or_qu; + if (ret_start) + *ret_start = rewinder.saved_rindex; + + CANCEL_REWINDER(rewinder); + return 0; +} + +static bool loc_size_ok(uint8_t size) { + uint8_t m = size >> 4, e = size & 0xF; + + return m <= 9 && e <= 9 && (m > 0 || e == 0); +} + +int dns_packet_read_rr( + DnsPacket *p, + DnsResourceRecord **ret, + bool *ret_cache_flush, + size_t *ret_start) { + + assert(p); + + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + size_t offset; + uint16_t rdlength; + bool cache_flush; + int r; + + r = dns_packet_read_key(p, &key, &cache_flush, NULL); + if (r < 0) + return r; + + if (!dns_class_is_valid_rr(key->class) || !dns_type_is_valid_rr(key->type)) + return -EBADMSG; + + rr = dns_resource_record_new(key); + if (!rr) + return -ENOMEM; + + r = dns_packet_read_uint32(p, &rr->ttl, NULL); + if (r < 0) + return r; + + /* RFC 2181, Section 8, suggests to + * treat a TTL with the MSB set as a zero TTL. */ + if (rr->ttl & UINT32_C(0x80000000)) + rr->ttl = 0; + + r = dns_packet_read_uint16(p, &rdlength, NULL); + if (r < 0) + return r; + + if (rdlength > p->size - p->rindex) + return -EBADMSG; + + offset = p->rindex; + + switch (rr->key->type) { + + case DNS_TYPE_SRV: + r = dns_packet_read_uint16(p, &rr->srv.priority, NULL); + if (r < 0) + return r; + r = dns_packet_read_uint16(p, &rr->srv.weight, NULL); + if (r < 0) + return r; + r = dns_packet_read_uint16(p, &rr->srv.port, NULL); + if (r < 0) + return r; + r = dns_packet_read_name(p, &rr->srv.name, true, NULL); + break; + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + r = dns_packet_read_name(p, &rr->ptr.name, true, NULL); + break; + + case DNS_TYPE_HINFO: + r = dns_packet_read_string(p, &rr->hinfo.cpu, NULL); + if (r < 0) + return r; + + r = dns_packet_read_string(p, &rr->hinfo.os, NULL); + break; + + case DNS_TYPE_SPF: /* exactly the same as TXT */ + case DNS_TYPE_TXT: + if (rdlength <= 0) { + r = dns_txt_item_new_empty(&rr->txt.items); + if (r < 0) + return r; + } else { + DnsTxtItem *last = NULL; + + while (p->rindex - offset < rdlength) { + DnsTxtItem *i; + const void *data; + size_t sz; + + r = dns_packet_read_raw_string(p, &data, &sz, NULL); + if (r < 0) + return r; + + i = malloc0(offsetof(DnsTxtItem, data) + sz + 1); /* extra NUL byte at the end */ + if (!i) + return -ENOMEM; + + memcpy(i->data, data, sz); + i->length = sz; + + LIST_INSERT_AFTER(items, rr->txt.items, last, i); + last = i; + } + } + + r = 0; + break; + + case DNS_TYPE_A: + r = dns_packet_read_blob(p, &rr->a.in_addr, sizeof(struct in_addr), NULL); + break; + + case DNS_TYPE_AAAA: + r = dns_packet_read_blob(p, &rr->aaaa.in6_addr, sizeof(struct in6_addr), NULL); + break; + + case DNS_TYPE_SOA: + r = dns_packet_read_name(p, &rr->soa.mname, true, NULL); + if (r < 0) + return r; + + r = dns_packet_read_name(p, &rr->soa.rname, true, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->soa.serial, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->soa.refresh, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->soa.retry, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->soa.expire, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->soa.minimum, NULL); + break; + + case DNS_TYPE_MX: + r = dns_packet_read_uint16(p, &rr->mx.priority, NULL); + if (r < 0) + return r; + + r = dns_packet_read_name(p, &rr->mx.exchange, true, NULL); + break; + + case DNS_TYPE_LOC: { + uint8_t t; + size_t pos; + + r = dns_packet_read_uint8(p, &t, &pos); + if (r < 0) + return r; + + if (t == 0) { + rr->loc.version = t; + + r = dns_packet_read_uint8(p, &rr->loc.size, NULL); + if (r < 0) + return r; + + if (!loc_size_ok(rr->loc.size)) + return -EBADMSG; + + r = dns_packet_read_uint8(p, &rr->loc.horiz_pre, NULL); + if (r < 0) + return r; + + if (!loc_size_ok(rr->loc.horiz_pre)) + return -EBADMSG; + + r = dns_packet_read_uint8(p, &rr->loc.vert_pre, NULL); + if (r < 0) + return r; + + if (!loc_size_ok(rr->loc.vert_pre)) + return -EBADMSG; + + r = dns_packet_read_uint32(p, &rr->loc.latitude, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->loc.longitude, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->loc.altitude, NULL); + if (r < 0) + return r; + + break; + } else { + dns_packet_rewind(p, pos); + rr->unparsable = true; + goto unparsable; + } + } + + case DNS_TYPE_DS: + r = dns_packet_read_uint16(p, &rr->ds.key_tag, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->ds.algorithm, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->ds.digest_type, NULL); + if (r < 0) + return r; + + if (rdlength < 4) + return -EBADMSG; + + r = dns_packet_read_memdup(p, rdlength - 4, + &rr->ds.digest, &rr->ds.digest_size, + NULL); + if (r < 0) + return r; + + if (rr->ds.digest_size <= 0) + /* the accepted size depends on the algorithm, but for now + just ensure that the value is greater than zero */ + return -EBADMSG; + + break; + + case DNS_TYPE_SSHFP: + r = dns_packet_read_uint8(p, &rr->sshfp.algorithm, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->sshfp.fptype, NULL); + if (r < 0) + return r; + + if (rdlength < 2) + return -EBADMSG; + + r = dns_packet_read_memdup(p, rdlength - 2, + &rr->sshfp.fingerprint, &rr->sshfp.fingerprint_size, + NULL); + + if (rr->sshfp.fingerprint_size <= 0) + /* the accepted size depends on the algorithm, but for now + just ensure that the value is greater than zero */ + return -EBADMSG; + + break; + + case DNS_TYPE_DNSKEY: + r = dns_packet_read_uint16(p, &rr->dnskey.flags, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->dnskey.protocol, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->dnskey.algorithm, NULL); + if (r < 0) + return r; + + if (rdlength < 4) + return -EBADMSG; + + r = dns_packet_read_memdup(p, rdlength - 4, + &rr->dnskey.key, &rr->dnskey.key_size, + NULL); + + if (rr->dnskey.key_size <= 0) + /* the accepted size depends on the algorithm, but for now + just ensure that the value is greater than zero */ + return -EBADMSG; + + break; + + case DNS_TYPE_RRSIG: + r = dns_packet_read_uint16(p, &rr->rrsig.type_covered, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->rrsig.algorithm, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->rrsig.labels, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->rrsig.original_ttl, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->rrsig.expiration, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &rr->rrsig.inception, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint16(p, &rr->rrsig.key_tag, NULL); + if (r < 0) + return r; + + r = dns_packet_read_name(p, &rr->rrsig.signer, false, NULL); + if (r < 0) + return r; + + if (rdlength < p->rindex - offset) + return -EBADMSG; + + r = dns_packet_read_memdup(p, offset + rdlength - p->rindex, + &rr->rrsig.signature, &rr->rrsig.signature_size, + NULL); + + if (rr->rrsig.signature_size <= 0) + /* the accepted size depends on the algorithm, but for now + just ensure that the value is greater than zero */ + return -EBADMSG; + + break; + + case DNS_TYPE_NSEC: { + + /* + * RFC6762, section 18.14 explicitly states mDNS should use name compression. + * This contradicts RFC3845, section 2.1.1 + */ + + bool allow_compressed = p->protocol == DNS_PROTOCOL_MDNS; + + r = dns_packet_read_name(p, &rr->nsec.next_domain_name, allow_compressed, NULL); + if (r < 0) + return r; + + if (rdlength < p->rindex - offset) + return -EBADMSG; + + r = dns_packet_read_type_windows(p, &rr->nsec.types, offset + rdlength - p->rindex, NULL); + + /* We accept empty NSEC bitmaps. The bit indicating the presence of the NSEC record itself + * is redundant and in e.g., RFC4956 this fact is used to define a use for NSEC records + * without the NSEC bit set. */ + + break; + } + case DNS_TYPE_NSEC3: { + uint8_t size; + + r = dns_packet_read_uint8(p, &rr->nsec3.algorithm, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->nsec3.flags, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint16(p, &rr->nsec3.iterations, NULL); + if (r < 0) + return r; + + /* this may be zero */ + r = dns_packet_read_uint8(p, &size, NULL); + if (r < 0) + return r; + + r = dns_packet_read_memdup(p, size, &rr->nsec3.salt, &rr->nsec3.salt_size, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &size, NULL); + if (r < 0) + return r; + + if (size <= 0) + return -EBADMSG; + + r = dns_packet_read_memdup(p, size, + &rr->nsec3.next_hashed_name, &rr->nsec3.next_hashed_name_size, + NULL); + if (r < 0) + return r; + + if (rdlength < p->rindex - offset) + return -EBADMSG; + + r = dns_packet_read_type_windows(p, &rr->nsec3.types, offset + rdlength - p->rindex, NULL); + + /* empty non-terminals can have NSEC3 records, so empty bitmaps are allowed */ + + break; + } + + case DNS_TYPE_TLSA: + r = dns_packet_read_uint8(p, &rr->tlsa.cert_usage, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->tlsa.selector, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint8(p, &rr->tlsa.matching_type, NULL); + if (r < 0) + return r; + + if (rdlength < 3) + return -EBADMSG; + + r = dns_packet_read_memdup(p, rdlength - 3, + &rr->tlsa.data, &rr->tlsa.data_size, + NULL); + + if (rr->tlsa.data_size <= 0) + /* the accepted size depends on the algorithm, but for now + just ensure that the value is greater than zero */ + return -EBADMSG; + + break; + + case DNS_TYPE_CAA: + r = dns_packet_read_uint8(p, &rr->caa.flags, NULL); + if (r < 0) + return r; + + r = dns_packet_read_string(p, &rr->caa.tag, NULL); + if (r < 0) + return r; + + if (rdlength < p->rindex - offset) + return -EBADMSG; + + r = dns_packet_read_memdup(p, + rdlength + offset - p->rindex, + &rr->caa.value, &rr->caa.value_size, NULL); + + break; + + case DNS_TYPE_OPT: /* we only care about the header of OPT for now. */ + case DNS_TYPE_OPENPGPKEY: + default: + unparsable: + r = dns_packet_read_memdup(p, rdlength, &rr->generic.data, &rr->generic.data_size, NULL); + + break; + } + if (r < 0) + return r; + if (p->rindex - offset != rdlength) + return -EBADMSG; + + if (ret) + *ret = TAKE_PTR(rr); + if (ret_cache_flush) + *ret_cache_flush = cache_flush; + if (ret_start) + *ret_start = rewinder.saved_rindex; + + CANCEL_REWINDER(rewinder); + return 0; +} + +static bool opt_is_good(DnsResourceRecord *rr, bool *rfc6975) { + const uint8_t* p; + bool found_dau_dhu_n3u = false; + size_t l; + + /* Checks whether the specified OPT RR is well-formed and whether it contains RFC6975 data (which is not OK in + * a reply). */ + + assert(rr); + assert(rr->key->type == DNS_TYPE_OPT); + + /* Check that the version is 0 */ + if (((rr->ttl >> 16) & UINT32_C(0xFF)) != 0) { + *rfc6975 = false; + return true; /* if it's not version 0, it's OK, but we will ignore the OPT field contents */ + } + + p = rr->opt.data; + l = rr->opt.data_size; + while (l > 0) { + uint16_t option_code, option_length; + + /* At least four bytes for OPTION-CODE and OPTION-LENGTH are required */ + if (l < 4U) + return false; + + option_code = unaligned_read_be16(p); + option_length = unaligned_read_be16(p + 2); + + if (l < option_length + 4U) + return false; + + /* RFC 6975 DAU, DHU or N3U fields found. */ + if (IN_SET(option_code, 5, 6, 7)) + found_dau_dhu_n3u = true; + + p += option_length + 4U; + l -= option_length + 4U; + } + + *rfc6975 = found_dau_dhu_n3u; + return true; +} + +static int dns_packet_extract_question(DnsPacket *p, DnsQuestion **ret_question) { + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + unsigned n; + int r; + + n = DNS_PACKET_QDCOUNT(p); + if (n > 0) { + question = dns_question_new(n); + if (!question) + return -ENOMEM; + + _cleanup_set_free_ Set *keys = NULL; /* references to keys are kept by Question */ + + keys = set_new(&dns_resource_key_hash_ops); + if (!keys) + return log_oom(); + + r = set_reserve(keys, n * 2); /* Higher multipliers give slightly higher efficiency through + * hash collisions, but the gains quickly drop off after 2. */ + if (r < 0) + return r; + + for (unsigned i = 0; i < n; i++) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + bool qu; + + r = dns_packet_read_key(p, &key, &qu, NULL); + if (r < 0) + return r; + + if (!dns_type_is_valid_query(key->type)) + return -EBADMSG; + + r = set_put(keys, key); + if (r < 0) + return r; + if (r == 0) + /* Already in the Question, let's skip */ + continue; + + r = dns_question_add_raw(question, key, qu ? DNS_QUESTION_WANTS_UNICAST_REPLY : 0); + if (r < 0) + return r; + } + } + + *ret_question = TAKE_PTR(question); + + return 0; +} + +static int dns_packet_extract_answer(DnsPacket *p, DnsAnswer **ret_answer) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + unsigned n; + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *previous = NULL; + bool bad_opt = false; + int r; + + n = DNS_PACKET_RRCOUNT(p); + if (n == 0) + return 0; + + answer = dns_answer_new(n); + if (!answer) + return -ENOMEM; + + for (unsigned i = 0; i < n; i++) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + bool cache_flush = false; + size_t start; + + if (p->rindex == p->size && p->opt) { + /* If we reached the end of the packet already, but there are still more RRs + * declared, then that's a corrupt packet. Let's accept the packet anyway, since it's + * apparently a common bug in routers. Let's however suppress OPT support in this + * case, so that we force the rest of the logic into lowest DNS baseline support. Or + * to say this differently: if the DNS server doesn't even get the RR counts right, + * it's highly unlikely it gets EDNS right. */ + log_debug("More resource records declared in packet than included, suppressing OPT."); + bad_opt = true; + break; + } + + r = dns_packet_read_rr(p, &rr, &cache_flush, &start); + if (r < 0) + return r; + + /* Try to reduce memory usage a bit */ + if (previous) + dns_resource_key_reduce(&rr->key, &previous->key); + + if (rr->key->type == DNS_TYPE_OPT) { + bool has_rfc6975; + + if (p->opt || bad_opt) { + /* Multiple OPT RRs? if so, let's ignore all, because there's + * something wrong with the server, and if one is valid we wouldn't + * know which one. */ + log_debug("Multiple OPT RRs detected, ignoring all."); + bad_opt = true; + continue; + } + + if (!dns_name_is_root(dns_resource_key_name(rr->key))) { + /* If the OPT RR is not owned by the root domain, then it is bad, + * let's ignore it. */ + log_debug("OPT RR is not owned by root domain, ignoring."); + bad_opt = true; + continue; + } + + if (i < DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)) { + /* OPT RR is in the wrong section? Some Belkin routers do this. This + * is a hint the EDNS implementation is borked, like the Belkin one + * is, hence ignore it. */ + log_debug("OPT RR in wrong section, ignoring."); + bad_opt = true; + continue; + } + + if (!opt_is_good(rr, &has_rfc6975)) { + log_debug("Malformed OPT RR, ignoring."); + bad_opt = true; + continue; + } + + if (DNS_PACKET_QR(p)) { + /* Additional checks for responses */ + + if (!DNS_RESOURCE_RECORD_OPT_VERSION_SUPPORTED(rr)) + /* If this is a reply and we don't know the EDNS version + * then something is weird... */ + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "EDNS version newer that our request, bad server."); + + if (has_rfc6975) { + /* If the OPT RR contains RFC6975 algorithm data, then this + * is indication that the server just copied the OPT it got + * from us (which contained that data) back into the reply. + * If so, then it doesn't properly support EDNS, as RFC6975 + * makes it very clear that the algorithm data should only + * be contained in questions, never in replies. Crappy + * Belkin routers copy the OPT data for example, hence let's + * detect this so that we downgrade early. */ + log_debug("OPT RR contains RFC6975 data, ignoring."); + bad_opt = true; + continue; + } + } + + p->opt = dns_resource_record_ref(rr); + p->opt_start = start; + assert(p->rindex >= start); + p->opt_size = p->rindex - start; + } else { + DnsAnswerFlags flags = 0; + + if (p->protocol == DNS_PROTOCOL_MDNS && !cache_flush) + flags |= DNS_ANSWER_SHARED_OWNER; + + /* According to RFC 4795, section 2.9. only the RRs from the Answer section shall be + * cached. Hence mark only those RRs as cacheable by default, but not the ones from + * the Additional or Authority sections. + * This restriction does not apply to mDNS records (RFC 6762). */ + if (i < DNS_PACKET_ANCOUNT(p)) + flags |= DNS_ANSWER_CACHEABLE|DNS_ANSWER_SECTION_ANSWER; + else if (i < DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)) + flags |= DNS_ANSWER_SECTION_AUTHORITY; + else { + flags |= DNS_ANSWER_SECTION_ADDITIONAL; + if (p->protocol == DNS_PROTOCOL_MDNS) + flags |= DNS_ANSWER_CACHEABLE; + } + + r = dns_answer_add(answer, rr, p->ifindex, flags, NULL); + if (r < 0) + return r; + } + + /* Remember this RR, so that we can potentially merge its ->key object with the + * next RR. Note that we only do this if we actually decided to keep the RR around. + */ + DNS_RR_REPLACE(previous, dns_resource_record_ref(rr)); + } + + if (bad_opt) { + p->opt = dns_resource_record_unref(p->opt); + p->opt_start = p->opt_size = SIZE_MAX; + } + + *ret_answer = TAKE_PTR(answer); + + return 0; +} + +int dns_packet_extract(DnsPacket *p) { + assert(p); + + if (p->extracted) + return 0; + + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + _unused_ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + int r; + + dns_packet_rewind(p, DNS_PACKET_HEADER_SIZE); + + r = dns_packet_extract_question(p, &question); + if (r < 0) + return r; + + r = dns_packet_extract_answer(p, &answer); + if (r < 0) + return r; + + if (p->rindex < p->size) { + log_debug("Trailing garbage in packet, suppressing OPT."); + p->opt = dns_resource_record_unref(p->opt); + p->opt_start = p->opt_size = SIZE_MAX; + } + + p->question = TAKE_PTR(question); + p->answer = TAKE_PTR(answer); + p->extracted = true; + + /* no CANCEL, always rewind */ + return 0; +} + +int dns_packet_is_reply_for(DnsPacket *p, const DnsResourceKey *key) { + int r; + + assert(p); + assert(key); + + /* Checks if the specified packet is a reply for the specified + * key and the specified key is the only one in the question + * section. */ + + if (DNS_PACKET_QR(p) != 1) + return 0; + + /* Let's unpack the packet, if that hasn't happened yet. */ + r = dns_packet_extract(p); + if (r < 0) + return r; + + if (!p->question) + return 0; + + if (p->question->n_keys != 1) + return 0; + + return dns_resource_key_equal(dns_question_first_key(p->question), key); +} + +int dns_packet_patch_max_udp_size(DnsPacket *p, uint16_t max_udp_size) { + assert(p); + assert(max_udp_size >= DNS_PACKET_UNICAST_SIZE_MAX); + + if (p->opt_start == SIZE_MAX) /* No OPT section, nothing to patch */ + return 0; + + assert(p->opt_size != SIZE_MAX); + assert(p->opt_size >= 5); + + unaligned_write_be16(DNS_PACKET_DATA(p) + p->opt_start + 3, max_udp_size); + return 1; +} + +static int patch_rr(DnsPacket *p, usec_t age) { + _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + size_t ttl_index; + uint32_t ttl; + uint16_t type, rdlength; + int r; + + /* Patches the RR at the current rindex, subtracts the specified time from the TTL */ + + r = dns_packet_read_name(p, NULL, true, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint16(p, &type, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint16(p, NULL, NULL); + if (r < 0) + return r; + + r = dns_packet_read_uint32(p, &ttl, &ttl_index); + if (r < 0) + return r; + + if (type != DNS_TYPE_OPT) { /* The TTL of the OPT field is not actually a TTL, skip it */ + ttl = LESS_BY(ttl * USEC_PER_SEC, age) / USEC_PER_SEC; + unaligned_write_be32(DNS_PACKET_DATA(p) + ttl_index, ttl); + } + + r = dns_packet_read_uint16(p, &rdlength, NULL); + if (r < 0) + return r; + + r = dns_packet_read(p, rdlength, NULL, NULL); + if (r < 0) + return r; + + CANCEL_REWINDER(rewinder); + return 0; +} + +int dns_packet_patch_ttls(DnsPacket *p, usec_t timestamp) { + assert(p); + assert(timestamp_is_set(timestamp)); + + /* Adjusts all TTLs in the packet by subtracting the time difference between now and the specified timestamp */ + + _unused_ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = REWINDER_INIT(p); + unsigned n; + usec_t k; + int r; + + k = now(CLOCK_BOOTTIME); + assert(k >= timestamp); + k -= timestamp; + + dns_packet_rewind(p, DNS_PACKET_HEADER_SIZE); + + n = DNS_PACKET_QDCOUNT(p); + for (unsigned i = 0; i < n; i++) { + r = dns_packet_read_key(p, NULL, NULL, NULL); + if (r < 0) + return r; + } + + n = DNS_PACKET_RRCOUNT(p); + for (unsigned i = 0; i < n; i++) { + + /* DNS servers suck, hence the RR count is in many servers off. If we reached the end + * prematurely, accept that, exit early */ + if (p->rindex == p->size) + break; + + r = patch_rr(p, k); + if (r < 0) + return r; + } + + return 0; +} + +static void dns_packet_hash_func(const DnsPacket *s, struct siphash *state) { + assert(s); + + siphash24_compress(&s->size, sizeof(s->size), state); + siphash24_compress(DNS_PACKET_DATA((DnsPacket*) s), s->size, state); +} + +static int dns_packet_compare_func(const DnsPacket *x, const DnsPacket *y) { + int r; + + r = CMP(x->size, y->size); + if (r != 0) + return r; + + return memcmp(DNS_PACKET_DATA((DnsPacket*) x), DNS_PACKET_DATA((DnsPacket*) y), x->size); +} + +DEFINE_HASH_OPS(dns_packet_hash_ops, DnsPacket, dns_packet_hash_func, dns_packet_compare_func); + +bool dns_packet_equal(const DnsPacket *a, const DnsPacket *b) { + return dns_packet_compare_func(a, b) == 0; +} + +int dns_packet_has_nsid_request(DnsPacket *p) { + bool has_nsid = false; + const uint8_t *d; + size_t l; + + assert(p); + + if (!p->opt) + return false; + + d = p->opt->opt.data; + l = p->opt->opt.data_size; + + while (l > 0) { + uint16_t code, length; + + if (l < 4U) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "EDNS0 variable part has invalid size."); + + code = unaligned_read_be16(d); + length = unaligned_read_be16(d + 2); + + if (l < 4U + length) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Truncated option in EDNS0 variable part."); + + if (code == 3) { + if (has_nsid) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Duplicate NSID option in EDNS0 variable part."); + + if (length != 0) + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Non-empty NSID option in DNS request."); + + has_nsid = true; + } + + d += 4U + length; + l -= 4U + length; + } + + return has_nsid; +} + +size_t dns_packet_size_unfragmented(DnsPacket *p) { + assert(p); + + if (p->fragsize == 0) /* Wasn't fragmented */ + return p->size; + + /* The fragment size (p->fragsize) covers the whole (fragmented) IP packet, while the regular packet + * size (p->size) only covers the DNS part. Thus, subtract the UDP header from the largest fragment + * size, in order to determine which size of DNS packet would have gone through without + * fragmenting. */ + + return LESS_BY(p->fragsize, udp_header_size(p->family)); +} + +static const char* const dns_rcode_table[_DNS_RCODE_MAX_DEFINED] = { + [DNS_RCODE_SUCCESS] = "SUCCESS", + [DNS_RCODE_FORMERR] = "FORMERR", + [DNS_RCODE_SERVFAIL] = "SERVFAIL", + [DNS_RCODE_NXDOMAIN] = "NXDOMAIN", + [DNS_RCODE_NOTIMP] = "NOTIMP", + [DNS_RCODE_REFUSED] = "REFUSED", + [DNS_RCODE_YXDOMAIN] = "YXDOMAIN", + [DNS_RCODE_YXRRSET] = "YRRSET", + [DNS_RCODE_NXRRSET] = "NXRRSET", + [DNS_RCODE_NOTAUTH] = "NOTAUTH", + [DNS_RCODE_NOTZONE] = "NOTZONE", + [DNS_RCODE_BADVERS] = "BADVERS", + [DNS_RCODE_BADKEY] = "BADKEY", + [DNS_RCODE_BADTIME] = "BADTIME", + [DNS_RCODE_BADMODE] = "BADMODE", + [DNS_RCODE_BADNAME] = "BADNAME", + [DNS_RCODE_BADALG] = "BADALG", + [DNS_RCODE_BADTRUNC] = "BADTRUNC", + [DNS_RCODE_BADCOOKIE] = "BADCOOKIE", +}; +DEFINE_STRING_TABLE_LOOKUP(dns_rcode, int); + +const char *format_dns_rcode(int i, char buf[static DECIMAL_STR_MAX(int)]) { + const char *p = dns_rcode_to_string(i); + if (p) + return p; + + return snprintf_ok(buf, DECIMAL_STR_MAX(int), "%i", i); +} + +static const char* const dns_protocol_table[_DNS_PROTOCOL_MAX] = { + [DNS_PROTOCOL_DNS] = "dns", + [DNS_PROTOCOL_MDNS] = "mdns", + [DNS_PROTOCOL_LLMNR] = "llmnr", +}; +DEFINE_STRING_TABLE_LOOKUP(dns_protocol, DnsProtocol); diff --git a/src/resolve/resolved-dns-packet.h b/src/resolve/resolved-dns-packet.h new file mode 100644 index 0000000..e4d3e94 --- /dev/null +++ b/src/resolve/resolved-dns-packet.h @@ -0,0 +1,349 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <netinet/ip.h> +#include <netinet/ip6.h> +#include <netinet/udp.h> + +#include "hashmap.h" +#include "in-addr-util.h" +#include "macro.h" +#include "sparse-endian.h" + +typedef struct DnsPacketHeader DnsPacketHeader; +typedef struct DnsPacket DnsPacket; + +#include "resolved-def.h" +#include "resolved-dns-answer.h" +#include "resolved-dns-question.h" +#include "resolved-dns-rr.h" + +typedef enum DnsProtocol { + DNS_PROTOCOL_DNS, + DNS_PROTOCOL_MDNS, + DNS_PROTOCOL_LLMNR, + _DNS_PROTOCOL_MAX, + _DNS_PROTOCOL_INVALID = -EINVAL, +} DnsProtocol; + +struct DnsPacketHeader { + uint16_t id; + be16_t flags; + be16_t qdcount; + be16_t ancount; + be16_t nscount; + be16_t arcount; +} _packed_; + +#define DNS_PACKET_HEADER_SIZE sizeof(DnsPacketHeader) +#define UDP4_PACKET_HEADER_SIZE (sizeof(struct iphdr) + sizeof(struct udphdr)) +#define UDP6_PACKET_HEADER_SIZE (sizeof(struct ip6_hdr) + sizeof(struct udphdr)) + +assert_cc(sizeof(struct ip6_hdr) == 40); +assert_cc(sizeof(struct iphdr) == 20); +assert_cc(sizeof(struct udphdr) == 8); +assert_cc(sizeof(DnsPacketHeader) == 12); + +/* The various DNS protocols deviate in how large a packet can grow, but the TCP transport has a 16bit size + * field, hence that appears to be the absolute maximum. */ +#define DNS_PACKET_SIZE_MAX 0xFFFFu + +/* The default size to use for allocation when we don't know how large + * the packet will turn out to be. */ +#define DNS_PACKET_SIZE_START 512u + +/* RFC 1035 say 512 is the maximum, for classic unicast DNS */ +#define DNS_PACKET_UNICAST_SIZE_MAX 512u + +/* With EDNS0 we can use larger packets, default to 1232, which is what is commonly used */ +#define DNS_PACKET_UNICAST_SIZE_LARGE_MAX 1232u + +struct DnsPacket { + unsigned n_ref; + DnsProtocol protocol; + size_t size, allocated, rindex, max_size, fragsize; + void *_data; /* don't access directly, use DNS_PACKET_DATA()! */ + Hashmap *names; /* For name compression */ + size_t opt_start, opt_size; + + /* Parsed data */ + DnsQuestion *question; + DnsAnswer *answer; + DnsResourceRecord *opt; + + /* For support of truncated packets */ + DnsPacket *more; + + /* Packet reception metadata */ + usec_t timestamp; /* CLOCK_BOOTTIME (or CLOCK_MONOTONIC if the former doesn't exist) */ + int ifindex; + int family, ipproto; + union in_addr_union sender, destination; + uint16_t sender_port, destination_port; + uint32_t ttl; + + bool on_stack; + bool extracted; + bool refuse_compression; + bool canonical_form; + + /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */ +}; + +static inline uint8_t* DNS_PACKET_DATA(const DnsPacket *p) { + if (_unlikely_(!p)) + return NULL; + + if (p->_data) + return p->_data; + + return ((uint8_t*) p) + ALIGN(sizeof(DnsPacket)); +} + +#define DNS_PACKET_HEADER(p) ((DnsPacketHeader*) DNS_PACKET_DATA(p)) +#define DNS_PACKET_ID(p) DNS_PACKET_HEADER(p)->id +#define DNS_PACKET_QR(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 15) & 1) +#define DNS_PACKET_OPCODE(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 11) & 15) +#define DNS_PACKET_AA(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 10) & 1) +#define DNS_PACKET_TC(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 9) & 1) +#define DNS_PACKET_RD(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 8) & 1) +#define DNS_PACKET_RA(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 7) & 1) +#define DNS_PACKET_AD(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 5) & 1) +#define DNS_PACKET_CD(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 4) & 1) + +#define DNS_PACKET_FLAG_TC (UINT16_C(1) << 9) + +static inline uint16_t DNS_PACKET_RCODE(DnsPacket *p) { + uint16_t rcode; + + if (p->opt) + rcode = (uint16_t) (p->opt->ttl >> 24); + else + rcode = 0; + + return rcode | (be16toh(DNS_PACKET_HEADER(p)->flags) & 0xF); +} + +static inline uint16_t DNS_PACKET_PAYLOAD_SIZE_MAX(DnsPacket *p) { + + /* Returns the advertised maximum size for replies, or the DNS default if there's nothing defined. */ + + if (p->ipproto == IPPROTO_TCP) /* we ignore EDNS(0) size data on TCP, like everybody else */ + return DNS_PACKET_SIZE_MAX; + + if (p->opt) + return MAX(DNS_PACKET_UNICAST_SIZE_MAX, p->opt->key->class); + + return DNS_PACKET_UNICAST_SIZE_MAX; +} + +static inline bool DNS_PACKET_DO(DnsPacket *p) { + if (!p->opt) + return false; + + return !!(p->opt->ttl & (1U << 15)); +} + +static inline bool DNS_PACKET_VERSION_SUPPORTED(DnsPacket *p) { + /* Returns true if this packet is in a version we support. Which means either non-EDNS or EDNS(0), but not EDNS + * of any newer versions */ + + if (!p->opt) + return true; + + return DNS_RESOURCE_RECORD_OPT_VERSION_SUPPORTED(p->opt); +} + +static inline bool DNS_PACKET_IS_FRAGMENTED(DnsPacket *p) { + assert(p); + + /* For ingress packets: was this packet fragmented according to our knowledge? */ + + return p->fragsize != 0; +} + +/* LLMNR defines some bits differently */ +#define DNS_PACKET_LLMNR_C(p) DNS_PACKET_AA(p) +#define DNS_PACKET_LLMNR_T(p) DNS_PACKET_RD(p) + +#define DNS_PACKET_QDCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->qdcount) +#define DNS_PACKET_ANCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->ancount) +#define DNS_PACKET_NSCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->nscount) +#define DNS_PACKET_ARCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->arcount) + +#define DNS_PACKET_MAKE_FLAGS(qr, opcode, aa, tc, rd, ra, ad, cd, rcode) \ + (((uint16_t) !!(qr) << 15) | \ + ((uint16_t) ((opcode) & 15) << 11) | \ + ((uint16_t) !!(aa) << 10) | /* on LLMNR: c */ \ + ((uint16_t) !!(tc) << 9) | \ + ((uint16_t) !!(rd) << 8) | /* on LLMNR: t */ \ + ((uint16_t) !!(ra) << 7) | \ + ((uint16_t) !!(ad) << 5) | \ + ((uint16_t) !!(cd) << 4) | \ + ((uint16_t) ((rcode) & 15))) + +static inline unsigned DNS_PACKET_RRCOUNT(DnsPacket *p) { + return + (unsigned) DNS_PACKET_ANCOUNT(p) + + (unsigned) DNS_PACKET_NSCOUNT(p) + + (unsigned) DNS_PACKET_ARCOUNT(p); +} + +int dns_packet_new(DnsPacket **p, DnsProtocol protocol, size_t min_alloc_dsize, size_t max_size); +int dns_packet_new_query(DnsPacket **p, DnsProtocol protocol, size_t min_alloc_dsize, bool dnssec_checking_disabled); + +int dns_packet_dup(DnsPacket **ret, DnsPacket *p); + +void dns_packet_set_flags(DnsPacket *p, bool dnssec_checking_disabled, bool truncated); + +DnsPacket *dns_packet_ref(DnsPacket *p); +DnsPacket *dns_packet_unref(DnsPacket *p); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsPacket*, dns_packet_unref); + +#define DNS_PACKET_REPLACE(a, b) \ + do { \ + typeof(a)* _a = &(a); \ + typeof(b) _b = (b); \ + dns_packet_unref(*_a); \ + *_a = _b; \ + } while(0) + +int dns_packet_validate(DnsPacket *p); +int dns_packet_validate_reply(DnsPacket *p); +int dns_packet_validate_query(DnsPacket *p); + +int dns_packet_is_reply_for(DnsPacket *p, const DnsResourceKey *key); + +int dns_packet_append_blob(DnsPacket *p, const void *d, size_t sz, size_t *start); +int dns_packet_append_uint8(DnsPacket *p, uint8_t v, size_t *start); +int dns_packet_append_uint16(DnsPacket *p, uint16_t v, size_t *start); +int dns_packet_append_uint32(DnsPacket *p, uint32_t v, size_t *start); +int dns_packet_append_string(DnsPacket *p, const char *s, size_t *start); +int dns_packet_append_raw_string(DnsPacket *p, const void *s, size_t size, size_t *start); +int dns_packet_append_label(DnsPacket *p, const char *s, size_t l, bool canonical_candidate, size_t *start); +int dns_packet_append_name(DnsPacket *p, const char *name, bool allow_compression, bool canonical_candidate, size_t *start); +int dns_packet_append_key(DnsPacket *p, const DnsResourceKey *key, const DnsAnswerFlags flags, size_t *start); +int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, const DnsAnswerFlags flags, size_t *start, size_t *rdata_start); +int dns_packet_append_opt(DnsPacket *p, uint16_t max_udp_size, bool edns0_do, bool include_rfc6975, const char *nsid, int rcode, size_t *ret_start); +int dns_packet_append_question(DnsPacket *p, DnsQuestion *q); +int dns_packet_append_answer(DnsPacket *p, DnsAnswer *a, unsigned *completed); + +int dns_packet_patch_max_udp_size(DnsPacket *p, uint16_t max_udp_size); +int dns_packet_patch_ttls(DnsPacket *p, usec_t timestamp); + +void dns_packet_truncate(DnsPacket *p, size_t sz); +int dns_packet_truncate_opt(DnsPacket *p); + +int dns_packet_read(DnsPacket *p, size_t sz, const void **ret, size_t *start); +int dns_packet_read_blob(DnsPacket *p, void *d, size_t sz, size_t *start); +int dns_packet_read_uint8(DnsPacket *p, uint8_t *ret, size_t *start); +int dns_packet_read_uint16(DnsPacket *p, uint16_t *ret, size_t *start); +int dns_packet_read_uint32(DnsPacket *p, uint32_t *ret, size_t *start); +int dns_packet_read_string(DnsPacket *p, char **ret, size_t *start); +int dns_packet_read_raw_string(DnsPacket *p, const void **ret, size_t *size, size_t *start); +int dns_packet_read_name(DnsPacket *p, char **ret, bool allow_compression, size_t *start); +int dns_packet_read_key(DnsPacket *p, DnsResourceKey **ret, bool *ret_cache_flush_or_qu, size_t *start); +int dns_packet_read_rr(DnsPacket *p, DnsResourceRecord **ret, bool *ret_cache_flush, size_t *start); + +void dns_packet_rewind(DnsPacket *p, size_t idx); + +int dns_packet_skip_question(DnsPacket *p); +int dns_packet_extract(DnsPacket *p); + +bool dns_packet_equal(const DnsPacket *a, const DnsPacket *b); + +int dns_packet_has_nsid_request(DnsPacket *p); + +/* https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 */ +enum { + DNS_RCODE_SUCCESS = 0, + DNS_RCODE_FORMERR = 1, + DNS_RCODE_SERVFAIL = 2, + DNS_RCODE_NXDOMAIN = 3, + DNS_RCODE_NOTIMP = 4, + DNS_RCODE_REFUSED = 5, + DNS_RCODE_YXDOMAIN = 6, + DNS_RCODE_YXRRSET = 7, + DNS_RCODE_NXRRSET = 8, + DNS_RCODE_NOTAUTH = 9, + DNS_RCODE_NOTZONE = 10, + DNS_RCODE_BADVERS = 16, + DNS_RCODE_BADSIG = 16, /* duplicate value! */ + DNS_RCODE_BADKEY = 17, + DNS_RCODE_BADTIME = 18, + DNS_RCODE_BADMODE = 19, + DNS_RCODE_BADNAME = 20, + DNS_RCODE_BADALG = 21, + DNS_RCODE_BADTRUNC = 22, + DNS_RCODE_BADCOOKIE = 23, + _DNS_RCODE_MAX_DEFINED, + _DNS_RCODE_MAX = 4095 /* 4 bit rcode in the header plus 8 bit rcode in OPT, makes 12 bit */ +}; + +const char* dns_rcode_to_string(int i) _const_; +int dns_rcode_from_string(const char *s) _pure_; +const char *format_dns_rcode(int i, char buf[static DECIMAL_STR_MAX(int)]); +#define FORMAT_DNS_RCODE(i) format_dns_rcode(i, (char [DECIMAL_STR_MAX(int)]) {}) + +const char* dns_protocol_to_string(DnsProtocol p) _const_; +DnsProtocol dns_protocol_from_string(const char *s) _pure_; + +#define LLMNR_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 252U) }) +#define LLMNR_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03 } }) + +#define MDNS_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 251U) }) +#define MDNS_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb } }) + +extern const struct hash_ops dns_packet_hash_ops; + +static inline uint64_t SD_RESOLVED_FLAGS_MAKE( + DnsProtocol protocol, + int family, + bool authenticated, + bool confidential) { + uint64_t f; + + /* Converts a protocol + family into a flags field as used in queries and responses */ + + f = (authenticated ? SD_RESOLVED_AUTHENTICATED : 0) | + (confidential ? SD_RESOLVED_CONFIDENTIAL : 0); + + switch (protocol) { + case DNS_PROTOCOL_DNS: + return f|SD_RESOLVED_DNS; + + case DNS_PROTOCOL_LLMNR: + return f|(family == AF_INET6 ? SD_RESOLVED_LLMNR_IPV6 : SD_RESOLVED_LLMNR_IPV4); + + case DNS_PROTOCOL_MDNS: + return f|(family == AF_INET6 ? SD_RESOLVED_MDNS_IPV6 : SD_RESOLVED_MDNS_IPV4); + + default: + return f; + } +} + +static inline size_t dns_packet_size_max(DnsPacket *p) { + assert(p); + + /* Why not insist on a fully initialized max_size during DnsPacket construction? Well, this way it's easy to + * allocate a transient, throw-away DnsPacket on the stack by simple zero initialization, without having to + * deal with explicit field initialization. */ + + return p->max_size != 0 ? p->max_size : DNS_PACKET_SIZE_MAX; +} + +static inline size_t udp_header_size(int af) { + + switch (af) { + case AF_INET: + return UDP4_PACKET_HEADER_SIZE; + case AF_INET6: + return UDP6_PACKET_HEADER_SIZE; + default: + assert_not_reached(); + } +} + +size_t dns_packet_size_unfragmented(DnsPacket *p); diff --git a/src/resolve/resolved-dns-query.c b/src/resolve/resolved-dns-query.c new file mode 100644 index 0000000..7eb6b97 --- /dev/null +++ b/src/resolve/resolved-dns-query.c @@ -0,0 +1,1299 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "dns-domain.h" +#include "dns-type.h" +#include "event-util.h" +#include "glyph-util.h" +#include "hostname-util.h" +#include "local-addresses.h" +#include "resolved-dns-query.h" +#include "resolved-dns-synthesize.h" +#include "resolved-etc-hosts.h" +#include "string-util.h" + +#define QUERIES_MAX 2048 +#define AUXILIARY_QUERIES_MAX 64 +#define CNAME_REDIRECTS_MAX 16 + +assert_cc(AUXILIARY_QUERIES_MAX < UINT8_MAX); +assert_cc(CNAME_REDIRECTS_MAX < UINT8_MAX); + +static int dns_query_candidate_new(DnsQueryCandidate **ret, DnsQuery *q, DnsScope *s) { + DnsQueryCandidate *c; + + assert(ret); + assert(q); + assert(s); + + c = new(DnsQueryCandidate, 1); + if (!c) + return -ENOMEM; + + *c = (DnsQueryCandidate) { + .n_ref = 1, + .query = q, + .scope = s, + }; + + LIST_PREPEND(candidates_by_query, q->candidates, c); + LIST_PREPEND(candidates_by_scope, s->query_candidates, c); + + *ret = c; + return 0; +} + +static void dns_query_candidate_stop(DnsQueryCandidate *c) { + DnsTransaction *t; + + assert(c); + + /* Detach all the DnsTransactions attached to this query */ + + while ((t = set_steal_first(c->transactions))) { + set_remove(t->notify_query_candidates, c); + set_remove(t->notify_query_candidates_done, c); + dns_transaction_gc(t); + } +} + +static DnsQueryCandidate* dns_query_candidate_unlink(DnsQueryCandidate *c) { + assert(c); + + /* Detach this DnsQueryCandidate from the Query and Scope objects */ + + if (c->query) { + LIST_REMOVE(candidates_by_query, c->query->candidates, c); + c->query = NULL; + } + + if (c->scope) { + LIST_REMOVE(candidates_by_scope, c->scope->query_candidates, c); + c->scope = NULL; + } + + return c; +} + +static DnsQueryCandidate* dns_query_candidate_free(DnsQueryCandidate *c) { + if (!c) + return NULL; + + dns_query_candidate_stop(c); + dns_query_candidate_unlink(c); + + set_free(c->transactions); + dns_search_domain_unref(c->search_domain); + + return mfree(c); +} + +DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(DnsQueryCandidate, dns_query_candidate, dns_query_candidate_free); + +static int dns_query_candidate_next_search_domain(DnsQueryCandidate *c) { + DnsSearchDomain *next; + + assert(c); + + if (c->search_domain && c->search_domain->linked) + next = c->search_domain->domains_next; + else + next = dns_scope_get_search_domains(c->scope); + + for (;;) { + if (!next) /* We hit the end of the list */ + return 0; + + if (!next->route_only) + break; + + /* Skip over route-only domains */ + next = next->domains_next; + } + + dns_search_domain_unref(c->search_domain); + c->search_domain = dns_search_domain_ref(next); + + return 1; +} + +static int dns_query_candidate_add_transaction( + DnsQueryCandidate *c, + DnsResourceKey *key, + DnsPacket *bypass) { + + _cleanup_(dns_transaction_gcp) DnsTransaction *t = NULL; + int r; + + assert(c); + assert(c->query); /* We shan't add transactions to a candidate that has been detached already */ + + if (key) { + /* Regular lookup with a resource key */ + assert(!bypass); + + t = dns_scope_find_transaction(c->scope, key, c->query->flags); + if (!t) { + r = dns_transaction_new(&t, c->scope, key, NULL, c->query->flags); + if (r < 0) + return r; + } else if (set_contains(c->transactions, t)) + return 0; + } else { + /* "Bypass" lookup with a query packet */ + assert(bypass); + + r = dns_transaction_new(&t, c->scope, NULL, bypass, c->query->flags); + if (r < 0) + return r; + } + + r = set_ensure_allocated(&t->notify_query_candidates_done, NULL); + if (r < 0) + return r; + + r = set_ensure_put(&t->notify_query_candidates, NULL, c); + if (r < 0) + return r; + + r = set_ensure_put(&c->transactions, NULL, t); + if (r < 0) { + (void) set_remove(t->notify_query_candidates, c); + return r; + } + + TAKE_PTR(t); + return 1; +} + +static int dns_query_candidate_go(DnsQueryCandidate *c) { + _unused_ _cleanup_(dns_query_candidate_unrefp) DnsQueryCandidate *keep_c = NULL; + DnsTransaction *t; + int r; + unsigned n = 0; + + assert(c); + + /* Let's keep a reference to the query while we're operating */ + keep_c = dns_query_candidate_ref(c); + + /* Start the transactions that are not started yet */ + SET_FOREACH(t, c->transactions) { + if (t->state != DNS_TRANSACTION_NULL) + continue; + + r = dns_transaction_go(t); + if (r < 0) + return r; + + n++; + } + + /* If there was nothing to start, then let's proceed immediately */ + if (n == 0) + dns_query_candidate_notify(c); + + return 0; +} + +static DnsTransactionState dns_query_candidate_state(DnsQueryCandidate *c) { + DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS; + DnsTransaction *t; + + assert(c); + + if (c->error_code != 0) + return DNS_TRANSACTION_ERRNO; + + SET_FOREACH(t, c->transactions) + + switch (t->state) { + + case DNS_TRANSACTION_NULL: + /* If there's a NULL transaction pending, then + * this means not all transactions where + * started yet, and we were called from within + * the stackframe that is supposed to start + * remaining transactions. In this case, + * simply claim the candidate is pending. */ + + case DNS_TRANSACTION_PENDING: + case DNS_TRANSACTION_VALIDATING: + /* If there's one transaction currently in + * VALIDATING state, then this means there's + * also one in PENDING state, hence we can + * return PENDING immediately. */ + return DNS_TRANSACTION_PENDING; + + case DNS_TRANSACTION_SUCCESS: + state = t->state; + break; + + default: + if (state != DNS_TRANSACTION_SUCCESS) + state = t->state; + + break; + } + + return state; +} + +static int dns_query_candidate_setup_transactions(DnsQueryCandidate *c) { + DnsQuestion *question; + DnsResourceKey *key; + int n = 0, r; + + assert(c); + assert(c->query); /* We shan't add transactions to a candidate that has been detached already */ + + dns_query_candidate_stop(c); + + if (c->query->question_bypass) { + /* If this is a bypass query, then pass the original query packet along to the transaction */ + + assert(dns_question_size(c->query->question_bypass->question) == 1); + + if (!dns_scope_good_key(c->scope, dns_question_first_key(c->query->question_bypass->question))) + return 0; + + r = dns_query_candidate_add_transaction(c, NULL, c->query->question_bypass); + if (r < 0) + goto fail; + + return 1; + } + + question = dns_query_question_for_protocol(c->query, c->scope->protocol); + + /* Create one transaction per question key */ + DNS_QUESTION_FOREACH(key, question) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *new_key = NULL; + DnsResourceKey *qkey; + + if (c->search_domain) { + r = dns_resource_key_new_append_suffix(&new_key, key, c->search_domain->name); + if (r < 0) + goto fail; + + qkey = new_key; + } else + qkey = key; + + if (!dns_scope_good_key(c->scope, qkey)) + continue; + + r = dns_query_candidate_add_transaction(c, qkey, NULL); + if (r < 0) + goto fail; + + n++; + } + + return n; + +fail: + dns_query_candidate_stop(c); + return r; +} + +void dns_query_candidate_notify(DnsQueryCandidate *c) { + DnsTransactionState state; + int r; + + assert(c); + + if (!c->query) /* This candidate has been abandoned, do nothing. */ + return; + + state = dns_query_candidate_state(c); + + if (DNS_TRANSACTION_IS_LIVE(state)) + return; + + if (state != DNS_TRANSACTION_SUCCESS && c->search_domain) { + + r = dns_query_candidate_next_search_domain(c); + if (r < 0) + goto fail; + + if (r > 0) { + /* OK, there's another search domain to try, let's do so. */ + + r = dns_query_candidate_setup_transactions(c); + if (r < 0) + goto fail; + + if (r > 0) { + /* New transactions where queued. Start them and wait */ + + r = dns_query_candidate_go(c); + if (r < 0) + goto fail; + + return; + } + } + + } + + dns_query_ready(c->query); + return; + +fail: + c->error_code = log_warning_errno(r, "Failed to follow search domains: %m"); + dns_query_ready(c->query); +} + +static void dns_query_stop(DnsQuery *q) { + assert(q); + + event_source_disable(q->timeout_event_source); + + LIST_FOREACH(candidates_by_query, c, q->candidates) + dns_query_candidate_stop(c); +} + +static void dns_query_unlink_candidates(DnsQuery *q) { + assert(q); + + while (q->candidates) + /* Here we drop *our* references to each of the candidates. If we had the only reference, the + * DnsQueryCandidate object will be freed. */ + dns_query_candidate_unref(dns_query_candidate_unlink(q->candidates)); +} + +static void dns_query_reset_answer(DnsQuery *q) { + assert(q); + + q->answer = dns_answer_unref(q->answer); + q->answer_rcode = 0; + q->answer_dnssec_result = _DNSSEC_RESULT_INVALID; + q->answer_errno = 0; + q->answer_query_flags = 0; + q->answer_protocol = _DNS_PROTOCOL_INVALID; + q->answer_family = AF_UNSPEC; + q->answer_search_domain = dns_search_domain_unref(q->answer_search_domain); + q->answer_full_packet = dns_packet_unref(q->answer_full_packet); +} + +DnsQuery *dns_query_free(DnsQuery *q) { + if (!q) + return NULL; + + q->timeout_event_source = sd_event_source_disable_unref(q->timeout_event_source); + + while (q->auxiliary_queries) + dns_query_free(q->auxiliary_queries); + + if (q->auxiliary_for) { + assert(q->auxiliary_for->n_auxiliary_queries > 0); + q->auxiliary_for->n_auxiliary_queries--; + LIST_REMOVE(auxiliary_queries, q->auxiliary_for->auxiliary_queries, q); + } + + dns_query_unlink_candidates(q); + + dns_question_unref(q->question_idna); + dns_question_unref(q->question_utf8); + dns_packet_unref(q->question_bypass); + dns_question_unref(q->collected_questions); + + dns_query_reset_answer(q); + + sd_bus_message_unref(q->bus_request); + sd_bus_track_unref(q->bus_track); + + if (q->varlink_request) { + varlink_set_userdata(q->varlink_request, NULL); + varlink_unref(q->varlink_request); + } + + if (q->request_packet) + hashmap_remove_value(q->stub_listener_extra ? + q->stub_listener_extra->queries_by_packet : + q->manager->stub_queries_by_packet, + q->request_packet, + q); + + dns_packet_unref(q->request_packet); + dns_answer_unref(q->reply_answer); + dns_answer_unref(q->reply_authoritative); + dns_answer_unref(q->reply_additional); + + if (q->request_stream) { + /* Detach the stream from our query, in case something else keeps a reference to it. */ + (void) set_remove(q->request_stream->queries, q); + q->request_stream = dns_stream_unref(q->request_stream); + } + + free(q->request_address_string); + + if (q->manager) { + LIST_REMOVE(queries, q->manager->dns_queries, q); + q->manager->n_dns_queries--; + } + + return mfree(q); +} + +int dns_query_new( + Manager *m, + DnsQuery **ret, + DnsQuestion *question_utf8, + DnsQuestion *question_idna, + DnsPacket *question_bypass, + int ifindex, + uint64_t flags) { + + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + DnsResourceKey *key; + int r; + + assert(m); + + if (question_bypass) { + /* It's either a "bypass" query, or a regular one, but can't be both. */ + if (question_utf8 || question_idna) + return -EINVAL; + + } else { + bool good = false; + + /* This (primarily) checks two things: + * + * 1. That the question is not empty + * 2. That all RR keys in the question objects are for the same domain + * + * Or in other words, a single DnsQuery object may be used to look up A+AAAA combination for + * the same domain name, or SRV+TXT (for DNS-SD services), but not for unrelated lookups. */ + + if (dns_question_size(question_utf8) > 0) { + r = dns_question_is_valid_for_query(question_utf8); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + good = true; + } + + /* If the IDNA and UTF8 questions are the same, merge their references */ + r = dns_question_is_equal(question_idna, question_utf8); + if (r < 0) + return r; + if (r > 0) + question_idna = question_utf8; + else { + if (dns_question_size(question_idna) > 0) { + r = dns_question_is_valid_for_query(question_idna); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + good = true; + } + } + + if (!good) /* don't allow empty queries */ + return -EINVAL; + } + + if (m->n_dns_queries >= QUERIES_MAX) + return -EBUSY; + + q = new(DnsQuery, 1); + if (!q) + return -ENOMEM; + + *q = (DnsQuery) { + .question_utf8 = dns_question_ref(question_utf8), + .question_idna = dns_question_ref(question_idna), + .question_bypass = dns_packet_ref(question_bypass), + .ifindex = ifindex, + .flags = flags, + .answer_dnssec_result = _DNSSEC_RESULT_INVALID, + .answer_protocol = _DNS_PROTOCOL_INVALID, + .answer_family = AF_UNSPEC, + }; + + if (question_bypass) { + DNS_QUESTION_FOREACH(key, question_bypass->question) + log_debug("Looking up bypass packet for %s.", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + } else { + /* First dump UTF8 question */ + DNS_QUESTION_FOREACH(key, question_utf8) + log_debug("Looking up RR for %s.", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + + /* And then dump the IDNA question, but only what hasn't been dumped already through the UTF8 question. */ + DNS_QUESTION_FOREACH(key, question_idna) { + r = dns_question_contains_key(question_utf8, key); + if (r < 0) + return r; + if (r > 0) + continue; + + log_debug("Looking up IDNA RR for %s.", + dns_resource_key_to_string(key, key_str, sizeof key_str)); + } + } + + LIST_PREPEND(queries, m->dns_queries, q); + m->n_dns_queries++; + q->manager = m; + + if (ret) + *ret = q; + + TAKE_PTR(q); + return 0; +} + +int dns_query_make_auxiliary(DnsQuery *q, DnsQuery *auxiliary_for) { + assert(q); + assert(auxiliary_for); + + /* Ensure that the query is not auxiliary yet, and + * nothing else is auxiliary to it either */ + assert(!q->auxiliary_for); + assert(!q->auxiliary_queries); + + /* Ensure that the unit we shall be made auxiliary for isn't + * auxiliary itself */ + assert(!auxiliary_for->auxiliary_for); + + if (auxiliary_for->n_auxiliary_queries >= AUXILIARY_QUERIES_MAX) + return -EAGAIN; + + LIST_PREPEND(auxiliary_queries, auxiliary_for->auxiliary_queries, q); + q->auxiliary_for = auxiliary_for; + + auxiliary_for->n_auxiliary_queries++; + return 0; +} + +void dns_query_complete(DnsQuery *q, DnsTransactionState state) { + assert(q); + assert(!DNS_TRANSACTION_IS_LIVE(state)); + assert(DNS_TRANSACTION_IS_LIVE(q->state)); + + /* Note that this call might invalidate the query. Callers should hence not attempt to access the + * query or transaction after calling this function. */ + + q->state = state; + + (void) manager_monitor_send(q->manager, q->state, q->answer_rcode, q->answer_errno, q->question_idna, q->question_utf8, q->question_bypass, q->collected_questions, q->answer); + + dns_query_stop(q); + if (q->complete) + q->complete(q); +} + +static int on_query_timeout(sd_event_source *s, usec_t usec, void *userdata) { + DnsQuery *q = ASSERT_PTR(userdata); + + assert(s); + + dns_query_complete(q, DNS_TRANSACTION_TIMEOUT); + return 0; +} + +static int dns_query_add_candidate(DnsQuery *q, DnsScope *s) { + _cleanup_(dns_query_candidate_unrefp) DnsQueryCandidate *c = NULL; + int r; + + assert(q); + assert(s); + + r = dns_query_candidate_new(&c, q, s); + if (r < 0) + return r; + + /* If this a single-label domain on DNS, we might append a suitable search domain first. */ + if (!FLAGS_SET(q->flags, SD_RESOLVED_NO_SEARCH) && + dns_scope_name_wants_search_domain(s, dns_question_first_name(q->question_idna))) { + /* OK, we want a search domain now. Let's find one for this scope */ + + r = dns_query_candidate_next_search_domain(c); + if (r < 0) + return r; + } + + r = dns_query_candidate_setup_transactions(c); + if (r < 0) + return r; + + TAKE_PTR(c); + return 0; +} + +static int dns_query_synthesize_reply(DnsQuery *q, DnsTransactionState *state) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + int r; + + assert(q); + assert(state); + + /* Tries to synthesize localhost RR replies (and others) where appropriate. Note that this is done *after* the + * the normal lookup finished. The data from the network hence takes precedence over the data we + * synthesize. (But note that many scopes refuse to resolve certain domain names) */ + + if (!IN_SET(*state, + DNS_TRANSACTION_RCODE_FAILURE, + DNS_TRANSACTION_NO_SERVERS, + DNS_TRANSACTION_TIMEOUT, + DNS_TRANSACTION_ATTEMPTS_MAX_REACHED, + DNS_TRANSACTION_NETWORK_DOWN, + DNS_TRANSACTION_NOT_FOUND)) + return 0; + + if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE)) + return 0; + + r = dns_synthesize_answer( + q->manager, + q->question_bypass ? q->question_bypass->question : q->question_utf8, + q->ifindex, + &answer); + if (r == -ENXIO) { + /* If we get ENXIO this tells us to generate NXDOMAIN unconditionally. */ + + dns_query_reset_answer(q); + q->answer_rcode = DNS_RCODE_NXDOMAIN; + q->answer_protocol = dns_synthesize_protocol(q->flags); + q->answer_family = dns_synthesize_family(q->flags); + q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC; + *state = DNS_TRANSACTION_RCODE_FAILURE; + + return 0; + } + if (r <= 0) + return r; + + dns_query_reset_answer(q); + + q->answer = TAKE_PTR(answer); + q->answer_rcode = DNS_RCODE_SUCCESS; + q->answer_protocol = dns_synthesize_protocol(q->flags); + q->answer_family = dns_synthesize_family(q->flags); + q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC; + + *state = DNS_TRANSACTION_SUCCESS; + + return 1; +} + +static int dns_query_try_etc_hosts(DnsQuery *q) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + int r; + + assert(q); + + /* Looks in /etc/hosts for matching entries. Note that this is done *before* the normal lookup is + * done. The data from /etc/hosts hence takes precedence over the network. */ + + if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE)) + return 0; + + r = manager_etc_hosts_lookup( + q->manager, + q->question_bypass ? q->question_bypass->question : q->question_utf8, + &answer); + if (r <= 0) + return r; + + dns_query_reset_answer(q); + + q->answer = TAKE_PTR(answer); + q->answer_rcode = DNS_RCODE_SUCCESS; + q->answer_protocol = dns_synthesize_protocol(q->flags); + q->answer_family = dns_synthesize_family(q->flags); + q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC; + + return 1; +} + +int dns_query_go(DnsQuery *q) { + DnsScopeMatch found = DNS_SCOPE_NO; + DnsScope *first = NULL; + int r; + + assert(q); + + if (q->state != DNS_TRANSACTION_NULL) + return 0; + + r = dns_query_try_etc_hosts(q); + if (r < 0) + return r; + if (r > 0) { + dns_query_complete(q, DNS_TRANSACTION_SUCCESS); + return 1; + } + + LIST_FOREACH(scopes, s, q->manager->dns_scopes) { + DnsScopeMatch match; + + match = dns_scope_good_domain(s, q); + assert(match >= 0); + if (match > found) { /* Does this match better? If so, remember how well it matched, and the first one + * that matches this well */ + found = match; + first = s; + } + } + + if (found == DNS_SCOPE_NO) { + DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS; + + r = dns_query_synthesize_reply(q, &state); + if (r < 0) + return r; + + dns_query_complete(q, state); + return 1; + } + + r = dns_query_add_candidate(q, first); + if (r < 0) + goto fail; + + LIST_FOREACH(scopes, s, first->scopes_next) { + DnsScopeMatch match; + + match = dns_scope_good_domain(s, q); + assert(match >= 0); + if (match < found) + continue; + + r = dns_query_add_candidate(q, s); + if (r < 0) + goto fail; + } + + dns_query_reset_answer(q); + + r = event_reset_time_relative( + q->manager->event, + &q->timeout_event_source, + CLOCK_BOOTTIME, + SD_RESOLVED_QUERY_TIMEOUT_USEC, + 0, on_query_timeout, q, + 0, "query-timeout", true); + if (r < 0) + goto fail; + + q->state = DNS_TRANSACTION_PENDING; + q->block_ready++; + + /* Start the transactions */ + LIST_FOREACH(candidates_by_query, c, q->candidates) { + r = dns_query_candidate_go(c); + if (r < 0) { + q->block_ready--; + goto fail; + } + } + + q->block_ready--; + dns_query_ready(q); + + return 1; + +fail: + dns_query_stop(q); + return r; +} + +static void dns_query_accept(DnsQuery *q, DnsQueryCandidate *c) { + DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS; + bool has_authenticated = false, has_non_authenticated = false, has_confidential = false, has_non_confidential = false; + DnssecResult dnssec_result_authenticated = _DNSSEC_RESULT_INVALID, dnssec_result_non_authenticated = _DNSSEC_RESULT_INVALID; + DnsTransaction *t; + int r; + + assert(q); + + if (!c) { + r = dns_query_synthesize_reply(q, &state); + if (r < 0) + goto fail; + + dns_query_complete(q, state); + return; + } + + if (c->error_code != 0) { + /* If the candidate had an error condition of its own, start with that. */ + state = DNS_TRANSACTION_ERRNO; + q->answer = dns_answer_unref(q->answer); + q->answer_rcode = 0; + q->answer_dnssec_result = _DNSSEC_RESULT_INVALID; + q->answer_query_flags = 0; + q->answer_errno = c->error_code; + q->answer_full_packet = dns_packet_unref(q->answer_full_packet); + } + + SET_FOREACH(t, c->transactions) { + + switch (t->state) { + + case DNS_TRANSACTION_SUCCESS: { + /* We found a successful reply, merge it into the answer */ + + if (state == DNS_TRANSACTION_SUCCESS) { + r = dns_answer_extend(&q->answer, t->answer); + if (r < 0) + goto fail; + + q->answer_query_flags |= dns_transaction_source_to_query_flags(t->answer_source); + } else { + /* Override non-successful previous answers */ + DNS_ANSWER_REPLACE(q->answer, dns_answer_ref(t->answer)); + q->answer_query_flags = dns_transaction_source_to_query_flags(t->answer_source); + } + + q->answer_rcode = t->answer_rcode; + q->answer_errno = 0; + + DNS_PACKET_REPLACE(q->answer_full_packet, dns_packet_ref(t->received)); + + if (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) { + has_authenticated = true; + dnssec_result_authenticated = t->answer_dnssec_result; + } else { + has_non_authenticated = true; + dnssec_result_non_authenticated = t->answer_dnssec_result; + } + + if (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL)) + has_confidential = true; + else + has_non_confidential = true; + + state = DNS_TRANSACTION_SUCCESS; + break; + } + + case DNS_TRANSACTION_NULL: + case DNS_TRANSACTION_PENDING: + case DNS_TRANSACTION_VALIDATING: + case DNS_TRANSACTION_ABORTED: + /* Ignore transactions that didn't complete */ + continue; + + default: + /* Any kind of failure? Store the data away, if there's nothing stored yet. */ + if (state == DNS_TRANSACTION_SUCCESS) + continue; + + /* If there's already an authenticated negative reply stored, then prefer that over any unauthenticated one */ + if (FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) && + !FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) + continue; + + DNS_ANSWER_REPLACE(q->answer, dns_answer_ref(t->answer)); + q->answer_rcode = t->answer_rcode; + q->answer_dnssec_result = t->answer_dnssec_result; + q->answer_query_flags = t->answer_query_flags | dns_transaction_source_to_query_flags(t->answer_source); + q->answer_errno = t->answer_errno; + DNS_PACKET_REPLACE(q->answer_full_packet, dns_packet_ref(t->received)); + + state = t->state; + break; + } + } + + if (state == DNS_TRANSACTION_SUCCESS) { + SET_FLAG(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED, has_authenticated && !has_non_authenticated); + SET_FLAG(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL, has_confidential && !has_non_confidential); + q->answer_dnssec_result = FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) ? dnssec_result_authenticated : dnssec_result_non_authenticated; + } + + q->answer_protocol = c->scope->protocol; + q->answer_family = c->scope->family; + + dns_search_domain_unref(q->answer_search_domain); + q->answer_search_domain = dns_search_domain_ref(c->search_domain); + + r = dns_query_synthesize_reply(q, &state); + if (r < 0) + goto fail; + + dns_query_complete(q, state); + return; + +fail: + q->answer_errno = -r; + dns_query_complete(q, DNS_TRANSACTION_ERRNO); +} + +void dns_query_ready(DnsQuery *q) { + DnsQueryCandidate *bad = NULL; + bool pending = false; + + assert(q); + assert(DNS_TRANSACTION_IS_LIVE(q->state)); + + /* Note that this call might invalidate the query. Callers + * should hence not attempt to access the query or transaction + * after calling this function, unless the block_ready + * counter was explicitly bumped before doing so. */ + + if (q->block_ready > 0) + return; + + LIST_FOREACH(candidates_by_query, c, q->candidates) { + DnsTransactionState state; + + state = dns_query_candidate_state(c); + switch (state) { + + case DNS_TRANSACTION_SUCCESS: + /* One of the candidates is successful, + * let's use it, and copy its data out */ + dns_query_accept(q, c); + return; + + case DNS_TRANSACTION_NULL: + case DNS_TRANSACTION_PENDING: + case DNS_TRANSACTION_VALIDATING: + /* One of the candidates is still going on, + * let's maybe wait for it */ + pending = true; + break; + + default: + /* Any kind of failure */ + bad = c; + break; + } + } + + if (pending) + return; + + dns_query_accept(q, bad); +} + +static int dns_query_collect_question(DnsQuery *q, DnsQuestion *question) { + _cleanup_(dns_question_unrefp) DnsQuestion *merged = NULL; + int r; + + assert(q); + + if (dns_question_size(question) == 0) + return 0; + + /* When redirecting, save the first element in the chain, for informational purposes when monitoring */ + r = dns_question_merge(q->collected_questions, question, &merged); + if (r < 0) + return r; + + dns_question_unref(q->collected_questions); + q->collected_questions = TAKE_PTR(merged); + + return 0; +} + +static int dns_query_cname_redirect(DnsQuery *q, const DnsResourceRecord *cname) { + _cleanup_(dns_question_unrefp) DnsQuestion *nq_idna = NULL, *nq_utf8 = NULL; + int r, k; + + assert(q); + + if (q->n_cname_redirects >= CNAME_REDIRECTS_MAX) + return -ELOOP; + q->n_cname_redirects++; + + r = dns_question_cname_redirect(q->question_idna, cname, &nq_idna); + if (r < 0) + return r; + if (r > 0) + log_debug("Following CNAME/DNAME %s %s %s.", + dns_question_first_name(q->question_idna), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), + dns_question_first_name(nq_idna)); + + k = dns_question_is_equal(q->question_idna, q->question_utf8); + if (k < 0) + return k; + if (k > 0) { + /* Same question? Shortcut new question generation */ + nq_utf8 = dns_question_ref(nq_idna); + k = r; + } else { + k = dns_question_cname_redirect(q->question_utf8, cname, &nq_utf8); + if (k < 0) + return k; + if (k > 0) + log_debug("Following UTF8 CNAME/DNAME %s %s %s.", + dns_question_first_name(q->question_utf8), + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), + dns_question_first_name(nq_utf8)); + } + + if (r == 0 && k == 0) /* No actual cname happened? */ + return -ELOOP; + + if (q->answer_protocol == DNS_PROTOCOL_DNS) + /* Don't permit CNAME redirects from unicast DNS to LLMNR or MulticastDNS, so that global resources + * cannot invade the local namespace. The opposite way we permit: local names may redirect to global + * ones. */ + q->flags &= ~(SD_RESOLVED_LLMNR|SD_RESOLVED_MDNS); /* mask away the local protocols */ + + /* Turn off searching for the new name */ + q->flags |= SD_RESOLVED_NO_SEARCH; + + r = dns_query_collect_question(q, q->question_idna); + if (r < 0) + return r; + r = dns_query_collect_question(q, q->question_utf8); + if (r < 0) + return r; + + /* Install the redirected question */ + dns_question_unref(q->question_idna); + q->question_idna = TAKE_PTR(nq_idna); + + dns_question_unref(q->question_utf8); + q->question_utf8 = TAKE_PTR(nq_utf8); + + dns_query_unlink_candidates(q); + + /* Note that we do *not* reset the answer here, because the answer we previously got might already + * include everything we need, let's check that first */ + + q->state = DNS_TRANSACTION_NULL; + + return 0; +} + +int dns_query_process_cname_one(DnsQuery *q) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *cname = NULL; + DnsQuestion *question; + DnsResourceRecord *rr; + bool full_match = true; + DnsResourceKey *k; + int r; + + assert(q); + + /* Processes a CNAME redirect if there's one. Returns one of three values: + * + * CNAME_QUERY_MATCH → direct RR match, caller should just use the RRs in this answer (and not + * bother with any CNAME/DNAME stuff) + * + * CNAME_QUERY_NOMATCH → no match at all, neither direct nor CNAME/DNAME, caller might decide to + * restart query or take things as NODATA reply. + * + * CNAME_QUERY_CNAME → no direct RR match, but a CNAME/DNAME match that we now followed for one step. + * + * The function might also return a failure, in particular -ELOOP if we encountered too many + * CNAMEs/DNAMEs in a chain or if following CNAMEs/DNAMEs was turned off. + * + * Note that this function doesn't actually restart the query. The caller can decide to do that in + * case of CNAME_QUERY_CNAME, though. */ + + if (!IN_SET(q->state, DNS_TRANSACTION_SUCCESS, DNS_TRANSACTION_NULL)) + return DNS_QUERY_NOMATCH; + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + /* Small reminder: our question will consist of one or more RR keys that match in name, but not in + * record type. Specifically, when we do an address lookup the question will typically consist of one + * A and one AAAA key lookup for the same domain name. When we get a response from a server we need + * to check if the answer answers all our questions to use it. Note that a response of CNAME/DNAME + * can answer both an A and the AAAA question for us, but an A/AAAA response only the relevant + * type. + * + * Hence we first check of the answers we collected are sufficient to answer all our questions + * directly. If one question wasn't answered we go on, waiting for more replies. However, if there's + * a CNAME/DNAME response we use it, and redirect to it, regardless if it was a response to the A or + * the AAAA query. */ + + DNS_QUESTION_FOREACH(k, question) { + bool match = false; + + DNS_ANSWER_FOREACH(rr, q->answer) { + r = dns_resource_key_match_rr(k, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain)); + if (r < 0) + return r; + if (r > 0) { + match = true; /* Yay, we found an RR that matches the key we are looking for */ + break; + } + } + + if (!match) { + /* Hmm. :-( there's no response for this key. This doesn't match. */ + full_match = false; + break; + } + } + + if (full_match) + return DNS_QUERY_MATCH; /* The answer can answer our question in full, no need to follow CNAMEs/DNAMEs */ + + /* Let's see if there is a CNAME/DNAME to match. This case is simpler: we accept the CNAME/DNAME that + * matches any of our questions. */ + DNS_ANSWER_FOREACH(rr, q->answer) { + r = dns_question_matches_cname_or_dname(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain)); + if (r < 0) + return r; + if (r > 0 && !cname) + cname = dns_resource_record_ref(rr); + } + + if (!cname) + return DNS_QUERY_NOMATCH; /* No match and no CNAME/DNAME to follow */ + + if (q->flags & SD_RESOLVED_NO_CNAME) + return -ELOOP; + + if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) + q->previous_redirect_unauthenticated = true; + if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL)) + q->previous_redirect_non_confidential = true; + if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_SYNTHETIC)) + q->previous_redirect_non_synthetic = true; + + /* OK, let's actually follow the CNAME */ + r = dns_query_cname_redirect(q, cname); + if (r < 0) + return r; + + return DNS_QUERY_CNAME; /* Tell caller that we did a single CNAME/DNAME redirection step */ +} + +int dns_query_process_cname_many(DnsQuery *q) { + int r; + + assert(q); + + /* Follows CNAMEs through the current packet: as long as the current packet can fulfill our + * redirected CNAME queries we keep going, and restart the query once the current packet isn't good + * enough anymore. It's a wrapper around dns_query_process_cname_one() and returns the same values, + * but with extended semantics. Specifically: + * + * DNS_QUERY_MATCH → as above + * + * DNS_QUERY_CNAME → we ran into a CNAME/DNAME redirect that we could not answer from the current + * message, and thus restarted the query to resolve it. + * + * DNS_QUERY_NOMATCH → we reached the end of CNAME/DNAME chain, and there are no direct matches nor a + * CNAME/DNAME match. i.e. this is a NODATA case. + * + * Note that this function will restart the query for the caller if needed, and that's the case + * DNS_QUERY_CNAME is returned. + */ + + r = dns_query_process_cname_one(q); + if (r != DNS_QUERY_CNAME) + return r; /* The first redirect is special: if it doesn't answer the question that's no + * reason to restart the query, we just accept this as a NODATA answer. */ + + for (;;) { + r = dns_query_process_cname_one(q); + if (r < 0 || r == DNS_QUERY_MATCH) + return r; + if (r == DNS_QUERY_NOMATCH) { + /* OK, so we followed one or more CNAME/DNAME RR but the existing packet can't answer + * this. Let's restart the query hence, with the new question. Why the different + * handling than the first chain element? Because if the server answers a direct + * question with an empty answer then this is a NODATA response. But if it responds + * with a CNAME chain that ultimately is incomplete (i.e. a non-empty but truncated + * CNAME chain) then we better follow up ourselves and ask for the rest of the + * chain. This is particular relevant since our cache will store CNAME/DNAME + * redirects that we learnt about for lookups of certain DNS types, but later on we + * can reuse this data even for other DNS types, but in that case need to follow up + * with the final lookup of the chain ourselves with the RR type we ourselves are + * interested in. */ + r = dns_query_go(q); + if (r < 0) + return r; + + return DNS_QUERY_CNAME; + } + + /* So we found a CNAME that the existing packet already answers, again via a CNAME, let's + * continue going then. */ + assert(r == DNS_QUERY_CNAME); + } +} + +DnsQuestion* dns_query_question_for_protocol(DnsQuery *q, DnsProtocol protocol) { + assert(q); + + if (q->question_bypass) + return q->question_bypass->question; + + switch (protocol) { + + case DNS_PROTOCOL_DNS: + return q->question_idna; + + case DNS_PROTOCOL_MDNS: + case DNS_PROTOCOL_LLMNR: + return q->question_utf8; + + default: + return NULL; + } +} + +const char *dns_query_string(DnsQuery *q) { + const char *name; + int r; + + /* Returns a somewhat useful human-readable lookup key string for this query */ + + if (q->question_bypass) + return dns_question_first_name(q->question_bypass->question); + + if (q->request_address_string) + return q->request_address_string; + + if (q->request_address_valid) { + r = in_addr_to_string(q->request_family, &q->request_address, &q->request_address_string); + if (r >= 0) + return q->request_address_string; + } + + name = dns_question_first_name(q->question_utf8); + if (name) + return name; + + return dns_question_first_name(q->question_idna); +} + +bool dns_query_fully_authenticated(DnsQuery *q) { + assert(q); + + return FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) && !q->previous_redirect_unauthenticated; +} + +bool dns_query_fully_confidential(DnsQuery *q) { + assert(q); + + return FLAGS_SET(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL) && !q->previous_redirect_non_confidential; +} + +bool dns_query_fully_authoritative(DnsQuery *q) { + assert(q); + + /* We are authoritative for everything synthetic (except if a previous CNAME/DNAME) wasn't + * synthetic. (Note: SD_RESOLVED_SYNTHETIC is reset on each CNAME/DNAME, hence the explicit check for + * previous synthetic DNAME/CNAME redirections.) */ + if ((q->answer_query_flags & SD_RESOLVED_SYNTHETIC) && !q->previous_redirect_non_synthetic) + return true; + + /* We are also authoritative for everything coming only from the trust anchor and the local + * zones. (Note: the SD_RESOLVED_FROM_xyz flags we merge on each redirect, hence no need to + * explicitly check previous redirects here.) */ + return (q->answer_query_flags & SD_RESOLVED_FROM_MASK & ~(SD_RESOLVED_FROM_TRUST_ANCHOR | SD_RESOLVED_FROM_ZONE)) == 0; +} diff --git a/src/resolve/resolved-dns-query.h b/src/resolve/resolved-dns-query.h new file mode 100644 index 0000000..2723299 --- /dev/null +++ b/src/resolve/resolved-dns-query.h @@ -0,0 +1,166 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" + +#include "set.h" +#include "varlink.h" + +typedef struct DnsQueryCandidate DnsQueryCandidate; +typedef struct DnsQuery DnsQuery; +typedef struct DnsStubListenerExtra DnsStubListenerExtra; + +#include "resolved-dns-answer.h" +#include "resolved-dns-question.h" +#include "resolved-dns-search-domain.h" +#include "resolved-dns-transaction.h" + +struct DnsQueryCandidate { + unsigned n_ref; + int error_code; + + DnsQuery *query; + DnsScope *scope; + + DnsSearchDomain *search_domain; + + Set *transactions; + + LIST_FIELDS(DnsQueryCandidate, candidates_by_query); + LIST_FIELDS(DnsQueryCandidate, candidates_by_scope); +}; + +struct DnsQuery { + Manager *manager; + + /* The question, formatted in IDNA for use on classic DNS, and as UTF8 for use in LLMNR or mDNS. Note + * that even on classic DNS some labels might use UTF8 encoding. Specifically, DNS-SD service names + * (in contrast to their domain suffixes) use UTF-8 encoding even on DNS. Thus, the difference + * between these two fields is mostly relevant only for explicit *hostname* lookups as well as the + * domain suffixes of service lookups. + * + * Note that questions may consist of multiple RR keys at once, but they must be for the same domain + * name. This is used for A+AAAA and TXT+SRV lookups: we'll allocate a single DnsQuery object for + * them instead of two separate ones. That allows us minor optimizations with response handling: + * CNAME/DNAMEs of the first reply we get can already be used to follow the CNAME/DNAME chain for + * both, and we can take benefit of server replies that oftentimes put A responses into AAAA queries + * and vice versa (in the additional section). */ + DnsQuestion *question_idna; + DnsQuestion *question_utf8; + + /* If this is not a question by ourselves, but a "bypass" request, we propagate the original packet + * here, and use that instead. */ + DnsPacket *question_bypass; + + /* When we follow a CNAME redirect, we save the original question here, for informational/monitoring + * purposes. We'll keep adding to this whenever we go one step in the redirect, so that in the end + * this will contain the complete set of CNAME questions. */ + DnsQuestion *collected_questions; + + uint64_t flags; + int ifindex; + + /* When resolving a service, we first create a TXT+SRV query, and then for the hostnames we discover + * auxiliary A+AAAA queries. This pointer always points from the auxiliary queries back to the + * TXT+SRV query. */ + int auxiliary_result; + DnsQuery *auxiliary_for; + LIST_HEAD(DnsQuery, auxiliary_queries); + + LIST_HEAD(DnsQueryCandidate, candidates); + sd_event_source *timeout_event_source; + + /* Discovered data */ + DnsAnswer *answer; + int answer_rcode; + DnssecResult answer_dnssec_result; + uint64_t answer_query_flags; + DnsProtocol answer_protocol; + int answer_family; + DnsPacket *answer_full_packet; + DnsSearchDomain *answer_search_domain; + + DnsTransactionState state; + int answer_errno; /* if state is DNS_TRANSACTION_ERRNO */ + + unsigned block_ready; + + uint8_t n_auxiliary_queries; + uint8_t n_cname_redirects; + + bool previous_redirect_unauthenticated:1; + bool previous_redirect_non_confidential:1; + bool previous_redirect_non_synthetic:1; + bool request_address_valid:1; + + /* Bus + Varlink client information */ + sd_bus_message *bus_request; + Varlink *varlink_request; + int request_family; + union in_addr_union request_address; + unsigned block_all_complete; + char *request_address_string; + + /* DNS stub information */ + DnsPacket *request_packet; + DnsStream *request_stream; + DnsAnswer *reply_answer; + DnsAnswer *reply_authoritative; + DnsAnswer *reply_additional; + DnsStubListenerExtra *stub_listener_extra; + + /* Completion callback */ + void (*complete)(DnsQuery* q); + + sd_bus_track *bus_track; + + LIST_FIELDS(DnsQuery, queries); + LIST_FIELDS(DnsQuery, auxiliary_queries); + + /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */ +}; + +enum { + DNS_QUERY_MATCH, + DNS_QUERY_NOMATCH, + DNS_QUERY_CNAME, +}; + +DnsQueryCandidate* dns_query_candidate_ref(DnsQueryCandidate*); +DnsQueryCandidate* dns_query_candidate_unref(DnsQueryCandidate*); +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQueryCandidate*, dns_query_candidate_unref); + +void dns_query_candidate_notify(DnsQueryCandidate *c); + +int dns_query_new(Manager *m, DnsQuery **q, DnsQuestion *question_utf8, DnsQuestion *question_idna, DnsPacket *question_bypass, int family, uint64_t flags); +DnsQuery *dns_query_free(DnsQuery *q); + +int dns_query_make_auxiliary(DnsQuery *q, DnsQuery *auxiliary_for); + +int dns_query_go(DnsQuery *q); +void dns_query_ready(DnsQuery *q); + +int dns_query_process_cname_one(DnsQuery *q); +int dns_query_process_cname_many(DnsQuery *q); + +void dns_query_complete(DnsQuery *q, DnsTransactionState state); + +DnsQuestion* dns_query_question_for_protocol(DnsQuery *q, DnsProtocol protocol); + +const char *dns_query_string(DnsQuery *q); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuery*, dns_query_free); + +bool dns_query_fully_authenticated(DnsQuery *q); +bool dns_query_fully_confidential(DnsQuery *q); +bool dns_query_fully_authoritative(DnsQuery *q); + +static inline uint64_t dns_query_reply_flags_make(DnsQuery *q) { + assert(q); + + return SD_RESOLVED_FLAGS_MAKE(q->answer_protocol, + q->answer_family, + dns_query_fully_authenticated(q), + dns_query_fully_confidential(q)) | + (q->answer_query_flags & (SD_RESOLVED_FROM_MASK|SD_RESOLVED_SYNTHETIC)); +} diff --git a/src/resolve/resolved-dns-question.c b/src/resolve/resolved-dns-question.c new file mode 100644 index 0000000..5754c85 --- /dev/null +++ b/src/resolve/resolved-dns-question.c @@ -0,0 +1,552 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "dns-domain.h" +#include "dns-type.h" +#include "resolved-dns-question.h" +#include "socket-util.h" + +DnsQuestion *dns_question_new(size_t n) { + DnsQuestion *q; + + if (n > UINT16_MAX) /* We can only place 64K key in an question section at max */ + n = UINT16_MAX; + + q = malloc0(offsetof(DnsQuestion, items) + sizeof(DnsQuestionItem) * n); + if (!q) + return NULL; + + q->n_ref = 1; + q->n_allocated = n; + + return q; +} + +static DnsQuestion *dns_question_free(DnsQuestion *q) { + DnsResourceKey *key; + + assert(q); + + DNS_QUESTION_FOREACH(key, q) + dns_resource_key_unref(key); + + return mfree(q); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsQuestion, dns_question, dns_question_free); + +int dns_question_add_raw(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags) { + /* Insert without checking for duplicates. */ + + assert(key); + assert(q); + + if (q->n_keys >= q->n_allocated) + return -ENOSPC; + + q->items[q->n_keys++] = (DnsQuestionItem) { + .key = dns_resource_key_ref(key), + .flags = flags, + }; + return 0; +} + +static int dns_question_add_raw_all(DnsQuestion *a, DnsQuestion *b) { + DnsQuestionItem *item; + int r; + + DNS_QUESTION_FOREACH_ITEM(item, b) { + r = dns_question_add_raw(a, item->key, item->flags); + if (r < 0) + return r; + } + + return 0; +} + +int dns_question_add(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags) { + DnsQuestionItem *item; + int r; + + assert(key); + + if (!q) + return -ENOSPC; + + + DNS_QUESTION_FOREACH_ITEM(item, q) { + r = dns_resource_key_equal(item->key, key); + if (r < 0) + return r; + if (r > 0 && item->flags == flags) + return 0; + } + + return dns_question_add_raw(q, key, flags); +} + +static int dns_question_add_all(DnsQuestion *a, DnsQuestion *b) { + DnsQuestionItem *item; + int r; + + DNS_QUESTION_FOREACH_ITEM(item, b) { + r = dns_question_add(a, item->key, item->flags); + if (r < 0) + return r; + } + + return 0; +} + +int dns_question_matches_rr(DnsQuestion *q, DnsResourceRecord *rr, const char *search_domain) { + DnsResourceKey *key; + int r; + + assert(rr); + + if (!q) + return 0; + + DNS_QUESTION_FOREACH(key, q) { + r = dns_resource_key_match_rr(key, rr, search_domain); + if (r != 0) + return r; + } + + return 0; +} + +int dns_question_matches_cname_or_dname(DnsQuestion *q, DnsResourceRecord *rr, const char *search_domain) { + DnsResourceKey *key; + int r; + + assert(rr); + + if (!q) + return 0; + + if (!IN_SET(rr->key->type, DNS_TYPE_CNAME, DNS_TYPE_DNAME)) + return 0; + + DNS_QUESTION_FOREACH(key, q) { + /* For a {C,D}NAME record we can never find a matching {C,D}NAME record */ + if (!dns_type_may_redirect(key->type)) + return 0; + + r = dns_resource_key_match_cname_or_dname(key, rr->key, search_domain); + if (r != 0) + return r; + } + + return 0; +} + +int dns_question_is_valid_for_query(DnsQuestion *q) { + const char *name; + size_t i; + int r; + + if (!q) + return 0; + + if (q->n_keys <= 0) + return 0; + + if (q->n_keys > 65535) + return 0; + + name = dns_resource_key_name(q->items[0].key); + if (!name) + return 0; + + /* Check that all keys in this question bear the same name */ + for (i = 0; i < q->n_keys; i++) { + assert(q->items[i].key); + + if (i > 0) { + r = dns_name_equal(dns_resource_key_name(q->items[i].key), name); + if (r <= 0) + return r; + } + + if (!dns_type_is_valid_query(q->items[i].key->type)) + return 0; + } + + return 1; +} + +int dns_question_contains_key(DnsQuestion *q, const DnsResourceKey *k) { + size_t j; + int r; + + assert(k); + + if (!q) + return 0; + + + for (j = 0; j < q->n_keys; j++) { + r = dns_resource_key_equal(q->items[j].key, k); + if (r != 0) + return r; + } + + return 0; +} + +static int dns_question_contains_item(DnsQuestion *q, const DnsQuestionItem *i) { + DnsQuestionItem *item; + int r; + + assert(i); + + DNS_QUESTION_FOREACH_ITEM(item, q) { + if (item->flags != i->flags) + continue; + r = dns_resource_key_equal(item->key, i->key); + if (r != 0) + return r; + } + + return false; +} + +int dns_question_is_equal(DnsQuestion *a, DnsQuestion *b) { + DnsQuestionItem *item; + int r; + + if (a == b) + return 1; + + if (!a) + return !b || b->n_keys == 0; + if (!b) + return a->n_keys == 0; + + /* Checks if all items in a are also contained b, and vice versa */ + + DNS_QUESTION_FOREACH_ITEM(item, a) { + r = dns_question_contains_item(b, item); + if (r <= 0) + return r; + } + DNS_QUESTION_FOREACH_ITEM(item, b) { + r = dns_question_contains_item(a, item); + if (r <= 0) + return r; + } + + return 1; +} + +int dns_question_cname_redirect(DnsQuestion *q, const DnsResourceRecord *cname, DnsQuestion **ret) { + _cleanup_(dns_question_unrefp) DnsQuestion *n = NULL; + DnsResourceKey *key; + bool same = true; + int r; + + assert(cname); + assert(ret); + assert(IN_SET(cname->key->type, DNS_TYPE_CNAME, DNS_TYPE_DNAME)); + + if (dns_question_size(q) <= 0) { + *ret = NULL; + return 0; + } + + DNS_QUESTION_FOREACH(key, q) { + _cleanup_free_ char *destination = NULL; + const char *d; + + if (cname->key->type == DNS_TYPE_CNAME) + d = cname->cname.name; + else { + r = dns_name_change_suffix(dns_resource_key_name(key), dns_resource_key_name(cname->key), cname->dname.name, &destination); + if (r < 0) + return r; + if (r == 0) + continue; + + d = destination; + } + + r = dns_name_equal(dns_resource_key_name(key), d); + if (r < 0) + return r; + + if (r == 0) { + same = false; + break; + } + } + + /* Fully the same, indicate we didn't do a thing */ + if (same) { + *ret = NULL; + return 0; + } + + n = dns_question_new(q->n_keys); + if (!n) + return -ENOMEM; + + /* Create a new question, and patch in the new name */ + DNS_QUESTION_FOREACH(key, q) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *k = NULL; + + k = dns_resource_key_new_redirect(key, cname); + if (!k) + return -ENOMEM; + + r = dns_question_add(n, k, 0); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(n); + + return 1; +} + +const char *dns_question_first_name(DnsQuestion *q) { + + if (!q) + return NULL; + + if (q->n_keys < 1) + return NULL; + + return dns_resource_key_name(q->items[0].key); +} + +int dns_question_new_address(DnsQuestion **ret, int family, const char *name, bool convert_idna) { + _cleanup_(dns_question_unrefp) DnsQuestion *q = NULL; + _cleanup_free_ char *buf = NULL; + int r; + + assert(ret); + assert(name); + + if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC)) + return -EAFNOSUPPORT; + + /* If IPv6 is off and the request has an unspecified lookup family, restrict it automatically to + * IPv4. */ + if (family == AF_UNSPEC && !socket_ipv6_is_enabled()) + family = AF_INET; + + if (convert_idna) { + r = dns_name_apply_idna(name, &buf); + if (r < 0) + return r; + if (r > 0 && !streq(name, buf)) + name = buf; + else + /* We did not manage to create convert the idna name, or it's + * the same as the original name. We assume the caller already + * created an unconverted question, so let's not repeat work + * unnecessarily. */ + return -EALREADY; + } + + q = dns_question_new(family == AF_UNSPEC ? 2 : 1); + if (!q) + return -ENOMEM; + + if (family != AF_INET6) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, name); + if (!key) + return -ENOMEM; + + r = dns_question_add(q, key, 0); + if (r < 0) + return r; + } + + if (family != AF_INET) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_AAAA, name); + if (!key) + return -ENOMEM; + + r = dns_question_add(q, key, 0); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(q); + + return 0; +} + +int dns_question_new_reverse(DnsQuestion **ret, int family, const union in_addr_union *a) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + _cleanup_(dns_question_unrefp) DnsQuestion *q = NULL; + _cleanup_free_ char *reverse = NULL; + int r; + + assert(ret); + assert(a); + + if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC)) + return -EAFNOSUPPORT; + + r = dns_name_reverse(family, a, &reverse); + if (r < 0) + return r; + + q = dns_question_new(1); + if (!q) + return -ENOMEM; + + key = dns_resource_key_new_consume(DNS_CLASS_IN, DNS_TYPE_PTR, reverse); + if (!key) + return -ENOMEM; + + reverse = NULL; + + r = dns_question_add(q, key, 0); + if (r < 0) + return r; + + *ret = TAKE_PTR(q); + + return 0; +} + +int dns_question_new_service( + DnsQuestion **ret, + const char *service, + const char *type, + const char *domain, + bool with_txt, + bool convert_idna) { + + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + _cleanup_(dns_question_unrefp) DnsQuestion *q = NULL; + _cleanup_free_ char *buf = NULL, *joined = NULL; + const char *name; + int r; + + assert(ret); + + /* We support three modes of invocation: + * + * 1. Only a domain is specified, in which case we assume a properly encoded SRV RR name, including service + * type and possibly a service name. If specified in this way we assume it's already IDNA converted if + * that's necessary. + * + * 2. Both service type and a domain specified, in which case a normal SRV RR is assumed, without a DNS-SD + * style prefix. In this case we'll IDNA convert the domain, if that's requested. + * + * 3. All three of service name, type and domain are specified, in which case a DNS-SD service is put + * together. The service name is never IDNA converted, and the domain is if requested. + * + * It's not supported to specify a service name without a type, or no domain name. + */ + + if (!domain) + return -EINVAL; + + if (type) { + if (convert_idna) { + r = dns_name_apply_idna(domain, &buf); + if (r < 0) + return r; + if (r > 0) + domain = buf; + } + + r = dns_service_join(service, type, domain, &joined); + if (r < 0) + return r; + + name = joined; + } else { + if (service) + return -EINVAL; + + name = domain; + } + + q = dns_question_new(1 + with_txt); + if (!q) + return -ENOMEM; + + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_SRV, name); + if (!key) + return -ENOMEM; + + r = dns_question_add(q, key, 0); + if (r < 0) + return r; + + if (with_txt) { + dns_resource_key_unref(key); + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_TXT, name); + if (!key) + return -ENOMEM; + + r = dns_question_add(q, key, 0); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(q); + + return 0; +} + +/* + * This function is not used in the code base, but is useful when debugging. Do not delete. + */ +void dns_question_dump(DnsQuestion *question, FILE *f) { + DnsResourceKey *k; + + if (!f) + f = stdout; + + DNS_QUESTION_FOREACH(k, question) { + char buf[DNS_RESOURCE_KEY_STRING_MAX]; + + fputc('\t', f); + fputs(dns_resource_key_to_string(k, buf, sizeof(buf)), f); + fputc('\n', f); + } +} + +int dns_question_merge(DnsQuestion *a, DnsQuestion *b, DnsQuestion **ret) { + _cleanup_(dns_question_unrefp) DnsQuestion *k = NULL; + int r; + + assert(ret); + + if (a == b || dns_question_size(b) <= 0) { + *ret = dns_question_ref(a); + return 0; + } + + if (dns_question_size(a) <= 0) { + *ret = dns_question_ref(b); + return 0; + } + + k = dns_question_new(dns_question_size(a) + dns_question_size(b)); + if (!k) + return -ENOMEM; + + r = dns_question_add_raw_all(k, a); + if (r < 0) + return r; + + r = dns_question_add_all(k, b); + if (r < 0) + return r; + + *ret = TAKE_PTR(k); + return 0; +} diff --git a/src/resolve/resolved-dns-question.h b/src/resolve/resolved-dns-question.h new file mode 100644 index 0000000..91bbeae --- /dev/null +++ b/src/resolve/resolved-dns-question.h @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct DnsQuestion DnsQuestion; +typedef struct DnsQuestionItem DnsQuestionItem; + +#include "macro.h" +#include "resolved-dns-rr.h" + +/* A simple array of resource keys */ + +typedef enum DnsQuestionFlags { + DNS_QUESTION_WANTS_UNICAST_REPLY = 1 << 0, /* For mDNS: sender is willing to accept unicast replies */ +} DnsQuestionFlags; + +struct DnsQuestionItem { + DnsResourceKey *key; + DnsQuestionFlags flags; +}; + +struct DnsQuestion { + unsigned n_ref; + size_t n_keys, n_allocated; + DnsQuestionItem items[0]; +}; + +DnsQuestion *dns_question_new(size_t n); +DnsQuestion *dns_question_ref(DnsQuestion *q); +DnsQuestion *dns_question_unref(DnsQuestion *q); + +int dns_question_new_address(DnsQuestion **ret, int family, const char *name, bool convert_idna); +int dns_question_new_reverse(DnsQuestion **ret, int family, const union in_addr_union *a); +int dns_question_new_service(DnsQuestion **ret, const char *service, const char *type, const char *domain, bool with_txt, bool convert_idna); + +int dns_question_add_raw(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags); +int dns_question_add(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags); + +int dns_question_matches_rr(DnsQuestion *q, DnsResourceRecord *rr, const char *search_domain); +int dns_question_matches_cname_or_dname(DnsQuestion *q, DnsResourceRecord *rr, const char* search_domain); +int dns_question_is_valid_for_query(DnsQuestion *q); +int dns_question_contains_key(DnsQuestion *q, const DnsResourceKey *k); +int dns_question_is_equal(DnsQuestion *a, DnsQuestion *b); + +int dns_question_cname_redirect(DnsQuestion *q, const DnsResourceRecord *cname, DnsQuestion **ret); + +void dns_question_dump(DnsQuestion *q, FILE *f); + +const char *dns_question_first_name(DnsQuestion *q); + +static inline DnsResourceKey *dns_question_first_key(DnsQuestion *q) { + return (q && q->n_keys > 0) ? q->items[0].key : NULL; +} + +static inline size_t dns_question_size(DnsQuestion *q) { + return q ? q->n_keys : 0; +} + +static inline bool dns_question_isempty(DnsQuestion *q) { + return dns_question_size(q) <= 0; +} + +int dns_question_merge(DnsQuestion *a, DnsQuestion *b, DnsQuestion **ret); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuestion*, dns_question_unref); + +#define _DNS_QUESTION_FOREACH(u, k, q) \ + for (size_t UNIQ_T(i, u) = ({ \ + (k) = ((q) && (q)->n_keys > 0) ? (q)->items[0].key : NULL; \ + 0; \ + }); \ + (q) && (UNIQ_T(i, u) < (q)->n_keys); \ + UNIQ_T(i, u)++, (k) = (UNIQ_T(i, u) < (q)->n_keys ? (q)->items[UNIQ_T(i, u)].key : NULL)) + +#define DNS_QUESTION_FOREACH(key, q) _DNS_QUESTION_FOREACH(UNIQ, key, q) + +#define _DNS_QUESTION_FOREACH_ITEM(u, item, q) \ + for (size_t UNIQ_T(i, u) = ({ \ + (item) = dns_question_isempty(q) ? NULL : (q)->items; \ + 0; \ + }); \ + UNIQ_T(i, u) < dns_question_size(q); \ + UNIQ_T(i, u)++, (item) = (UNIQ_T(i, u) < dns_question_size(q) ? (q)->items + UNIQ_T(i, u) : NULL)) + +#define DNS_QUESTION_FOREACH_ITEM(item, q) _DNS_QUESTION_FOREACH_ITEM(UNIQ, item, q) diff --git a/src/resolve/resolved-dns-rr.c b/src/resolve/resolved-dns-rr.c new file mode 100644 index 0000000..a4379c1 --- /dev/null +++ b/src/resolve/resolved-dns-rr.c @@ -0,0 +1,2137 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <math.h> + +#include "alloc-util.h" +#include "dns-domain.h" +#include "dns-type.h" +#include "escape.h" +#include "hexdecoct.h" +#include "memory-util.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-rr.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "terminal-util.h" + +DnsResourceKey* dns_resource_key_new(uint16_t class, uint16_t type, const char *name) { + DnsResourceKey *k; + size_t l; + + assert(name); + + l = strlen(name); + k = malloc0(sizeof(DnsResourceKey) + l + 1); + if (!k) + return NULL; + + k->n_ref = 1; + k->class = class; + k->type = type; + + strcpy((char*) k + sizeof(DnsResourceKey), name); + + return k; +} + +DnsResourceKey* dns_resource_key_new_redirect(const DnsResourceKey *key, const DnsResourceRecord *cname) { + int r; + + assert(key); + assert(cname); + + assert(IN_SET(cname->key->type, DNS_TYPE_CNAME, DNS_TYPE_DNAME)); + + if (cname->key->type == DNS_TYPE_CNAME) + return dns_resource_key_new(key->class, key->type, cname->cname.name); + else { + _cleanup_free_ char *destination = NULL; + DnsResourceKey *k; + + r = dns_name_change_suffix(dns_resource_key_name(key), dns_resource_key_name(cname->key), cname->dname.name, &destination); + if (r < 0) + return NULL; + if (r == 0) + return dns_resource_key_ref((DnsResourceKey*) key); + + k = dns_resource_key_new_consume(key->class, key->type, destination); + if (!k) + return NULL; + + TAKE_PTR(destination); + return k; + } +} + +int dns_resource_key_new_append_suffix(DnsResourceKey **ret, DnsResourceKey *key, char *name) { + DnsResourceKey *new_key; + char *joined; + int r; + + assert(ret); + assert(key); + assert(name); + + if (dns_name_is_root(name)) { + *ret = dns_resource_key_ref(key); + return 0; + } + + r = dns_name_concat(dns_resource_key_name(key), name, 0, &joined); + if (r < 0) + return r; + + new_key = dns_resource_key_new_consume(key->class, key->type, joined); + if (!new_key) { + free(joined); + return -ENOMEM; + } + + *ret = new_key; + return 0; +} + +DnsResourceKey* dns_resource_key_new_consume(uint16_t class, uint16_t type, char *name) { + DnsResourceKey *k; + + assert(name); + + k = new(DnsResourceKey, 1); + if (!k) + return NULL; + + *k = (DnsResourceKey) { + .n_ref = 1, + .class = class, + .type = type, + ._name = name, + }; + + return k; +} + +DnsResourceKey* dns_resource_key_ref(DnsResourceKey *k) { + + if (!k) + return NULL; + + /* Static/const keys created with DNS_RESOURCE_KEY_CONST will + * set this to -1, they should not be reffed/unreffed */ + assert(k->n_ref != UINT_MAX); + + assert(k->n_ref > 0); + k->n_ref++; + + return k; +} + +DnsResourceKey* dns_resource_key_unref(DnsResourceKey *k) { + if (!k) + return NULL; + + assert(k->n_ref != UINT_MAX); + assert(k->n_ref > 0); + + if (k->n_ref == 1) { + free(k->_name); + free(k); + } else + k->n_ref--; + + return NULL; +} + +const char* dns_resource_key_name(const DnsResourceKey *key) { + const char *name; + + if (!key) + return NULL; + + if (key->_name) + name = key->_name; + else + name = (char*) key + sizeof(DnsResourceKey); + + if (dns_name_is_root(name)) + return "."; + else + return name; +} + +bool dns_resource_key_is_address(const DnsResourceKey *key) { + assert(key); + + /* Check if this is an A or AAAA resource key */ + + return key->class == DNS_CLASS_IN && IN_SET(key->type, DNS_TYPE_A, DNS_TYPE_AAAA); +} + +bool dns_resource_key_is_dnssd_ptr(const DnsResourceKey *key) { + assert(key); + + /* Check if this is a PTR resource key used in + Service Instance Enumeration as described in RFC6763 p4.1. */ + + if (key->type != DNS_TYPE_PTR) + return false; + + return dns_name_endswith(dns_resource_key_name(key), "_tcp.local") || + dns_name_endswith(dns_resource_key_name(key), "_udp.local"); +} + +int dns_resource_key_equal(const DnsResourceKey *a, const DnsResourceKey *b) { + int r; + + if (a == b) + return 1; + + r = dns_name_equal(dns_resource_key_name(a), dns_resource_key_name(b)); + if (r <= 0) + return r; + + if (a->class != b->class) + return 0; + + if (a->type != b->type) + return 0; + + return 1; +} + +int dns_resource_key_match_rr(const DnsResourceKey *key, DnsResourceRecord *rr, const char *search_domain) { + int r; + + assert(key); + assert(rr); + + if (key == rr->key) + return 1; + + /* Checks if an rr matches the specified key. If a search + * domain is specified, it will also be checked if the key + * with the search domain suffixed might match the RR. */ + + if (rr->key->class != key->class && key->class != DNS_CLASS_ANY) + return 0; + + if (rr->key->type != key->type && key->type != DNS_TYPE_ANY) + return 0; + + r = dns_name_equal(dns_resource_key_name(rr->key), dns_resource_key_name(key)); + if (r != 0) + return r; + + if (search_domain) { + _cleanup_free_ char *joined = NULL; + + r = dns_name_concat(dns_resource_key_name(key), search_domain, 0, &joined); + if (r < 0) + return r; + + return dns_name_equal(dns_resource_key_name(rr->key), joined); + } + + return 0; +} + +int dns_resource_key_match_cname_or_dname(const DnsResourceKey *key, const DnsResourceKey *cname, const char *search_domain) { + int r; + + assert(key); + assert(cname); + + if (cname->class != key->class && key->class != DNS_CLASS_ANY) + return 0; + + if (!dns_type_may_redirect(key->type)) + return 0; + + if (cname->type == DNS_TYPE_CNAME) + r = dns_name_equal(dns_resource_key_name(key), dns_resource_key_name(cname)); + else if (cname->type == DNS_TYPE_DNAME) + r = dns_name_endswith(dns_resource_key_name(key), dns_resource_key_name(cname)); + else + return 0; + + if (r != 0) + return r; + + if (search_domain) { + _cleanup_free_ char *joined = NULL; + + r = dns_name_concat(dns_resource_key_name(key), search_domain, 0, &joined); + if (r < 0) + return r; + + if (cname->type == DNS_TYPE_CNAME) + return dns_name_equal(joined, dns_resource_key_name(cname)); + else if (cname->type == DNS_TYPE_DNAME) + return dns_name_endswith(joined, dns_resource_key_name(cname)); + } + + return 0; +} + +int dns_resource_key_match_soa(const DnsResourceKey *key, const DnsResourceKey *soa) { + assert(soa); + assert(key); + + /* Checks whether 'soa' is a SOA record for the specified key. */ + + if (soa->class != key->class) + return 0; + + if (soa->type != DNS_TYPE_SOA) + return 0; + + return dns_name_endswith(dns_resource_key_name(key), dns_resource_key_name(soa)); +} + +static void dns_resource_key_hash_func(const DnsResourceKey *k, struct siphash *state) { + assert(k); + + dns_name_hash_func(dns_resource_key_name(k), state); + siphash24_compress(&k->class, sizeof(k->class), state); + siphash24_compress(&k->type, sizeof(k->type), state); +} + +static int dns_resource_key_compare_func(const DnsResourceKey *x, const DnsResourceKey *y) { + int r; + + r = dns_name_compare_func(dns_resource_key_name(x), dns_resource_key_name(y)); + if (r != 0) + return r; + + r = CMP(x->type, y->type); + if (r != 0) + return r; + + return CMP(x->class, y->class); +} + +DEFINE_HASH_OPS(dns_resource_key_hash_ops, DnsResourceKey, dns_resource_key_hash_func, dns_resource_key_compare_func); + +char* dns_resource_key_to_string(const DnsResourceKey *key, char *buf, size_t buf_size) { + const char *c, *t; + char *ans = buf; + + /* If we cannot convert the CLASS/TYPE into a known string, + use the format recommended by RFC 3597, Section 5. */ + + c = dns_class_to_string(key->class); + t = dns_type_to_string(key->type); + + (void) snprintf(buf, buf_size, "%s %s%s%.0u %s%s%.0u", + dns_resource_key_name(key), + strempty(c), c ? "" : "CLASS", c ? 0u : key->class, + strempty(t), t ? "" : "TYPE", t ? 0u : key->type); + + return ans; +} + +bool dns_resource_key_reduce(DnsResourceKey **a, DnsResourceKey **b) { + assert(a); + assert(b); + + /* Try to replace one RR key by another if they are identical, thus saving a bit of memory. Note that we do + * this only for RR keys, not for RRs themselves, as they carry a lot of additional metadata (where they come + * from, validity data, and suchlike), and cannot be replaced so easily by other RRs that have the same + * superficial data. */ + + if (!*a) + return false; + if (!*b) + return false; + + /* We refuse merging const keys */ + if ((*a)->n_ref == UINT_MAX) + return false; + if ((*b)->n_ref == UINT_MAX) + return false; + + /* Already the same? */ + if (*a == *b) + return true; + + /* Are they really identical? */ + if (dns_resource_key_equal(*a, *b) <= 0) + return false; + + /* Keep the one which already has more references. */ + if ((*a)->n_ref > (*b)->n_ref) + DNS_RESOURCE_KEY_REPLACE(*b, dns_resource_key_ref(*a)); + else + DNS_RESOURCE_KEY_REPLACE(*a, dns_resource_key_ref(*b)); + + return true; +} + +DnsResourceRecord* dns_resource_record_new(DnsResourceKey *key) { + DnsResourceRecord *rr; + + rr = new(DnsResourceRecord, 1); + if (!rr) + return NULL; + + *rr = (DnsResourceRecord) { + .n_ref = 1, + .key = dns_resource_key_ref(key), + .expiry = USEC_INFINITY, + .n_skip_labels_signer = UINT8_MAX, + .n_skip_labels_source = UINT8_MAX, + }; + + return rr; +} + +DnsResourceRecord* dns_resource_record_new_full(uint16_t class, uint16_t type, const char *name) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + + key = dns_resource_key_new(class, type, name); + if (!key) + return NULL; + + return dns_resource_record_new(key); +} + +static DnsResourceRecord* dns_resource_record_free(DnsResourceRecord *rr) { + assert(rr); + + if (rr->key) { + switch (rr->key->type) { + + case DNS_TYPE_SRV: + free(rr->srv.name); + break; + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + free(rr->ptr.name); + break; + + case DNS_TYPE_HINFO: + free(rr->hinfo.cpu); + free(rr->hinfo.os); + break; + + case DNS_TYPE_TXT: + case DNS_TYPE_SPF: + dns_txt_item_free_all(rr->txt.items); + break; + + case DNS_TYPE_SOA: + free(rr->soa.mname); + free(rr->soa.rname); + break; + + case DNS_TYPE_MX: + free(rr->mx.exchange); + break; + + case DNS_TYPE_DS: + free(rr->ds.digest); + break; + + case DNS_TYPE_SSHFP: + free(rr->sshfp.fingerprint); + break; + + case DNS_TYPE_DNSKEY: + free(rr->dnskey.key); + break; + + case DNS_TYPE_RRSIG: + free(rr->rrsig.signer); + free(rr->rrsig.signature); + break; + + case DNS_TYPE_NSEC: + free(rr->nsec.next_domain_name); + bitmap_free(rr->nsec.types); + break; + + case DNS_TYPE_NSEC3: + free(rr->nsec3.next_hashed_name); + free(rr->nsec3.salt); + bitmap_free(rr->nsec3.types); + break; + + case DNS_TYPE_LOC: + case DNS_TYPE_A: + case DNS_TYPE_AAAA: + break; + + case DNS_TYPE_TLSA: + free(rr->tlsa.data); + break; + + case DNS_TYPE_CAA: + free(rr->caa.tag); + free(rr->caa.value); + break; + + case DNS_TYPE_OPENPGPKEY: + default: + if (!rr->unparsable) + free(rr->generic.data); + } + + if (rr->unparsable) + free(rr->generic.data); + + free(rr->wire_format); + dns_resource_key_unref(rr->key); + } + + free(rr->to_string); + return mfree(rr); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsResourceRecord, dns_resource_record, dns_resource_record_free); + +int dns_resource_record_new_reverse(DnsResourceRecord **ret, int family, const union in_addr_union *address, const char *hostname) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + _cleanup_free_ char *ptr = NULL; + int r; + + assert(ret); + assert(address); + assert(hostname); + + r = dns_name_reverse(family, address, &ptr); + if (r < 0) + return r; + + key = dns_resource_key_new_consume(DNS_CLASS_IN, DNS_TYPE_PTR, ptr); + if (!key) + return -ENOMEM; + + ptr = NULL; + + rr = dns_resource_record_new(key); + if (!rr) + return -ENOMEM; + + rr->ptr.name = strdup(hostname); + if (!rr->ptr.name) + return -ENOMEM; + + *ret = TAKE_PTR(rr); + + return 0; +} + +int dns_resource_record_new_address(DnsResourceRecord **ret, int family, const union in_addr_union *address, const char *name) { + DnsResourceRecord *rr; + + assert(ret); + assert(address); + assert(family); + + if (family == AF_INET) { + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_A, name); + if (!rr) + return -ENOMEM; + + rr->a.in_addr = address->in; + + } else if (family == AF_INET6) { + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_AAAA, name); + if (!rr) + return -ENOMEM; + + rr->aaaa.in6_addr = address->in6; + } else + return -EAFNOSUPPORT; + + *ret = rr; + + return 0; +} + +#define FIELD_EQUAL(a, b, field) \ + ((a).field ## _size == (b).field ## _size && \ + memcmp_safe((a).field, (b).field, (a).field ## _size) == 0) + +int dns_resource_record_payload_equal(const DnsResourceRecord *a, const DnsResourceRecord *b) { + int r; + + /* Check if a and b are the same, but don't look at their keys */ + + if (a->unparsable != b->unparsable) + return 0; + + switch (a->unparsable ? _DNS_TYPE_INVALID : a->key->type) { + + case DNS_TYPE_SRV: + r = dns_name_equal(a->srv.name, b->srv.name); + if (r <= 0) + return r; + + return a->srv.priority == b->srv.priority && + a->srv.weight == b->srv.weight && + a->srv.port == b->srv.port; + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + return dns_name_equal(a->ptr.name, b->ptr.name); + + case DNS_TYPE_HINFO: + return strcaseeq(a->hinfo.cpu, b->hinfo.cpu) && + strcaseeq(a->hinfo.os, b->hinfo.os); + + case DNS_TYPE_SPF: /* exactly the same as TXT */ + case DNS_TYPE_TXT: + return dns_txt_item_equal(a->txt.items, b->txt.items); + + case DNS_TYPE_A: + return memcmp(&a->a.in_addr, &b->a.in_addr, sizeof(struct in_addr)) == 0; + + case DNS_TYPE_AAAA: + return memcmp(&a->aaaa.in6_addr, &b->aaaa.in6_addr, sizeof(struct in6_addr)) == 0; + + case DNS_TYPE_SOA: + r = dns_name_equal(a->soa.mname, b->soa.mname); + if (r <= 0) + return r; + r = dns_name_equal(a->soa.rname, b->soa.rname); + if (r <= 0) + return r; + + return a->soa.serial == b->soa.serial && + a->soa.refresh == b->soa.refresh && + a->soa.retry == b->soa.retry && + a->soa.expire == b->soa.expire && + a->soa.minimum == b->soa.minimum; + + case DNS_TYPE_MX: + if (a->mx.priority != b->mx.priority) + return 0; + + return dns_name_equal(a->mx.exchange, b->mx.exchange); + + case DNS_TYPE_LOC: + assert(a->loc.version == b->loc.version); + + return a->loc.size == b->loc.size && + a->loc.horiz_pre == b->loc.horiz_pre && + a->loc.vert_pre == b->loc.vert_pre && + a->loc.latitude == b->loc.latitude && + a->loc.longitude == b->loc.longitude && + a->loc.altitude == b->loc.altitude; + + case DNS_TYPE_DS: + return a->ds.key_tag == b->ds.key_tag && + a->ds.algorithm == b->ds.algorithm && + a->ds.digest_type == b->ds.digest_type && + FIELD_EQUAL(a->ds, b->ds, digest); + + case DNS_TYPE_SSHFP: + return a->sshfp.algorithm == b->sshfp.algorithm && + a->sshfp.fptype == b->sshfp.fptype && + FIELD_EQUAL(a->sshfp, b->sshfp, fingerprint); + + case DNS_TYPE_DNSKEY: + return a->dnskey.flags == b->dnskey.flags && + a->dnskey.protocol == b->dnskey.protocol && + a->dnskey.algorithm == b->dnskey.algorithm && + FIELD_EQUAL(a->dnskey, b->dnskey, key); + + case DNS_TYPE_RRSIG: + /* do the fast comparisons first */ + return a->rrsig.type_covered == b->rrsig.type_covered && + a->rrsig.algorithm == b->rrsig.algorithm && + a->rrsig.labels == b->rrsig.labels && + a->rrsig.original_ttl == b->rrsig.original_ttl && + a->rrsig.expiration == b->rrsig.expiration && + a->rrsig.inception == b->rrsig.inception && + a->rrsig.key_tag == b->rrsig.key_tag && + FIELD_EQUAL(a->rrsig, b->rrsig, signature) && + dns_name_equal(a->rrsig.signer, b->rrsig.signer); + + case DNS_TYPE_NSEC: + return dns_name_equal(a->nsec.next_domain_name, b->nsec.next_domain_name) && + bitmap_equal(a->nsec.types, b->nsec.types); + + case DNS_TYPE_NSEC3: + return a->nsec3.algorithm == b->nsec3.algorithm && + a->nsec3.flags == b->nsec3.flags && + a->nsec3.iterations == b->nsec3.iterations && + FIELD_EQUAL(a->nsec3, b->nsec3, salt) && + FIELD_EQUAL(a->nsec3, b->nsec3, next_hashed_name) && + bitmap_equal(a->nsec3.types, b->nsec3.types); + + case DNS_TYPE_TLSA: + return a->tlsa.cert_usage == b->tlsa.cert_usage && + a->tlsa.selector == b->tlsa.selector && + a->tlsa.matching_type == b->tlsa.matching_type && + FIELD_EQUAL(a->tlsa, b->tlsa, data); + + case DNS_TYPE_CAA: + return a->caa.flags == b->caa.flags && + streq(a->caa.tag, b->caa.tag) && + FIELD_EQUAL(a->caa, b->caa, value); + + case DNS_TYPE_OPENPGPKEY: + default: + return FIELD_EQUAL(a->generic, b->generic, data); + } +} + +int dns_resource_record_equal(const DnsResourceRecord *a, const DnsResourceRecord *b) { + int r; + + assert(a); + assert(b); + + if (a == b) + return 1; + + r = dns_resource_key_equal(a->key, b->key); + if (r <= 0) + return r; + + return dns_resource_record_payload_equal(a, b); +} + +static char* format_location(uint32_t latitude, uint32_t longitude, uint32_t altitude, + uint8_t size, uint8_t horiz_pre, uint8_t vert_pre) { + char *s; + char NS = latitude >= 1U<<31 ? 'N' : 'S'; + char EW = longitude >= 1U<<31 ? 'E' : 'W'; + + int lat = latitude >= 1U<<31 ? (int) (latitude - (1U<<31)) : (int) ((1U<<31) - latitude); + int lon = longitude >= 1U<<31 ? (int) (longitude - (1U<<31)) : (int) ((1U<<31) - longitude); + double alt = altitude >= 10000000u ? altitude - 10000000u : -(double)(10000000u - altitude); + double siz = (size >> 4) * exp10((double) (size & 0xF)); + double hor = (horiz_pre >> 4) * exp10((double) (horiz_pre & 0xF)); + double ver = (vert_pre >> 4) * exp10((double) (vert_pre & 0xF)); + + if (asprintf(&s, "%d %d %.3f %c %d %d %.3f %c %.2fm %.2fm %.2fm %.2fm", + (lat / 60000 / 60), + (lat / 60000) % 60, + (lat % 60000) / 1000., + NS, + (lon / 60000 / 60), + (lon / 60000) % 60, + (lon % 60000) / 1000., + EW, + alt / 100., + siz / 100., + hor / 100., + ver / 100.) < 0) + return NULL; + + return s; +} + +static int format_timestamp_dns(char *buf, size_t l, time_t sec) { + struct tm tm; + + assert(buf); + assert(l > STRLEN("YYYYMMDDHHmmSS")); + + if (!gmtime_r(&sec, &tm)) + return -EINVAL; + + if (strftime(buf, l, "%Y%m%d%H%M%S", &tm) <= 0) + return -EINVAL; + + return 0; +} + +static char *format_types(Bitmap *types) { + _cleanup_strv_free_ char **strv = NULL; + _cleanup_free_ char *str = NULL; + unsigned type; + int r; + + BITMAP_FOREACH(type, types) { + if (dns_type_to_string(type)) { + r = strv_extend(&strv, dns_type_to_string(type)); + if (r < 0) + return NULL; + } else { + char *t; + + r = asprintf(&t, "TYPE%u", type); + if (r < 0) + return NULL; + + r = strv_consume(&strv, t); + if (r < 0) + return NULL; + } + } + + str = strv_join(strv, " "); + if (!str) + return NULL; + + return strjoin("( ", str, " )"); +} + +static char *format_txt(DnsTxtItem *first) { + size_t c = 1; + char *p, *s; + + LIST_FOREACH(items, i, first) + c += i->length * 4 + 3; + + p = s = new(char, c); + if (!s) + return NULL; + + LIST_FOREACH(items, i, first) { + if (i != first) + *(p++) = ' '; + + *(p++) = '"'; + + for (size_t j = 0; j < i->length; j++) { + if (i->data[j] < ' ' || i->data[j] == '"' || i->data[j] >= 127) { + *(p++) = '\\'; + *(p++) = '0' + (i->data[j] / 100); + *(p++) = '0' + ((i->data[j] / 10) % 10); + *(p++) = '0' + (i->data[j] % 10); + } else + *(p++) = i->data[j]; + } + + *(p++) = '"'; + } + + *p = 0; + return s; +} + +const char *dns_resource_record_to_string(DnsResourceRecord *rr) { + _cleanup_free_ char *s = NULL, *t = NULL; + char k[DNS_RESOURCE_KEY_STRING_MAX]; + int r; + + assert(rr); + + if (rr->to_string) + return rr->to_string; + + dns_resource_key_to_string(rr->key, k, sizeof(k)); + + switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) { + + case DNS_TYPE_SRV: + r = asprintf(&s, "%s %u %u %u %s", + k, + rr->srv.priority, + rr->srv.weight, + rr->srv.port, + strna(rr->srv.name)); + if (r < 0) + return NULL; + break; + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + s = strjoin(k, " ", rr->ptr.name); + if (!s) + return NULL; + + break; + + case DNS_TYPE_HINFO: + s = strjoin(k, " ", rr->hinfo.cpu, " ", rr->hinfo.os); + if (!s) + return NULL; + break; + + case DNS_TYPE_SPF: /* exactly the same as TXT */ + case DNS_TYPE_TXT: + t = format_txt(rr->txt.items); + if (!t) + return NULL; + + s = strjoin(k, " ", t); + if (!s) + return NULL; + break; + + case DNS_TYPE_A: + r = in_addr_to_string(AF_INET, (const union in_addr_union*) &rr->a.in_addr, &t); + if (r < 0) + return NULL; + + s = strjoin(k, " ", t); + if (!s) + return NULL; + break; + + case DNS_TYPE_AAAA: + r = in_addr_to_string(AF_INET6, (const union in_addr_union*) &rr->aaaa.in6_addr, &t); + if (r < 0) + return NULL; + + s = strjoin(k, " ", t); + if (!s) + return NULL; + break; + + case DNS_TYPE_SOA: + r = asprintf(&s, "%s %s %s %u %u %u %u %u", + k, + strna(rr->soa.mname), + strna(rr->soa.rname), + rr->soa.serial, + rr->soa.refresh, + rr->soa.retry, + rr->soa.expire, + rr->soa.minimum); + if (r < 0) + return NULL; + break; + + case DNS_TYPE_MX: + r = asprintf(&s, "%s %u %s", + k, + rr->mx.priority, + rr->mx.exchange); + if (r < 0) + return NULL; + break; + + case DNS_TYPE_LOC: + assert(rr->loc.version == 0); + + t = format_location(rr->loc.latitude, + rr->loc.longitude, + rr->loc.altitude, + rr->loc.size, + rr->loc.horiz_pre, + rr->loc.vert_pre); + if (!t) + return NULL; + + s = strjoin(k, " ", t); + if (!s) + return NULL; + break; + + case DNS_TYPE_DS: + t = hexmem(rr->ds.digest, rr->ds.digest_size); + if (!t) + return NULL; + + r = asprintf(&s, "%s %u %u %u %s", + k, + rr->ds.key_tag, + rr->ds.algorithm, + rr->ds.digest_type, + t); + if (r < 0) + return NULL; + break; + + case DNS_TYPE_SSHFP: + t = hexmem(rr->sshfp.fingerprint, rr->sshfp.fingerprint_size); + if (!t) + return NULL; + + r = asprintf(&s, "%s %u %u %s", + k, + rr->sshfp.algorithm, + rr->sshfp.fptype, + t); + if (r < 0) + return NULL; + break; + + case DNS_TYPE_DNSKEY: { + _cleanup_free_ char *alg = NULL; + uint16_t key_tag; + + key_tag = dnssec_keytag(rr, true); + + r = dnssec_algorithm_to_string_alloc(rr->dnskey.algorithm, &alg); + if (r < 0) + return NULL; + + r = asprintf(&t, "%s %u %u %s", + k, + rr->dnskey.flags, + rr->dnskey.protocol, + alg); + if (r < 0) + return NULL; + + r = base64_append(&t, r, + rr->dnskey.key, rr->dnskey.key_size, + 8, columns()); + if (r < 0) + return NULL; + + r = asprintf(&s, "%s\n" + " -- Flags:%s%s%s\n" + " -- Key tag: %u", + t, + rr->dnskey.flags & DNSKEY_FLAG_SEP ? " SEP" : "", + rr->dnskey.flags & DNSKEY_FLAG_REVOKE ? " REVOKE" : "", + rr->dnskey.flags & DNSKEY_FLAG_ZONE_KEY ? " ZONE_KEY" : "", + key_tag); + if (r < 0) + return NULL; + + break; + } + + case DNS_TYPE_RRSIG: { + _cleanup_free_ char *alg = NULL; + char expiration[STRLEN("YYYYMMDDHHmmSS") + 1], inception[STRLEN("YYYYMMDDHHmmSS") + 1]; + const char *type; + + type = dns_type_to_string(rr->rrsig.type_covered); + + r = dnssec_algorithm_to_string_alloc(rr->rrsig.algorithm, &alg); + if (r < 0) + return NULL; + + r = format_timestamp_dns(expiration, sizeof(expiration), rr->rrsig.expiration); + if (r < 0) + return NULL; + + r = format_timestamp_dns(inception, sizeof(inception), rr->rrsig.inception); + if (r < 0) + return NULL; + + /* TYPE?? follows + * http://tools.ietf.org/html/rfc3597#section-5 */ + + r = asprintf(&s, "%s %s%.*u %s %u %u %s %s %u %s", + k, + type ?: "TYPE", + type ? 0 : 1, type ? 0u : (unsigned) rr->rrsig.type_covered, + alg, + rr->rrsig.labels, + rr->rrsig.original_ttl, + expiration, + inception, + rr->rrsig.key_tag, + rr->rrsig.signer); + if (r < 0) + return NULL; + + r = base64_append(&s, r, + rr->rrsig.signature, rr->rrsig.signature_size, + 8, columns()); + if (r < 0) + return NULL; + + break; + } + + case DNS_TYPE_NSEC: + t = format_types(rr->nsec.types); + if (!t) + return NULL; + + r = asprintf(&s, "%s %s %s", + k, + rr->nsec.next_domain_name, + t); + if (r < 0) + return NULL; + break; + + case DNS_TYPE_NSEC3: { + _cleanup_free_ char *salt = NULL, *hash = NULL; + + if (rr->nsec3.salt_size > 0) { + salt = hexmem(rr->nsec3.salt, rr->nsec3.salt_size); + if (!salt) + return NULL; + } + + hash = base32hexmem(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size, false); + if (!hash) + return NULL; + + t = format_types(rr->nsec3.types); + if (!t) + return NULL; + + r = asprintf(&s, "%s %"PRIu8" %"PRIu8" %"PRIu16" %s %s %s", + k, + rr->nsec3.algorithm, + rr->nsec3.flags, + rr->nsec3.iterations, + rr->nsec3.salt_size > 0 ? salt : "-", + hash, + t); + if (r < 0) + return NULL; + + break; + } + + case DNS_TYPE_TLSA: + t = hexmem(rr->tlsa.data, rr->tlsa.data_size); + if (!t) + return NULL; + + r = asprintf(&s, + "%s %u %u %u %s\n" + " -- Cert. usage: %s\n" + " -- Selector: %s\n" + " -- Matching type: %s", + k, + rr->tlsa.cert_usage, + rr->tlsa.selector, + rr->tlsa.matching_type, + t, + tlsa_cert_usage_to_string(rr->tlsa.cert_usage), + tlsa_selector_to_string(rr->tlsa.selector), + tlsa_matching_type_to_string(rr->tlsa.matching_type)); + if (r < 0) + return NULL; + + break; + + case DNS_TYPE_CAA: + t = octescape(rr->caa.value, rr->caa.value_size); + if (!t) + return NULL; + + r = asprintf(&s, "%s %u %s \"%s\"%s%s%s%.0u", + k, + rr->caa.flags, + rr->caa.tag, + t, + rr->caa.flags ? "\n -- Flags:" : "", + rr->caa.flags & CAA_FLAG_CRITICAL ? " critical" : "", + rr->caa.flags & ~CAA_FLAG_CRITICAL ? " " : "", + rr->caa.flags & ~CAA_FLAG_CRITICAL); + if (r < 0) + return NULL; + + break; + + case DNS_TYPE_OPENPGPKEY: + r = asprintf(&s, "%s", k); + if (r < 0) + return NULL; + + r = base64_append(&s, r, + rr->generic.data, rr->generic.data_size, + 8, columns()); + if (r < 0) + return NULL; + break; + + default: + /* Format as documented in RFC 3597, Section 5 */ + if (rr->generic.data_size == 0) + r = asprintf(&s, "%s \\# 0", k); + else { + t = hexmem(rr->generic.data, rr->generic.data_size); + if (!t) + return NULL; + r = asprintf(&s, "%s \\# %zu %s", k, rr->generic.data_size, t); + } + if (r < 0) + return NULL; + break; + } + + rr->to_string = s; + return TAKE_PTR(s); +} + +ssize_t dns_resource_record_payload(DnsResourceRecord *rr, void **out) { + assert(rr); + assert(out); + + switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) { + case DNS_TYPE_SRV: + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + case DNS_TYPE_HINFO: + case DNS_TYPE_SPF: + case DNS_TYPE_TXT: + case DNS_TYPE_A: + case DNS_TYPE_AAAA: + case DNS_TYPE_SOA: + case DNS_TYPE_MX: + case DNS_TYPE_LOC: + case DNS_TYPE_DS: + case DNS_TYPE_DNSKEY: + case DNS_TYPE_RRSIG: + case DNS_TYPE_NSEC: + case DNS_TYPE_NSEC3: + return -EINVAL; + + case DNS_TYPE_SSHFP: + *out = rr->sshfp.fingerprint; + return rr->sshfp.fingerprint_size; + + case DNS_TYPE_TLSA: + *out = rr->tlsa.data; + return rr->tlsa.data_size; + + case DNS_TYPE_OPENPGPKEY: + default: + *out = rr->generic.data; + return rr->generic.data_size; + } +} + +int dns_resource_record_to_wire_format(DnsResourceRecord *rr, bool canonical) { + + DnsPacket packet = { + .n_ref = 1, + .protocol = DNS_PROTOCOL_DNS, + .on_stack = true, + .refuse_compression = true, + .canonical_form = canonical, + }; + + size_t start, rds; + int r; + + assert(rr); + + /* Generates the RR in wire-format, optionally in the + * canonical form as discussed in the DNSSEC RFC 4034, Section + * 6.2. We allocate a throw-away DnsPacket object on the stack + * here, because we need some book-keeping for memory + * management, and can reuse the DnsPacket serializer, that + * can generate the canonical form, too, but also knows label + * compression and suchlike. */ + + if (rr->wire_format && rr->wire_format_canonical == canonical) + return 0; + + r = dns_packet_append_rr(&packet, rr, 0, &start, &rds); + if (r < 0) { + dns_packet_unref(&packet); + return r; + } + + assert(start == 0); + assert(packet._data); + + free(rr->wire_format); + rr->wire_format = packet._data; + rr->wire_format_size = packet.size; + rr->wire_format_rdata_offset = rds; + rr->wire_format_canonical = canonical; + + packet._data = NULL; + dns_packet_unref(&packet); + + return 0; +} + +int dns_resource_record_signer(DnsResourceRecord *rr, const char **ret) { + const char *n; + int r; + + assert(rr); + assert(ret); + + /* Returns the RRset's signer, if it is known. */ + + if (rr->n_skip_labels_signer == UINT8_MAX) + return -ENODATA; + + n = dns_resource_key_name(rr->key); + r = dns_name_skip(n, rr->n_skip_labels_signer, &n); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + *ret = n; + return 0; +} + +int dns_resource_record_source(DnsResourceRecord *rr, const char **ret) { + const char *n; + int r; + + assert(rr); + assert(ret); + + /* Returns the RRset's synthesizing source, if it is known. */ + + if (rr->n_skip_labels_source == UINT8_MAX) + return -ENODATA; + + n = dns_resource_key_name(rr->key); + r = dns_name_skip(n, rr->n_skip_labels_source, &n); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + + *ret = n; + return 0; +} + +int dns_resource_record_is_signer(DnsResourceRecord *rr, const char *zone) { + const char *signer; + int r; + + assert(rr); + + r = dns_resource_record_signer(rr, &signer); + if (r < 0) + return r; + + return dns_name_equal(zone, signer); +} + +int dns_resource_record_is_synthetic(DnsResourceRecord *rr) { + int r; + + assert(rr); + + /* Returns > 0 if the RR is generated from a wildcard, and is not the asterisk name itself */ + + if (rr->n_skip_labels_source == UINT8_MAX) + return -ENODATA; + + if (rr->n_skip_labels_source == 0) + return 0; + + if (rr->n_skip_labels_source > 1) + return 1; + + r = dns_name_startswith(dns_resource_key_name(rr->key), "*"); + if (r < 0) + return r; + + return !r; +} + +void dns_resource_record_hash_func(const DnsResourceRecord *rr, struct siphash *state) { + assert(rr); + + dns_resource_key_hash_func(rr->key, state); + + switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) { + + case DNS_TYPE_SRV: + siphash24_compress(&rr->srv.priority, sizeof(rr->srv.priority), state); + siphash24_compress(&rr->srv.weight, sizeof(rr->srv.weight), state); + siphash24_compress(&rr->srv.port, sizeof(rr->srv.port), state); + dns_name_hash_func(rr->srv.name, state); + break; + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + dns_name_hash_func(rr->ptr.name, state); + break; + + case DNS_TYPE_HINFO: + string_hash_func(rr->hinfo.cpu, state); + string_hash_func(rr->hinfo.os, state); + break; + + case DNS_TYPE_TXT: + case DNS_TYPE_SPF: { + LIST_FOREACH(items, j, rr->txt.items) { + siphash24_compress_safe(j->data, j->length, state); + + /* Add an extra NUL byte, so that "a" followed by "b" doesn't result in the same hash as "ab" + * followed by "". */ + siphash24_compress_byte(0, state); + } + break; + } + + case DNS_TYPE_A: + siphash24_compress(&rr->a.in_addr, sizeof(rr->a.in_addr), state); + break; + + case DNS_TYPE_AAAA: + siphash24_compress(&rr->aaaa.in6_addr, sizeof(rr->aaaa.in6_addr), state); + break; + + case DNS_TYPE_SOA: + dns_name_hash_func(rr->soa.mname, state); + dns_name_hash_func(rr->soa.rname, state); + siphash24_compress(&rr->soa.serial, sizeof(rr->soa.serial), state); + siphash24_compress(&rr->soa.refresh, sizeof(rr->soa.refresh), state); + siphash24_compress(&rr->soa.retry, sizeof(rr->soa.retry), state); + siphash24_compress(&rr->soa.expire, sizeof(rr->soa.expire), state); + siphash24_compress(&rr->soa.minimum, sizeof(rr->soa.minimum), state); + break; + + case DNS_TYPE_MX: + siphash24_compress(&rr->mx.priority, sizeof(rr->mx.priority), state); + dns_name_hash_func(rr->mx.exchange, state); + break; + + case DNS_TYPE_LOC: + siphash24_compress(&rr->loc.version, sizeof(rr->loc.version), state); + siphash24_compress(&rr->loc.size, sizeof(rr->loc.size), state); + siphash24_compress(&rr->loc.horiz_pre, sizeof(rr->loc.horiz_pre), state); + siphash24_compress(&rr->loc.vert_pre, sizeof(rr->loc.vert_pre), state); + siphash24_compress(&rr->loc.latitude, sizeof(rr->loc.latitude), state); + siphash24_compress(&rr->loc.longitude, sizeof(rr->loc.longitude), state); + siphash24_compress(&rr->loc.altitude, sizeof(rr->loc.altitude), state); + break; + + case DNS_TYPE_SSHFP: + siphash24_compress(&rr->sshfp.algorithm, sizeof(rr->sshfp.algorithm), state); + siphash24_compress(&rr->sshfp.fptype, sizeof(rr->sshfp.fptype), state); + siphash24_compress_safe(rr->sshfp.fingerprint, rr->sshfp.fingerprint_size, state); + break; + + case DNS_TYPE_DNSKEY: + siphash24_compress(&rr->dnskey.flags, sizeof(rr->dnskey.flags), state); + siphash24_compress(&rr->dnskey.protocol, sizeof(rr->dnskey.protocol), state); + siphash24_compress(&rr->dnskey.algorithm, sizeof(rr->dnskey.algorithm), state); + siphash24_compress_safe(rr->dnskey.key, rr->dnskey.key_size, state); + break; + + case DNS_TYPE_RRSIG: + siphash24_compress(&rr->rrsig.type_covered, sizeof(rr->rrsig.type_covered), state); + siphash24_compress(&rr->rrsig.algorithm, sizeof(rr->rrsig.algorithm), state); + siphash24_compress(&rr->rrsig.labels, sizeof(rr->rrsig.labels), state); + siphash24_compress(&rr->rrsig.original_ttl, sizeof(rr->rrsig.original_ttl), state); + siphash24_compress(&rr->rrsig.expiration, sizeof(rr->rrsig.expiration), state); + siphash24_compress(&rr->rrsig.inception, sizeof(rr->rrsig.inception), state); + siphash24_compress(&rr->rrsig.key_tag, sizeof(rr->rrsig.key_tag), state); + dns_name_hash_func(rr->rrsig.signer, state); + siphash24_compress_safe(rr->rrsig.signature, rr->rrsig.signature_size, state); + break; + + case DNS_TYPE_NSEC: + dns_name_hash_func(rr->nsec.next_domain_name, state); + /* FIXME: we leave out the type bitmap here. Hash + * would be better if we'd take it into account + * too. */ + break; + + case DNS_TYPE_DS: + siphash24_compress(&rr->ds.key_tag, sizeof(rr->ds.key_tag), state); + siphash24_compress(&rr->ds.algorithm, sizeof(rr->ds.algorithm), state); + siphash24_compress(&rr->ds.digest_type, sizeof(rr->ds.digest_type), state); + siphash24_compress_safe(rr->ds.digest, rr->ds.digest_size, state); + break; + + case DNS_TYPE_NSEC3: + siphash24_compress(&rr->nsec3.algorithm, sizeof(rr->nsec3.algorithm), state); + siphash24_compress(&rr->nsec3.flags, sizeof(rr->nsec3.flags), state); + siphash24_compress(&rr->nsec3.iterations, sizeof(rr->nsec3.iterations), state); + siphash24_compress_safe(rr->nsec3.salt, rr->nsec3.salt_size, state); + siphash24_compress_safe(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size, state); + /* FIXME: We leave the bitmaps out */ + break; + + case DNS_TYPE_TLSA: + siphash24_compress(&rr->tlsa.cert_usage, sizeof(rr->tlsa.cert_usage), state); + siphash24_compress(&rr->tlsa.selector, sizeof(rr->tlsa.selector), state); + siphash24_compress(&rr->tlsa.matching_type, sizeof(rr->tlsa.matching_type), state); + siphash24_compress_safe(rr->tlsa.data, rr->tlsa.data_size, state); + break; + + case DNS_TYPE_CAA: + siphash24_compress(&rr->caa.flags, sizeof(rr->caa.flags), state); + string_hash_func(rr->caa.tag, state); + siphash24_compress_safe(rr->caa.value, rr->caa.value_size, state); + break; + + case DNS_TYPE_OPENPGPKEY: + default: + siphash24_compress_safe(rr->generic.data, rr->generic.data_size, state); + break; + } +} + +int dns_resource_record_compare_func(const DnsResourceRecord *x, const DnsResourceRecord *y) { + int r; + + r = dns_resource_key_compare_func(x->key, y->key); + if (r != 0) + return r; + + if (dns_resource_record_payload_equal(x, y) > 0) + return 0; + + /* We still use CMP() here, even though don't implement proper + * ordering, since the hashtable doesn't need ordering anyway. */ + return CMP(x, y); +} + +DEFINE_HASH_OPS(dns_resource_record_hash_ops, DnsResourceRecord, dns_resource_record_hash_func, dns_resource_record_compare_func); + +DnsResourceRecord *dns_resource_record_copy(DnsResourceRecord *rr) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *copy = NULL; + DnsResourceRecord *t; + + assert(rr); + + copy = dns_resource_record_new(rr->key); + if (!copy) + return NULL; + + copy->ttl = rr->ttl; + copy->expiry = rr->expiry; + copy->n_skip_labels_signer = rr->n_skip_labels_signer; + copy->n_skip_labels_source = rr->n_skip_labels_source; + copy->unparsable = rr->unparsable; + + switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) { + + case DNS_TYPE_SRV: + copy->srv.priority = rr->srv.priority; + copy->srv.weight = rr->srv.weight; + copy->srv.port = rr->srv.port; + copy->srv.name = strdup(rr->srv.name); + if (!copy->srv.name) + return NULL; + break; + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + copy->ptr.name = strdup(rr->ptr.name); + if (!copy->ptr.name) + return NULL; + break; + + case DNS_TYPE_HINFO: + copy->hinfo.cpu = strdup(rr->hinfo.cpu); + if (!copy->hinfo.cpu) + return NULL; + + copy->hinfo.os = strdup(rr->hinfo.os); + if (!copy->hinfo.os) + return NULL; + break; + + case DNS_TYPE_TXT: + case DNS_TYPE_SPF: + copy->txt.items = dns_txt_item_copy(rr->txt.items); + if (!copy->txt.items) + return NULL; + break; + + case DNS_TYPE_A: + copy->a = rr->a; + break; + + case DNS_TYPE_AAAA: + copy->aaaa = rr->aaaa; + break; + + case DNS_TYPE_SOA: + copy->soa.mname = strdup(rr->soa.mname); + if (!copy->soa.mname) + return NULL; + copy->soa.rname = strdup(rr->soa.rname); + if (!copy->soa.rname) + return NULL; + copy->soa.serial = rr->soa.serial; + copy->soa.refresh = rr->soa.refresh; + copy->soa.retry = rr->soa.retry; + copy->soa.expire = rr->soa.expire; + copy->soa.minimum = rr->soa.minimum; + break; + + case DNS_TYPE_MX: + copy->mx.priority = rr->mx.priority; + copy->mx.exchange = strdup(rr->mx.exchange); + if (!copy->mx.exchange) + return NULL; + break; + + case DNS_TYPE_LOC: + copy->loc = rr->loc; + break; + + case DNS_TYPE_SSHFP: + copy->sshfp.algorithm = rr->sshfp.algorithm; + copy->sshfp.fptype = rr->sshfp.fptype; + copy->sshfp.fingerprint = memdup(rr->sshfp.fingerprint, rr->sshfp.fingerprint_size); + if (!copy->sshfp.fingerprint) + return NULL; + copy->sshfp.fingerprint_size = rr->sshfp.fingerprint_size; + break; + + case DNS_TYPE_DNSKEY: + copy->dnskey.flags = rr->dnskey.flags; + copy->dnskey.protocol = rr->dnskey.protocol; + copy->dnskey.algorithm = rr->dnskey.algorithm; + copy->dnskey.key = memdup(rr->dnskey.key, rr->dnskey.key_size); + if (!copy->dnskey.key) + return NULL; + copy->dnskey.key_size = rr->dnskey.key_size; + break; + + case DNS_TYPE_RRSIG: + copy->rrsig.type_covered = rr->rrsig.type_covered; + copy->rrsig.algorithm = rr->rrsig.algorithm; + copy->rrsig.labels = rr->rrsig.labels; + copy->rrsig.original_ttl = rr->rrsig.original_ttl; + copy->rrsig.expiration = rr->rrsig.expiration; + copy->rrsig.inception = rr->rrsig.inception; + copy->rrsig.key_tag = rr->rrsig.key_tag; + copy->rrsig.signer = strdup(rr->rrsig.signer); + if (!copy->rrsig.signer) + return NULL; + copy->rrsig.signature = memdup(rr->rrsig.signature, rr->rrsig.signature_size); + if (!copy->rrsig.signature) + return NULL; + copy->rrsig.signature_size = rr->rrsig.signature_size; + break; + + case DNS_TYPE_NSEC: + copy->nsec.next_domain_name = strdup(rr->nsec.next_domain_name); + if (!copy->nsec.next_domain_name) + return NULL; + if (rr->nsec.types) { + copy->nsec.types = bitmap_copy(rr->nsec.types); + if (!copy->nsec.types) + return NULL; + } + break; + + case DNS_TYPE_DS: + copy->ds.key_tag = rr->ds.key_tag; + copy->ds.algorithm = rr->ds.algorithm; + copy->ds.digest_type = rr->ds.digest_type; + copy->ds.digest = memdup(rr->ds.digest, rr->ds.digest_size); + if (!copy->ds.digest) + return NULL; + copy->ds.digest_size = rr->ds.digest_size; + break; + + case DNS_TYPE_NSEC3: + copy->nsec3.algorithm = rr->nsec3.algorithm; + copy->nsec3.flags = rr->nsec3.flags; + copy->nsec3.iterations = rr->nsec3.iterations; + copy->nsec3.salt = memdup(rr->nsec3.salt, rr->nsec3.salt_size); + if (!copy->nsec3.salt) + return NULL; + copy->nsec3.salt_size = rr->nsec3.salt_size; + copy->nsec3.next_hashed_name = memdup(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size); + if (!copy->nsec3.next_hashed_name) + return NULL; + copy->nsec3.next_hashed_name_size = rr->nsec3.next_hashed_name_size; + if (rr->nsec3.types) { + copy->nsec3.types = bitmap_copy(rr->nsec3.types); + if (!copy->nsec3.types) + return NULL; + } + break; + + case DNS_TYPE_TLSA: + copy->tlsa.cert_usage = rr->tlsa.cert_usage; + copy->tlsa.selector = rr->tlsa.selector; + copy->tlsa.matching_type = rr->tlsa.matching_type; + copy->tlsa.data = memdup(rr->tlsa.data, rr->tlsa.data_size); + if (!copy->tlsa.data) + return NULL; + copy->tlsa.data_size = rr->tlsa.data_size; + break; + + case DNS_TYPE_CAA: + copy->caa.flags = rr->caa.flags; + copy->caa.tag = strdup(rr->caa.tag); + if (!copy->caa.tag) + return NULL; + copy->caa.value = memdup(rr->caa.value, rr->caa.value_size); + if (!copy->caa.value) + return NULL; + copy->caa.value_size = rr->caa.value_size; + break; + + case DNS_TYPE_OPT: + default: + copy->generic.data = memdup(rr->generic.data, rr->generic.data_size); + if (!copy->generic.data) + return NULL; + copy->generic.data_size = rr->generic.data_size; + break; + } + + t = TAKE_PTR(copy); + + return t; +} + +int dns_resource_record_clamp_ttl(DnsResourceRecord **rr, uint32_t max_ttl) { + DnsResourceRecord *old_rr, *new_rr; + uint32_t new_ttl; + + assert(rr); + old_rr = *rr; + + if (old_rr->key->type == DNS_TYPE_OPT) + return -EINVAL; + + new_ttl = MIN(old_rr->ttl, max_ttl); + if (new_ttl == old_rr->ttl) + return 0; + + if (old_rr->n_ref == 1) { + /* Patch in place */ + old_rr->ttl = new_ttl; + return 1; + } + + new_rr = dns_resource_record_copy(old_rr); + if (!new_rr) + return -ENOMEM; + + new_rr->ttl = new_ttl; + + DNS_RR_REPLACE(*rr, new_rr); + return 1; +} + +bool dns_resource_record_is_link_local_address(DnsResourceRecord *rr) { + assert(rr); + + if (rr->key->class != DNS_CLASS_IN) + return false; + + if (rr->key->type == DNS_TYPE_A) + return in4_addr_is_link_local(&rr->a.in_addr); + + if (rr->key->type == DNS_TYPE_AAAA) + return in6_addr_is_link_local(&rr->aaaa.in6_addr); + + return false; +} + +int dns_resource_record_get_cname_target(DnsResourceKey *key, DnsResourceRecord *cname, char **ret) { + _cleanup_free_ char *d = NULL; + int r; + + assert(key); + assert(cname); + + /* Checks if the RR `cname` is a CNAME/DNAME RR that matches the specified `key`. If so, returns the + * target domain. If not, returns -EUNATCH */ + + if (key->class != cname->key->class && key->class != DNS_CLASS_ANY) + return -EUNATCH; + + if (!dns_type_may_redirect(key->type)) /* This key type is not subject to CNAME/DNAME redirection? + * Then let's refuse right-away */ + return -EUNATCH; + + if (cname->key->type == DNS_TYPE_CNAME) { + r = dns_name_equal(dns_resource_key_name(key), + dns_resource_key_name(cname->key)); + if (r < 0) + return r; + if (r == 0) + return -EUNATCH; /* CNAME RR key doesn't actually match the original key */ + + d = strdup(cname->cname.name); + if (!d) + return -ENOMEM; + + } else if (cname->key->type == DNS_TYPE_DNAME) { + + r = dns_name_change_suffix( + dns_resource_key_name(key), + dns_resource_key_name(cname->key), + cname->dname.name, + &d); + if (r < 0) + return r; + if (r == 0) + return -EUNATCH; /* DNAME RR key doesn't actually match the original key */ + + } else + return -EUNATCH; /* Not a CNAME/DNAME RR, hence doesn't match the proposition either */ + + *ret = TAKE_PTR(d); + return 0; +} + +DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *first) { + LIST_FOREACH(items, i, first) + free(i); + + return NULL; +} + +bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b) { + DnsTxtItem *bb = b; + + if (a == b) + return true; + + LIST_FOREACH(items, aa, a) { + if (!bb) + return false; + + if (memcmp_nn(aa->data, aa->length, bb->data, bb->length) != 0) + return false; + + bb = bb->items_next; + } + + return !bb; +} + +DnsTxtItem *dns_txt_item_copy(DnsTxtItem *first) { + DnsTxtItem *copy = NULL, *end = NULL; + + LIST_FOREACH(items, i, first) { + DnsTxtItem *j; + + j = memdup(i, offsetof(DnsTxtItem, data) + i->length + 1); + if (!j) { + dns_txt_item_free_all(copy); + return NULL; + } + + LIST_INSERT_AFTER(items, copy, end, j); + end = j; + } + + return copy; +} + +int dns_txt_item_new_empty(DnsTxtItem **ret) { + DnsTxtItem *i; + + /* RFC 6763, section 6.1 suggests to treat + * empty TXT RRs as equivalent to a TXT record + * with a single empty string. */ + + i = malloc0(offsetof(DnsTxtItem, data) + 1); /* for safety reasons we add an extra NUL byte */ + if (!i) + return -ENOMEM; + + *ret = i; + + return 0; +} + +int dns_resource_record_new_from_raw(DnsResourceRecord **ret, const void *data, size_t size) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + int r; + + r = dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX); + if (r < 0) + return r; + + p->refuse_compression = true; + + r = dns_packet_append_blob(p, data, size, NULL); + if (r < 0) + return r; + + return dns_packet_read_rr(p, ret, NULL, NULL); +} + +int dns_resource_key_to_json(DnsResourceKey *key, JsonVariant **ret) { + assert(key); + assert(ret); + + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("class", JSON_BUILD_INTEGER(key->class)), + JSON_BUILD_PAIR("type", JSON_BUILD_INTEGER(key->type)), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(dns_resource_key_name(key))))); +} + +static int type_bitmap_to_json(Bitmap *b, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; + unsigned t; + int r; + + assert(ret); + + BITMAP_FOREACH(t, b) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + r = json_variant_new_unsigned(&v, t); + if (r < 0) + return r; + + r = json_variant_append_array(&l, v); + if (r < 0) + return r; + } + + if (!l) + return json_variant_new_array(ret, NULL, 0); + + *ret = TAKE_PTR(l); + return 0; +} + +static int txt_to_json(DnsTxtItem *items, JsonVariant **ret) { + JsonVariant **elements = NULL; + size_t n = 0; + int r; + + assert(ret); + + LIST_FOREACH(items, i, items) { + if (!GREEDY_REALLOC(elements, n + 1)) { + r = -ENOMEM; + goto finalize; + } + + r = json_variant_new_octescape(elements + n, i->data, i->length); + if (r < 0) + goto finalize; + + n++; + } + + r = json_variant_new_array(ret, elements, n); + +finalize: + for (size_t i = 0; i < n; i++) + json_variant_unref(elements[i]); + + free(elements); + return r; +} + +int dns_resource_record_to_json(DnsResourceRecord *rr, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *k = NULL; + int r; + + assert(rr); + assert(ret); + + r = dns_resource_key_to_json(rr->key, &k); + if (r < 0) + return r; + + switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) { + + case DNS_TYPE_SRV: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("priority", JSON_BUILD_UNSIGNED(rr->srv.priority)), + JSON_BUILD_PAIR("weight", JSON_BUILD_UNSIGNED(rr->srv.weight)), + JSON_BUILD_PAIR("port", JSON_BUILD_UNSIGNED(rr->srv.port)), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(rr->srv.name)))); + + case DNS_TYPE_PTR: + case DNS_TYPE_NS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(rr->ptr.name)))); + + case DNS_TYPE_HINFO: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("cpu", JSON_BUILD_STRING(rr->hinfo.cpu)), + JSON_BUILD_PAIR("os", JSON_BUILD_STRING(rr->hinfo.os)))); + + case DNS_TYPE_SPF: + case DNS_TYPE_TXT: { + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; + + r = txt_to_json(rr->txt.items, &l); + if (r < 0) + return r; + + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("items", JSON_BUILD_VARIANT(l)))); + } + + case DNS_TYPE_A: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("address", JSON_BUILD_IN4_ADDR(&rr->a.in_addr)))); + + case DNS_TYPE_AAAA: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("address", JSON_BUILD_IN6_ADDR(&rr->aaaa.in6_addr)))); + + case DNS_TYPE_SOA: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("mname", JSON_BUILD_STRING(rr->soa.mname)), + JSON_BUILD_PAIR("rname", JSON_BUILD_STRING(rr->soa.rname)), + JSON_BUILD_PAIR("serial", JSON_BUILD_UNSIGNED(rr->soa.serial)), + JSON_BUILD_PAIR("refresh", JSON_BUILD_UNSIGNED(rr->soa.refresh)), + JSON_BUILD_PAIR("expire", JSON_BUILD_UNSIGNED(rr->soa.retry)), + JSON_BUILD_PAIR("minimum", JSON_BUILD_UNSIGNED(rr->soa.minimum)))); + + case DNS_TYPE_MX: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("priority", JSON_BUILD_UNSIGNED(rr->mx.priority)), + JSON_BUILD_PAIR("exchange", JSON_BUILD_STRING(rr->mx.exchange)))); + case DNS_TYPE_LOC: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("version", JSON_BUILD_UNSIGNED(rr->loc.version)), + JSON_BUILD_PAIR("size", JSON_BUILD_UNSIGNED(rr->loc.size)), + JSON_BUILD_PAIR("horiz_pre", JSON_BUILD_UNSIGNED(rr->loc.horiz_pre)), + JSON_BUILD_PAIR("vert_pre", JSON_BUILD_UNSIGNED(rr->loc.vert_pre)), + JSON_BUILD_PAIR("latitude", JSON_BUILD_UNSIGNED(rr->loc.latitude)), + JSON_BUILD_PAIR("longitude", JSON_BUILD_UNSIGNED(rr->loc.longitude)), + JSON_BUILD_PAIR("altitude", JSON_BUILD_UNSIGNED(rr->loc.altitude)))); + + case DNS_TYPE_DS: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("keyTag", JSON_BUILD_UNSIGNED(rr->ds.key_tag)), + JSON_BUILD_PAIR("algorithm", JSON_BUILD_UNSIGNED(rr->ds.algorithm)), + JSON_BUILD_PAIR("digestType", JSON_BUILD_UNSIGNED(rr->ds.digest_type)), + JSON_BUILD_PAIR("digest", JSON_BUILD_HEX(rr->ds.digest, rr->ds.digest_size)))); + + case DNS_TYPE_SSHFP: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("algorithm", JSON_BUILD_UNSIGNED(rr->sshfp.algorithm)), + JSON_BUILD_PAIR("fptype", JSON_BUILD_UNSIGNED(rr->sshfp.fptype)), + JSON_BUILD_PAIR("fingerprint", JSON_BUILD_HEX(rr->sshfp.fingerprint, rr->sshfp.fingerprint_size)))); + + case DNS_TYPE_DNSKEY: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("flags", JSON_BUILD_UNSIGNED(rr->dnskey.flags)), + JSON_BUILD_PAIR("protocol", JSON_BUILD_UNSIGNED(rr->dnskey.protocol)), + JSON_BUILD_PAIR("algorithm", JSON_BUILD_UNSIGNED(rr->dnskey.algorithm)), + JSON_BUILD_PAIR("dnskey", JSON_BUILD_BASE64(rr->dnskey.key, rr->dnskey.key_size)))); + + + case DNS_TYPE_RRSIG: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("signer", JSON_BUILD_STRING(rr->rrsig.signer)), + JSON_BUILD_PAIR("typeCovered", JSON_BUILD_UNSIGNED(rr->rrsig.type_covered)), + JSON_BUILD_PAIR("algorithm", JSON_BUILD_UNSIGNED(rr->rrsig.algorithm)), + JSON_BUILD_PAIR("labels", JSON_BUILD_UNSIGNED(rr->rrsig.labels)), + JSON_BUILD_PAIR("originalTtl", JSON_BUILD_UNSIGNED(rr->rrsig.original_ttl)), + JSON_BUILD_PAIR("expiration", JSON_BUILD_UNSIGNED(rr->rrsig.expiration)), + JSON_BUILD_PAIR("inception", JSON_BUILD_UNSIGNED(rr->rrsig.inception)), + JSON_BUILD_PAIR("keyTag", JSON_BUILD_UNSIGNED(rr->rrsig.key_tag)), + JSON_BUILD_PAIR("signature", JSON_BUILD_BASE64(rr->rrsig.signature, rr->rrsig.signature_size)))); + + case DNS_TYPE_NSEC: { + _cleanup_(json_variant_unrefp) JsonVariant *bm = NULL; + + r = type_bitmap_to_json(rr->nsec.types, &bm); + if (r < 0) + return r; + + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("nextDomain", JSON_BUILD_STRING(rr->nsec.next_domain_name)), + JSON_BUILD_PAIR("types", JSON_BUILD_VARIANT(bm)))); + } + + case DNS_TYPE_NSEC3: { + _cleanup_(json_variant_unrefp) JsonVariant *bm = NULL; + + r = type_bitmap_to_json(rr->nsec3.types, &bm); + if (r < 0) + return r; + + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("algorithm", JSON_BUILD_UNSIGNED(rr->nsec3.algorithm)), + JSON_BUILD_PAIR("flags", JSON_BUILD_UNSIGNED(rr->nsec3.flags)), + JSON_BUILD_PAIR("iterations", JSON_BUILD_UNSIGNED(rr->nsec3.iterations)), + JSON_BUILD_PAIR("salt", JSON_BUILD_HEX(rr->nsec3.salt, rr->nsec3.salt_size)), + JSON_BUILD_PAIR("hash", JSON_BUILD_BASE32HEX(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size)), + JSON_BUILD_PAIR("types", JSON_BUILD_VARIANT(bm)))); + } + + case DNS_TYPE_TLSA: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("certUsage", JSON_BUILD_UNSIGNED(rr->tlsa.cert_usage)), + JSON_BUILD_PAIR("selector", JSON_BUILD_UNSIGNED(rr->tlsa.selector)), + JSON_BUILD_PAIR("matchingType", JSON_BUILD_UNSIGNED(rr->tlsa.matching_type)), + JSON_BUILD_PAIR("data", JSON_BUILD_HEX(rr->tlsa.data, rr->tlsa.data_size)))); + + case DNS_TYPE_CAA: + return json_build(ret, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("key", JSON_BUILD_VARIANT(k)), + JSON_BUILD_PAIR("flags", JSON_BUILD_UNSIGNED(rr->caa.flags)), + JSON_BUILD_PAIR("tag", JSON_BUILD_STRING(rr->caa.tag)), + JSON_BUILD_PAIR("value", JSON_BUILD_OCTESCAPE(rr->caa.value, rr->caa.value_size)))); + + default: + /* Can't provide broken-down format */ + *ret = NULL; + return 0; + } +} + +static const char* const dnssec_algorithm_table[_DNSSEC_ALGORITHM_MAX_DEFINED] = { + /* Mnemonics as listed on https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml */ + [DNSSEC_ALGORITHM_RSAMD5] = "RSAMD5", + [DNSSEC_ALGORITHM_DH] = "DH", + [DNSSEC_ALGORITHM_DSA] = "DSA", + [DNSSEC_ALGORITHM_ECC] = "ECC", + [DNSSEC_ALGORITHM_RSASHA1] = "RSASHA1", + [DNSSEC_ALGORITHM_DSA_NSEC3_SHA1] = "DSA-NSEC3-SHA1", + [DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1] = "RSASHA1-NSEC3-SHA1", + [DNSSEC_ALGORITHM_RSASHA256] = "RSASHA256", + [DNSSEC_ALGORITHM_RSASHA512] = "RSASHA512", + [DNSSEC_ALGORITHM_ECC_GOST] = "ECC-GOST", + [DNSSEC_ALGORITHM_ECDSAP256SHA256] = "ECDSAP256SHA256", + [DNSSEC_ALGORITHM_ECDSAP384SHA384] = "ECDSAP384SHA384", + [DNSSEC_ALGORITHM_ED25519] = "ED25519", + [DNSSEC_ALGORITHM_ED448] = "ED448", + [DNSSEC_ALGORITHM_INDIRECT] = "INDIRECT", + [DNSSEC_ALGORITHM_PRIVATEDNS] = "PRIVATEDNS", + [DNSSEC_ALGORITHM_PRIVATEOID] = "PRIVATEOID", +}; +DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(dnssec_algorithm, int, 255); + +static const char* const dnssec_digest_table[_DNSSEC_DIGEST_MAX_DEFINED] = { + /* Names as listed on https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml */ + [DNSSEC_DIGEST_SHA1] = "SHA-1", + [DNSSEC_DIGEST_SHA256] = "SHA-256", + [DNSSEC_DIGEST_GOST_R_34_11_94] = "GOST_R_34.11-94", + [DNSSEC_DIGEST_SHA384] = "SHA-384", +}; +DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(dnssec_digest, int, 255); diff --git a/src/resolve/resolved-dns-rr.h b/src/resolve/resolved-dns-rr.h new file mode 100644 index 0000000..d558842 --- /dev/null +++ b/src/resolve/resolved-dns-rr.h @@ -0,0 +1,383 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <netinet/in.h> + +#include "bitmap.h" +#include "dns-def.h" +#include "dns-type.h" +#include "hashmap.h" +#include "in-addr-util.h" +#include "json.h" +#include "list.h" +#include "string-util.h" +#include "time-util.h" + +typedef struct DnsResourceKey DnsResourceKey; +typedef struct DnsResourceRecord DnsResourceRecord; +typedef struct DnsTxtItem DnsTxtItem; + +/* DNSKEY RR flags */ +#define DNSKEY_FLAG_SEP (UINT16_C(1) << 0) +#define DNSKEY_FLAG_REVOKE (UINT16_C(1) << 7) +#define DNSKEY_FLAG_ZONE_KEY (UINT16_C(1) << 8) + +/* mDNS RR flags */ +#define MDNS_RR_CACHE_FLUSH_OR_QU (UINT16_C(1) << 15) + +/* DNSSEC algorithm identifiers, see + * http://tools.ietf.org/html/rfc4034#appendix-A.1 and + * https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml */ +enum { + DNSSEC_ALGORITHM_RSAMD5 = 1, + DNSSEC_ALGORITHM_DH, + DNSSEC_ALGORITHM_DSA, + DNSSEC_ALGORITHM_ECC, + DNSSEC_ALGORITHM_RSASHA1, + DNSSEC_ALGORITHM_DSA_NSEC3_SHA1, + DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1, + DNSSEC_ALGORITHM_RSASHA256 = 8, /* RFC 5702 */ + DNSSEC_ALGORITHM_RSASHA512 = 10, /* RFC 5702 */ + DNSSEC_ALGORITHM_ECC_GOST = 12, /* RFC 5933 */ + DNSSEC_ALGORITHM_ECDSAP256SHA256 = 13, /* RFC 6605 */ + DNSSEC_ALGORITHM_ECDSAP384SHA384 = 14, /* RFC 6605 */ + DNSSEC_ALGORITHM_ED25519 = 15, /* RFC 8080 */ + DNSSEC_ALGORITHM_ED448 = 16, /* RFC 8080 */ + DNSSEC_ALGORITHM_INDIRECT = 252, + DNSSEC_ALGORITHM_PRIVATEDNS, + DNSSEC_ALGORITHM_PRIVATEOID, + _DNSSEC_ALGORITHM_MAX_DEFINED +}; + +/* DNSSEC digest identifiers, see + * https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml */ +enum { + DNSSEC_DIGEST_SHA1 = 1, + DNSSEC_DIGEST_SHA256 = 2, /* RFC 4509 */ + DNSSEC_DIGEST_GOST_R_34_11_94 = 3, /* RFC 5933 */ + DNSSEC_DIGEST_SHA384 = 4, /* RFC 6605 */ + _DNSSEC_DIGEST_MAX_DEFINED +}; + +/* DNSSEC NSEC3 hash algorithms, see + * https://www.iana.org/assignments/dnssec-nsec3-parameters/dnssec-nsec3-parameters.xhtml */ +enum { + NSEC3_ALGORITHM_SHA1 = 1, + _NSEC3_ALGORITHM_MAX_DEFINED +}; + +struct DnsResourceKey { + unsigned n_ref; /* (unsigned -1) for const keys, see below */ + uint16_t class, type; + char *_name; /* don't access directly, use dns_resource_key_name()! */ +}; + +/* Creates a temporary resource key. This is only useful to quickly + * look up something, without allocating a full DnsResourceKey object + * for it. Note that it is not OK to take references to this kind of + * resource key object. */ +#define DNS_RESOURCE_KEY_CONST(c, t, n) \ + ((DnsResourceKey) { \ + .n_ref = UINT_MAX, \ + .class = c, \ + .type = t, \ + ._name = (char*) n, \ + }) + +struct DnsTxtItem { + size_t length; + LIST_FIELDS(DnsTxtItem, items); + uint8_t data[]; +}; + +struct DnsResourceRecord { + unsigned n_ref; + uint32_t ttl; + usec_t expiry; /* RRSIG signature expiry */ + + DnsResourceKey *key; + + char *to_string; + + /* How many labels to strip to determine "signer" of the RRSIG (aka, the zone). -1 if not signed. */ + uint8_t n_skip_labels_signer; + /* How many labels to strip to determine "synthesizing source" of this RR, i.e. the wildcard's immediate parent. -1 if not signed. */ + uint8_t n_skip_labels_source; + + bool unparsable; + bool wire_format_canonical; + + void *wire_format; + size_t wire_format_size; + size_t wire_format_rdata_offset; + + union { + struct { + void *data; + size_t data_size; + } generic, opt; + + struct { + char *name; + uint16_t priority; + uint16_t weight; + uint16_t port; + } srv; + + struct { + char *name; + } ptr, ns, cname, dname; + + struct { + char *cpu; + char *os; + } hinfo; + + struct { + DnsTxtItem *items; + } txt, spf; + + struct { + struct in_addr in_addr; + } a; + + struct { + struct in6_addr in6_addr; + } aaaa; + + struct { + char *mname; + char *rname; + uint32_t serial; + uint32_t refresh; + uint32_t retry; + uint32_t expire; + uint32_t minimum; + } soa; + + struct { + char *exchange; + uint16_t priority; + } mx; + + /* https://tools.ietf.org/html/rfc1876 */ + struct { + uint8_t version; + uint8_t size; + uint8_t horiz_pre; + uint8_t vert_pre; + uint32_t latitude; + uint32_t longitude; + uint32_t altitude; + } loc; + + /* https://tools.ietf.org/html/rfc4255#section-3.1 */ + struct { + void *fingerprint; + size_t fingerprint_size; + + uint8_t algorithm; + uint8_t fptype; + } sshfp; + + /* http://tools.ietf.org/html/rfc4034#section-2.1 */ + struct { + void* key; + size_t key_size; + + uint16_t flags; + uint8_t protocol; + uint8_t algorithm; + } dnskey; + + /* http://tools.ietf.org/html/rfc4034#section-3.1 */ + struct { + char *signer; + void *signature; + size_t signature_size; + + uint16_t type_covered; + uint8_t algorithm; + uint8_t labels; + uint32_t original_ttl; + uint32_t expiration; + uint32_t inception; + uint16_t key_tag; + } rrsig; + + /* https://tools.ietf.org/html/rfc4034#section-4.1 */ + struct { + char *next_domain_name; + Bitmap *types; + } nsec; + + /* https://tools.ietf.org/html/rfc4034#section-5.1 */ + struct { + void *digest; + size_t digest_size; + + uint16_t key_tag; + uint8_t algorithm; + uint8_t digest_type; + } ds; + + struct { + Bitmap *types; + void *salt; + size_t salt_size; + void *next_hashed_name; + size_t next_hashed_name_size; + + uint8_t algorithm; + uint8_t flags; + uint16_t iterations; + } nsec3; + + /* https://tools.ietf.org/html/draft-ietf-dane-protocol-23 */ + struct { + void *data; + size_t data_size; + + uint8_t cert_usage; + uint8_t selector; + uint8_t matching_type; + } tlsa; + + /* https://tools.ietf.org/html/rfc6844 */ + struct { + char *tag; + void *value; + size_t value_size; + + uint8_t flags; + } caa; + }; + + /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */ +}; + +/* We use uint8_t for label counts above, and UINT8_MAX/-1 has special meaning. */ +assert_cc(DNS_N_LABELS_MAX < UINT8_MAX); + +static inline const void* DNS_RESOURCE_RECORD_RDATA(const DnsResourceRecord *rr) { + if (!rr) + return NULL; + + if (!rr->wire_format) + return NULL; + + assert(rr->wire_format_rdata_offset <= rr->wire_format_size); + return (uint8_t*) rr->wire_format + rr->wire_format_rdata_offset; +} + +static inline size_t DNS_RESOURCE_RECORD_RDATA_SIZE(const DnsResourceRecord *rr) { + if (!rr) + return 0; + if (!rr->wire_format) + return 0; + + assert(rr->wire_format_rdata_offset <= rr->wire_format_size); + return rr->wire_format_size - rr->wire_format_rdata_offset; +} + +static inline uint8_t DNS_RESOURCE_RECORD_OPT_VERSION_SUPPORTED(const DnsResourceRecord *rr) { + assert(rr); + assert(rr->key->type == DNS_TYPE_OPT); + + return ((rr->ttl >> 16) & 0xFF) == 0; +} + +DnsResourceKey* dns_resource_key_new(uint16_t class, uint16_t type, const char *name); +DnsResourceKey* dns_resource_key_new_redirect(const DnsResourceKey *key, const DnsResourceRecord *cname); +int dns_resource_key_new_append_suffix(DnsResourceKey **ret, DnsResourceKey *key, char *name); +DnsResourceKey* dns_resource_key_new_consume(uint16_t class, uint16_t type, char *name); +DnsResourceKey* dns_resource_key_ref(DnsResourceKey *key); +DnsResourceKey* dns_resource_key_unref(DnsResourceKey *key); + +#define DNS_RESOURCE_KEY_REPLACE(a, b) \ + do { \ + typeof(a)* _a = &(a); \ + typeof(b) _b = (b); \ + dns_resource_key_unref(*_a); \ + *_a = _b; \ + } while(0) + +const char* dns_resource_key_name(const DnsResourceKey *key); +bool dns_resource_key_is_address(const DnsResourceKey *key); +bool dns_resource_key_is_dnssd_ptr(const DnsResourceKey *key); +int dns_resource_key_equal(const DnsResourceKey *a, const DnsResourceKey *b); +int dns_resource_key_match_rr(const DnsResourceKey *key, DnsResourceRecord *rr, const char *search_domain); +int dns_resource_key_match_cname_or_dname(const DnsResourceKey *key, const DnsResourceKey *cname, const char *search_domain); +int dns_resource_key_match_soa(const DnsResourceKey *key, const DnsResourceKey *soa); + +/* _DNS_{CLASS,TYPE}_STRING_MAX include one byte for NUL, which we use for space instead below. + * DNS_HOSTNAME_MAX does not include the NUL byte, so we need to add 1. */ +#define DNS_RESOURCE_KEY_STRING_MAX (_DNS_CLASS_STRING_MAX + _DNS_TYPE_STRING_MAX + DNS_HOSTNAME_MAX + 1) + +char* dns_resource_key_to_string(const DnsResourceKey *key, char *buf, size_t buf_size); +ssize_t dns_resource_record_payload(DnsResourceRecord *rr, void **out); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsResourceKey*, dns_resource_key_unref); + +static inline bool dns_key_is_shared(const DnsResourceKey *key) { + return IN_SET(key->type, DNS_TYPE_PTR); +} + +bool dns_resource_key_reduce(DnsResourceKey **a, DnsResourceKey **b); + +DnsResourceRecord* dns_resource_record_new(DnsResourceKey *key); +DnsResourceRecord* dns_resource_record_new_full(uint16_t class, uint16_t type, const char *name); +DnsResourceRecord* dns_resource_record_ref(DnsResourceRecord *rr); +DnsResourceRecord* dns_resource_record_unref(DnsResourceRecord *rr); + +#define DNS_RR_REPLACE(a, b) \ + do { \ + typeof(a)* _a = &(a); \ + typeof(b) _b = (b); \ + dns_resource_record_unref(*_a); \ + *_a = _b; \ + } while(0) + +int dns_resource_record_new_reverse(DnsResourceRecord **ret, int family, const union in_addr_union *address, const char *name); +int dns_resource_record_new_address(DnsResourceRecord **ret, int family, const union in_addr_union *address, const char *name); +int dns_resource_record_equal(const DnsResourceRecord *a, const DnsResourceRecord *b); +int dns_resource_record_payload_equal(const DnsResourceRecord *a, const DnsResourceRecord *b); + +const char* dns_resource_record_to_string(DnsResourceRecord *rr); +DnsResourceRecord *dns_resource_record_copy(DnsResourceRecord *rr); +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsResourceRecord*, dns_resource_record_unref); + +int dns_resource_record_to_wire_format(DnsResourceRecord *rr, bool canonical); + +int dns_resource_record_signer(DnsResourceRecord *rr, const char **ret); +int dns_resource_record_source(DnsResourceRecord *rr, const char **ret); +int dns_resource_record_is_signer(DnsResourceRecord *rr, const char *zone); +int dns_resource_record_is_synthetic(DnsResourceRecord *rr); + +int dns_resource_record_clamp_ttl(DnsResourceRecord **rr, uint32_t max_ttl); + +bool dns_resource_record_is_link_local_address(DnsResourceRecord *rr); + +int dns_resource_record_get_cname_target(DnsResourceKey *key, DnsResourceRecord *cname, char **ret); + +DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *i); +bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b); +DnsTxtItem *dns_txt_item_copy(DnsTxtItem *i); +int dns_txt_item_new_empty(DnsTxtItem **ret); + +int dns_resource_record_new_from_raw(DnsResourceRecord **ret, const void *data, size_t size); + +int dns_resource_key_to_json(DnsResourceKey *key, JsonVariant **ret); +int dns_resource_record_to_json(DnsResourceRecord *rr, JsonVariant **ret); + +void dns_resource_record_hash_func(const DnsResourceRecord *i, struct siphash *state); +int dns_resource_record_compare_func(const DnsResourceRecord *x, const DnsResourceRecord *y); + +extern const struct hash_ops dns_resource_key_hash_ops; +extern const struct hash_ops dns_resource_record_hash_ops; + +int dnssec_algorithm_to_string_alloc(int i, char **ret); +int dnssec_algorithm_from_string(const char *s) _pure_; + +int dnssec_digest_to_string_alloc(int i, char **ret); +int dnssec_digest_from_string(const char *s) _pure_; diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c new file mode 100644 index 0000000..4085b01 --- /dev/null +++ b/src/resolve/resolved-dns-scope.c @@ -0,0 +1,1645 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/tcp.h> + +#include "af-list.h" +#include "alloc-util.h" +#include "dns-domain.h" +#include "errno-util.h" +#include "fd-util.h" +#include "hostname-util.h" +#include "missing_network.h" +#include "random-util.h" +#include "resolved-dnssd.h" +#include "resolved-dns-scope.h" +#include "resolved-dns-zone.h" +#include "resolved-llmnr.h" +#include "resolved-mdns.h" +#include "socket-util.h" +#include "strv.h" + +#define MULTICAST_RATELIMIT_INTERVAL_USEC (1*USEC_PER_SEC) +#define MULTICAST_RATELIMIT_BURST 1000 + +/* After how much time to repeat LLMNR requests, see RFC 4795 Section 7 */ +#define MULTICAST_RESEND_TIMEOUT_MIN_USEC (100 * USEC_PER_MSEC) +#define MULTICAST_RESEND_TIMEOUT_MAX_USEC (1 * USEC_PER_SEC) + +int dns_scope_new(Manager *m, DnsScope **ret, Link *l, DnsProtocol protocol, int family) { + DnsScope *s; + + assert(m); + assert(ret); + + s = new(DnsScope, 1); + if (!s) + return -ENOMEM; + + *s = (DnsScope) { + .manager = m, + .link = l, + .protocol = protocol, + .family = family, + .resend_timeout = MULTICAST_RESEND_TIMEOUT_MIN_USEC, + }; + + if (protocol == DNS_PROTOCOL_DNS) { + /* Copy DNSSEC mode from the link if it is set there, + * otherwise take the manager's DNSSEC mode. Note that + * we copy this only at scope creation time, and do + * not update it from the on, even if the setting + * changes. */ + + if (l) { + s->dnssec_mode = link_get_dnssec_mode(l); + s->dns_over_tls_mode = link_get_dns_over_tls_mode(l); + } else { + s->dnssec_mode = manager_get_dnssec_mode(m); + s->dns_over_tls_mode = manager_get_dns_over_tls_mode(m); + } + + } else { + s->dnssec_mode = DNSSEC_NO; + s->dns_over_tls_mode = DNS_OVER_TLS_NO; + } + + LIST_PREPEND(scopes, m->dns_scopes, s); + + dns_scope_llmnr_membership(s, true); + dns_scope_mdns_membership(s, true); + + log_debug("New scope on link %s, protocol %s, family %s", l ? l->ifname : "*", dns_protocol_to_string(protocol), family == AF_UNSPEC ? "*" : af_to_name(family)); + + /* Enforce ratelimiting for the multicast protocols */ + s->ratelimit = (RateLimit) { MULTICAST_RATELIMIT_INTERVAL_USEC, MULTICAST_RATELIMIT_BURST }; + + *ret = s; + return 0; +} + +static void dns_scope_abort_transactions(DnsScope *s) { + assert(s); + + while (s->transactions) { + DnsTransaction *t = s->transactions; + + /* Abort the transaction, but make sure it is not + * freed while we still look at it */ + + t->block_gc++; + if (DNS_TRANSACTION_IS_LIVE(t->state)) + dns_transaction_complete(t, DNS_TRANSACTION_ABORTED); + t->block_gc--; + + dns_transaction_free(t); + } +} + +DnsScope* dns_scope_free(DnsScope *s) { + if (!s) + return NULL; + + log_debug("Removing scope on link %s, protocol %s, family %s", s->link ? s->link->ifname : "*", dns_protocol_to_string(s->protocol), s->family == AF_UNSPEC ? "*" : af_to_name(s->family)); + + dns_scope_llmnr_membership(s, false); + dns_scope_mdns_membership(s, false); + dns_scope_abort_transactions(s); + + while (s->query_candidates) + dns_query_candidate_unref(s->query_candidates); + + hashmap_free(s->transactions_by_key); + + ordered_hashmap_free_with_destructor(s->conflict_queue, dns_resource_record_unref); + sd_event_source_disable_unref(s->conflict_event_source); + + sd_event_source_disable_unref(s->announce_event_source); + + dns_cache_flush(&s->cache); + dns_zone_flush(&s->zone); + + LIST_REMOVE(scopes, s->manager->dns_scopes, s); + return mfree(s); +} + +DnsServer *dns_scope_get_dns_server(DnsScope *s) { + assert(s); + + if (s->protocol != DNS_PROTOCOL_DNS) + return NULL; + + if (s->link) + return link_get_dns_server(s->link); + else + return manager_get_dns_server(s->manager); +} + +unsigned dns_scope_get_n_dns_servers(DnsScope *s) { + unsigned n = 0; + DnsServer *i; + + assert(s); + + if (s->protocol != DNS_PROTOCOL_DNS) + return 0; + + if (s->link) + i = s->link->dns_servers; + else + i = s->manager->dns_servers; + + for (; i; i = i->servers_next) + n++; + + return n; +} + +void dns_scope_next_dns_server(DnsScope *s, DnsServer *if_current) { + assert(s); + + if (s->protocol != DNS_PROTOCOL_DNS) + return; + + /* Changes to the next DNS server in the list. If 'if_current' is passed will do so only if the + * current DNS server still matches it. */ + + if (s->link) + link_next_dns_server(s->link, if_current); + else + manager_next_dns_server(s->manager, if_current); +} + +void dns_scope_packet_received(DnsScope *s, usec_t rtt) { + assert(s); + + if (rtt <= s->max_rtt) + return; + + s->max_rtt = rtt; + s->resend_timeout = MIN(MAX(MULTICAST_RESEND_TIMEOUT_MIN_USEC, s->max_rtt * 2), MULTICAST_RESEND_TIMEOUT_MAX_USEC); +} + +void dns_scope_packet_lost(DnsScope *s, usec_t usec) { + assert(s); + + if (s->resend_timeout <= usec) + s->resend_timeout = MIN(s->resend_timeout * 2, MULTICAST_RESEND_TIMEOUT_MAX_USEC); +} + +static int dns_scope_emit_one(DnsScope *s, int fd, int family, DnsPacket *p) { + int r; + + assert(s); + assert(p); + assert(p->protocol == s->protocol); + + if (family == AF_UNSPEC) { + if (s->family == AF_UNSPEC) + return -EAFNOSUPPORT; + + family = s->family; + } + + switch (s->protocol) { + + case DNS_PROTOCOL_DNS: { + size_t mtu, udp_size, min_mtu, socket_mtu = 0; + + assert(fd >= 0); + + if (DNS_PACKET_QDCOUNT(p) > 1) /* Classic DNS only allows one question per packet */ + return -EOPNOTSUPP; + + if (p->size > DNS_PACKET_UNICAST_SIZE_MAX) + return -EMSGSIZE; + + /* Determine the local most accurate MTU */ + if (s->link) + mtu = s->link->mtu; + else + mtu = manager_find_mtu(s->manager); + + /* Acquire the socket's PMDU MTU */ + r = socket_get_mtu(fd, family, &socket_mtu); + if (r < 0 && !ERRNO_IS_DISCONNECT(r)) /* Will return ENOTCONN if no information is available yet */ + return log_debug_errno(r, "Failed to read socket MTU: %m"); + + /* Determine the appropriate UDP header size */ + udp_size = udp_header_size(family); + min_mtu = udp_size + DNS_PACKET_HEADER_SIZE; + + log_debug("Emitting UDP, link MTU is %zu, socket MTU is %zu, minimal MTU is %zu", + mtu, socket_mtu, min_mtu); + + /* Clamp by the kernel's idea of the (path) MTU */ + if (socket_mtu != 0 && socket_mtu < mtu) + mtu = socket_mtu; + + /* Put a lower limit, in case all MTU data we acquired was rubbish */ + if (mtu < min_mtu) + mtu = min_mtu; + + /* Now check our packet size against the MTU we determined */ + if (udp_size + p->size > mtu) + return -EMSGSIZE; /* This means: try TCP instead */ + + r = manager_write(s->manager, fd, p); + if (r < 0) + return r; + + break; + } + + case DNS_PROTOCOL_LLMNR: { + union in_addr_union addr; + + assert(fd < 0); + + if (DNS_PACKET_QDCOUNT(p) > 1) + return -EOPNOTSUPP; + + if (!ratelimit_below(&s->ratelimit)) + return -EBUSY; + + if (family == AF_INET) { + addr.in = LLMNR_MULTICAST_IPV4_ADDRESS; + fd = manager_llmnr_ipv4_udp_fd(s->manager); + } else if (family == AF_INET6) { + addr.in6 = LLMNR_MULTICAST_IPV6_ADDRESS; + fd = manager_llmnr_ipv6_udp_fd(s->manager); + } else + return -EAFNOSUPPORT; + if (fd < 0) + return fd; + + r = manager_send(s->manager, fd, s->link->ifindex, family, &addr, LLMNR_PORT, NULL, p); + if (r < 0) + return r; + + break; + } + + case DNS_PROTOCOL_MDNS: { + union in_addr_union addr; + assert(fd < 0); + + if (!ratelimit_below(&s->ratelimit)) + return -EBUSY; + + if (family == AF_INET) { + if (in4_addr_is_null(&p->destination.in)) + addr.in = MDNS_MULTICAST_IPV4_ADDRESS; + else + addr = p->destination; + fd = manager_mdns_ipv4_fd(s->manager); + } else if (family == AF_INET6) { + if (in6_addr_is_null(&p->destination.in6)) + addr.in6 = MDNS_MULTICAST_IPV6_ADDRESS; + else + addr = p->destination; + fd = manager_mdns_ipv6_fd(s->manager); + } else + return -EAFNOSUPPORT; + if (fd < 0) + return fd; + + r = manager_send(s->manager, fd, s->link->ifindex, family, &addr, p->destination_port ?: MDNS_PORT, NULL, p); + if (r < 0) + return r; + + break; + } + + default: + return -EAFNOSUPPORT; + } + + return 1; +} + +int dns_scope_emit_udp(DnsScope *s, int fd, int af, DnsPacket *p) { + int r; + + assert(s); + assert(p); + assert(p->protocol == s->protocol); + assert((s->protocol == DNS_PROTOCOL_DNS) == (fd >= 0)); + + do { + /* If there are multiple linked packets, set the TC bit in all but the last of them */ + if (p->more) { + assert(p->protocol == DNS_PROTOCOL_MDNS); + dns_packet_set_flags(p, true, true); + } + + r = dns_scope_emit_one(s, fd, af, p); + if (r < 0) + return r; + + p = p->more; + } while (p); + + return 0; +} + +static int dns_scope_socket( + DnsScope *s, + int type, + int family, + const union in_addr_union *address, + DnsServer *server, + uint16_t port, + union sockaddr_union *ret_socket_address) { + + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + socklen_t salen; + int r, ifindex; + + assert(s); + + if (server) { + assert(family == AF_UNSPEC); + assert(!address); + + ifindex = dns_server_ifindex(server); + + switch (server->family) { + case AF_INET: + sa = (union sockaddr_union) { + .in.sin_family = server->family, + .in.sin_port = htobe16(port), + .in.sin_addr = server->address.in, + }; + salen = sizeof(sa.in); + break; + case AF_INET6: + sa = (union sockaddr_union) { + .in6.sin6_family = server->family, + .in6.sin6_port = htobe16(port), + .in6.sin6_addr = server->address.in6, + .in6.sin6_scope_id = ifindex, + }; + salen = sizeof(sa.in6); + break; + default: + return -EAFNOSUPPORT; + } + } else { + assert(family != AF_UNSPEC); + assert(address); + + ifindex = s->link ? s->link->ifindex : 0; + + switch (family) { + case AF_INET: + sa = (union sockaddr_union) { + .in.sin_family = family, + .in.sin_port = htobe16(port), + .in.sin_addr = address->in, + }; + salen = sizeof(sa.in); + break; + case AF_INET6: + sa = (union sockaddr_union) { + .in6.sin6_family = family, + .in6.sin6_port = htobe16(port), + .in6.sin6_addr = address->in6, + .in6.sin6_scope_id = ifindex, + }; + salen = sizeof(sa.in6); + break; + default: + return -EAFNOSUPPORT; + } + } + + fd = socket(sa.sa.sa_family, type|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + if (type == SOCK_STREAM) { + r = setsockopt_int(fd, IPPROTO_TCP, TCP_NODELAY, true); + if (r < 0) + return r; + } + + if (ifindex != 0) { + r = socket_set_unicast_if(fd, sa.sa.sa_family, ifindex); + if (r < 0) + return r; + } + + if (s->protocol == DNS_PROTOCOL_LLMNR) { + /* RFC 4795, section 2.5 requires the TTL to be set to 1 */ + r = socket_set_ttl(fd, sa.sa.sa_family, 1); + if (r < 0) + return r; + } + + if (type == SOCK_DGRAM) { + /* Set IP_RECVERR or IPV6_RECVERR to get ICMP error feedback. See discussion in #10345. */ + r = socket_set_recverr(fd, sa.sa.sa_family, true); + if (r < 0) + return r; + + r = socket_set_recvpktinfo(fd, sa.sa.sa_family, true); + if (r < 0) + return r; + + /* Turn of path MTU discovery for security reasons */ + r = socket_disable_pmtud(fd, sa.sa.sa_family); + if (r < 0) + log_debug_errno(r, "Failed to disable UDP PMTUD, ignoring: %m"); + + /* Learn about fragmentation taking place */ + r = socket_set_recvfragsize(fd, sa.sa.sa_family, true); + if (r < 0) + log_debug_errno(r, "Failed to enable fragment size reception, ignoring: %m"); + } + + if (ret_socket_address) + *ret_socket_address = sa; + else { + bool bound = false; + + /* Let's temporarily bind the socket to the specified ifindex. The kernel currently takes + * only the SO_BINDTODEVICE/SO_BINDTOINDEX ifindex into account when making routing decisions + * in connect() — and not IP_UNICAST_IF. We don't really want any of the other semantics of + * SO_BINDTODEVICE/SO_BINDTOINDEX, hence we immediately unbind the socket after the fact + * again. + * + * As a special exception we don't do this if we notice that the specified IP address is on + * the local host. SO_BINDTODEVICE in combination with destination addresses on the local + * host result in EHOSTUNREACH, since Linux won't send the packets out of the specified + * interface, but delivers them directly to the local socket. */ + if (s->link && + !manager_find_link_address(s->manager, sa.sa.sa_family, sockaddr_in_addr(&sa.sa)) && + in_addr_is_localhost(sa.sa.sa_family, sockaddr_in_addr(&sa.sa)) == 0) { + r = socket_bind_to_ifindex(fd, ifindex); + if (r < 0) + return r; + + bound = true; + } + + r = connect(fd, &sa.sa, salen); + if (r < 0 && errno != EINPROGRESS) + return -errno; + + if (bound) { + r = socket_bind_to_ifindex(fd, 0); + if (r < 0) + return r; + } + } + + return TAKE_FD(fd); +} + +int dns_scope_socket_udp(DnsScope *s, DnsServer *server) { + return dns_scope_socket(s, SOCK_DGRAM, AF_UNSPEC, NULL, server, dns_server_port(server), NULL); +} + +int dns_scope_socket_tcp(DnsScope *s, int family, const union in_addr_union *address, DnsServer *server, uint16_t port, union sockaddr_union *ret_socket_address) { + /* If ret_socket_address is not NULL, the caller is responsible + * for calling connect() or sendmsg(). This is required by TCP + * Fast Open, to be able to send the initial SYN packet along + * with the first data packet. */ + return dns_scope_socket(s, SOCK_STREAM, family, address, server, port, ret_socket_address); +} + +static DnsScopeMatch match_link_local_reverse_lookups(const char *domain) { + assert(domain); + + if (dns_name_endswith(domain, "254.169.in-addr.arpa") > 0) + return DNS_SCOPE_YES_BASE + 4; /* 4 labels match */ + + if (dns_name_endswith(domain, "8.e.f.ip6.arpa") > 0 || + dns_name_endswith(domain, "9.e.f.ip6.arpa") > 0 || + dns_name_endswith(domain, "a.e.f.ip6.arpa") > 0 || + dns_name_endswith(domain, "b.e.f.ip6.arpa") > 0) + return DNS_SCOPE_YES_BASE + 5; /* 5 labels match */ + + return _DNS_SCOPE_MATCH_INVALID; +} + +static DnsScopeMatch match_subnet_reverse_lookups( + DnsScope *s, + const char *domain, + bool exclude_own) { + + union in_addr_union ia; + int f, r; + + assert(s); + assert(domain); + + /* Checks whether the specified domain is a reverse address domain (i.e. in the .in-addr.arpa or + * .ip6.arpa area), and if so, whether the address matches any of the local subnets of the link the + * scope is associated with. If so, our scope should consider itself relevant for any lookup in the + * domain, since it apparently refers to hosts on this link's subnet. + * + * If 'exclude_own' is true this will return DNS_SCOPE_NO for any IP addresses assigned locally. This + * is useful for LLMNR/mDNS as we never want to look up our own hostname on LLMNR/mDNS but always use + * the locally synthesized one. */ + + if (!s->link) + return _DNS_SCOPE_MATCH_INVALID; /* No link, hence no local addresses to check */ + + r = dns_name_address(domain, &f, &ia); + if (r < 0) + log_debug_errno(r, "Failed to determine whether '%s' is an address domain: %m", domain); + if (r <= 0) + return _DNS_SCOPE_MATCH_INVALID; + + if (s->family != AF_UNSPEC && f != s->family) + return _DNS_SCOPE_MATCH_INVALID; /* Don't look for IPv4 addresses on LLMNR/mDNS over IPv6 and vice versa */ + + LIST_FOREACH(addresses, a, s->link->addresses) { + + if (a->family != f) + continue; + + /* Equals our own address? nah, let's not use this scope. The local synthesizer will pick it up for us. */ + if (exclude_own && + in_addr_equal(f, &a->in_addr, &ia) > 0) + return DNS_SCOPE_NO; + + if (a->prefixlen == UCHAR_MAX) /* don't know subnet mask */ + continue; + + /* Check if the address is in the local subnet */ + r = in_addr_prefix_covers(f, &a->in_addr, a->prefixlen, &ia); + if (r < 0) + log_debug_errno(r, "Failed to determine whether link address covers lookup address '%s': %m", domain); + if (r > 0) + /* Note that we only claim zero labels match. This is so that this is at the same + * priority a DNS scope with "." as routing domain is. */ + return DNS_SCOPE_YES_BASE + 0; + } + + return _DNS_SCOPE_MATCH_INVALID; +} + +DnsScopeMatch dns_scope_good_domain( + DnsScope *s, + DnsQuery *q) { + + DnsQuestion *question; + const char *domain; + uint64_t flags; + int ifindex; + + /* This returns the following return values: + * + * DNS_SCOPE_NO → This scope is not suitable for lookups of this domain, at all + * DNS_SCOPE_MAYBE → This scope is suitable, but only if nothing else wants it + * DNS_SCOPE_YES_BASE+n → This scope is suitable, and 'n' suffix labels match + * + * (The idea is that the caller will only use the scopes with the longest 'n' returned. If no scopes return + * DNS_SCOPE_YES_BASE+n, then it should use those which returned DNS_SCOPE_MAYBE. It should never use those + * which returned DNS_SCOPE_NO.) + */ + + assert(s); + assert(q); + + question = dns_query_question_for_protocol(q, s->protocol); + if (!question) + return DNS_SCOPE_NO; + + domain = dns_question_first_name(question); + if (!domain) + return DNS_SCOPE_NO; + + ifindex = q->ifindex; + flags = q->flags; + + /* Checks if the specified domain is something to look up on this scope. Note that this accepts + * non-qualified hostnames, i.e. those without any search path suffixed. */ + + if (ifindex != 0 && (!s->link || s->link->ifindex != ifindex)) + return DNS_SCOPE_NO; + + if ((SD_RESOLVED_FLAGS_MAKE(s->protocol, s->family, false, false) & flags) == 0) + return DNS_SCOPE_NO; + + /* Never resolve any loopback hostname or IP address via DNS, LLMNR or mDNS. Instead, always rely on + * synthesized RRs for these. */ + if (is_localhost(domain) || + dns_name_endswith(domain, "127.in-addr.arpa") > 0 || + dns_name_equal(domain, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa") > 0) + return DNS_SCOPE_NO; + + /* Never respond to some of the domains listed in RFC6303 + RFC6761 */ + if (dns_name_dont_resolve(domain)) + return DNS_SCOPE_NO; + + /* Never go to network for the _gateway or _outbound domain — they're something special, synthesized locally. */ + if (is_gateway_hostname(domain) || is_outbound_hostname(domain)) + return DNS_SCOPE_NO; + + switch (s->protocol) { + + case DNS_PROTOCOL_DNS: { + bool has_search_domains = false; + DnsScopeMatch m; + int n_best = -1; + + if (dns_name_is_empty(domain)) { + DnsResourceKey *t; + bool found = false; + + /* Refuse empty name if only A and/or AAAA records are requested. */ + + DNS_QUESTION_FOREACH(t, question) + if (!IN_SET(t->type, DNS_TYPE_A, DNS_TYPE_AAAA)) { + found = true; + break; + } + + if (!found) + return DNS_SCOPE_NO; + } + + /* Never route things to scopes that lack DNS servers */ + if (!dns_scope_get_dns_server(s)) + return DNS_SCOPE_NO; + + /* Always honour search domains for routing queries, except if this scope lacks DNS servers. Note that + * we return DNS_SCOPE_YES here, rather than just DNS_SCOPE_MAYBE, which means other wildcard scopes + * won't be considered anymore. */ + LIST_FOREACH(domains, d, dns_scope_get_search_domains(s)) { + + if (!d->route_only && !dns_name_is_root(d->name)) + has_search_domains = true; + + if (dns_name_endswith(domain, d->name) > 0) { + int c; + + c = dns_name_count_labels(d->name); + if (c < 0) + continue; + + if (c > n_best) + n_best = c; + } + } + + /* If there's a true search domain defined for this scope, and the query is single-label, + * then let's resolve things here, prefereably. Note that LLMNR considers itself + * authoritative for single-label names too, at the same preference, see below. */ + if (has_search_domains && dns_name_is_single_label(domain)) + return DNS_SCOPE_YES_BASE + 1; + + /* If ResolveUnicastSingleLabel=yes and the query is single-label, then bump match result + to prevent LLMNR monopoly among candidates. */ + if (s->manager->resolve_unicast_single_label && dns_name_is_single_label(domain)) + return DNS_SCOPE_YES_BASE + 1; + + /* Let's return the number of labels in the best matching result */ + if (n_best >= 0) { + assert(n_best <= DNS_SCOPE_YES_END - DNS_SCOPE_YES_BASE); + return DNS_SCOPE_YES_BASE + n_best; + } + + /* Exclude link-local IP ranges */ + if (match_link_local_reverse_lookups(domain) >= DNS_SCOPE_YES_BASE || + /* If networks use .local in their private setups, they are supposed to also add .local + * to their search domains, which we already checked above. Otherwise, we consider .local + * specific to mDNS and won't send such queries ordinary DNS servers. */ + dns_name_endswith(domain, "local") > 0) + return DNS_SCOPE_NO; + + /* If the IP address to look up matches the local subnet, then implicitly synthesizes + * DNS_SCOPE_YES_BASE + 0 on this interface, i.e. preferably resolve IP addresses via the DNS + * server belonging to this interface. */ + m = match_subnet_reverse_lookups(s, domain, false); + if (m >= 0) + return m; + + /* If there was no match at all, then see if this scope is suitable as default route. */ + if (!dns_scope_is_default_route(s)) + return DNS_SCOPE_NO; + + return DNS_SCOPE_MAYBE; + } + + case DNS_PROTOCOL_MDNS: { + DnsScopeMatch m; + + m = match_link_local_reverse_lookups(domain); + if (m >= 0) + return m; + + m = match_subnet_reverse_lookups(s, domain, true); + if (m >= 0) + return m; + + if ((s->family == AF_INET && dns_name_endswith(domain, "in-addr.arpa") > 0) || + (s->family == AF_INET6 && dns_name_endswith(domain, "ip6.arpa") > 0)) + return DNS_SCOPE_MAYBE; + + if ((dns_name_endswith(domain, "local") > 0 && /* only resolve names ending in .local via mDNS */ + dns_name_equal(domain, "local") == 0 && /* but not the single-label "local" name itself */ + manager_is_own_hostname(s->manager, domain) <= 0)) /* never resolve the local hostname via mDNS */ + return DNS_SCOPE_YES_BASE + 1; /* Return +1, as the top-level .local domain matches, i.e. one label */ + + return DNS_SCOPE_NO; + } + + case DNS_PROTOCOL_LLMNR: { + DnsScopeMatch m; + + m = match_link_local_reverse_lookups(domain); + if (m >= 0) + return m; + + m = match_subnet_reverse_lookups(s, domain, true); + if (m >= 0) + return m; + + if ((s->family == AF_INET && dns_name_endswith(domain, "in-addr.arpa") > 0) || + (s->family == AF_INET6 && dns_name_endswith(domain, "ip6.arpa") > 0)) + return DNS_SCOPE_MAYBE; + + if ((dns_name_is_single_label(domain) && /* only resolve single label names via LLMNR */ + !is_gateway_hostname(domain) && /* don't resolve "_gateway" with LLMNR, let local synthesizing logic handle that */ + !is_outbound_hostname(domain) && /* similar for "_outbound" */ + dns_name_equal(domain, "local") == 0 && /* don't resolve "local" with LLMNR, it's the top-level domain of mDNS after all, see above */ + manager_is_own_hostname(s->manager, domain) <= 0)) /* never resolve the local hostname via LLMNR */ + return DNS_SCOPE_YES_BASE + 1; /* Return +1, as we consider ourselves authoritative + * for single-label names, i.e. one label. This is + * particularly relevant as it means a "." route on some + * other scope won't pull all traffic away from + * us. (If people actually want to pull traffic away + * from us they should turn off LLMNR on the + * link). Note that unicast DNS scopes with search + * domains also consider themselves authoritative for + * single-label domains, at the same preference (see + * above). */ + + return DNS_SCOPE_NO; + } + + default: + assert_not_reached(); + } +} + +bool dns_scope_good_key(DnsScope *s, const DnsResourceKey *key) { + int key_family; + + assert(s); + assert(key); + + /* Check if it makes sense to resolve the specified key on this scope. Note that this call assumes a + * fully qualified name, i.e. the search suffixes already appended. */ + + if (!IN_SET(key->class, DNS_CLASS_IN, DNS_CLASS_ANY)) + return false; + + if (s->protocol == DNS_PROTOCOL_DNS) { + + /* On classic DNS, looking up non-address RRs is always fine. (Specifically, we want to + * permit looking up DNSKEY and DS records on the root and top-level domains.) */ + if (!dns_resource_key_is_address(key)) + return true; + + /* Unless explicitly overridden, we refuse to look up A and AAAA RRs on the root and + * single-label domains, under the assumption that those should be resolved via LLMNR or + * search path only, and should not be leaked onto the internet. */ + const char* name = dns_resource_key_name(key); + + if (!s->manager->resolve_unicast_single_label && + dns_name_is_single_label(name)) + return false; + + return !dns_name_is_root(name); + } + + /* Never route DNSSEC RR queries to LLMNR/mDNS scopes */ + if (dns_type_is_dnssec(key->type)) + return false; + + /* On mDNS and LLMNR, send A and AAAA queries only on the respective scopes */ + + key_family = dns_type_to_af(key->type); + if (key_family < 0) + return true; + + return key_family == s->family; +} + +static int dns_scope_multicast_membership(DnsScope *s, bool b, struct in_addr in, struct in6_addr in6) { + int fd; + + assert(s); + assert(s->link); + + if (s->family == AF_INET) { + struct ip_mreqn mreqn = { + .imr_multiaddr = in, + .imr_ifindex = s->link->ifindex, + }; + + if (s->protocol == DNS_PROTOCOL_LLMNR) + fd = manager_llmnr_ipv4_udp_fd(s->manager); + else + fd = manager_mdns_ipv4_fd(s->manager); + + if (fd < 0) + return fd; + + /* Always first try to drop membership before we add + * one. This is necessary on some devices, such as + * veth. */ + if (b) + (void) setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreqn, sizeof(mreqn)); + + if (setsockopt(fd, IPPROTO_IP, b ? IP_ADD_MEMBERSHIP : IP_DROP_MEMBERSHIP, &mreqn, sizeof(mreqn)) < 0) + return -errno; + + } else if (s->family == AF_INET6) { + struct ipv6_mreq mreq = { + .ipv6mr_multiaddr = in6, + .ipv6mr_interface = s->link->ifindex, + }; + + if (s->protocol == DNS_PROTOCOL_LLMNR) + fd = manager_llmnr_ipv6_udp_fd(s->manager); + else + fd = manager_mdns_ipv6_fd(s->manager); + + if (fd < 0) + return fd; + + if (b) + (void) setsockopt(fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, &mreq, sizeof(mreq)); + + if (setsockopt(fd, IPPROTO_IPV6, b ? IPV6_ADD_MEMBERSHIP : IPV6_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) + return -errno; + } else + return -EAFNOSUPPORT; + + return 0; +} + +int dns_scope_llmnr_membership(DnsScope *s, bool b) { + assert(s); + + if (s->protocol != DNS_PROTOCOL_LLMNR) + return 0; + + return dns_scope_multicast_membership(s, b, LLMNR_MULTICAST_IPV4_ADDRESS, LLMNR_MULTICAST_IPV6_ADDRESS); +} + +int dns_scope_mdns_membership(DnsScope *s, bool b) { + assert(s); + + if (s->protocol != DNS_PROTOCOL_MDNS) + return 0; + + return dns_scope_multicast_membership(s, b, MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_IPV6_ADDRESS); +} + +int dns_scope_make_reply_packet( + DnsScope *s, + uint16_t id, + int rcode, + DnsQuestion *q, + DnsAnswer *answer, + DnsAnswer *soa, + bool tentative, + DnsPacket **ret) { + + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + unsigned n_answer = 0, n_soa = 0; + int r; + bool c_or_aa; + + assert(s); + assert(ret); + + if (dns_question_isempty(q) && + dns_answer_isempty(answer) && + dns_answer_isempty(soa)) + return -EINVAL; + + r = dns_packet_new(&p, s->protocol, 0, DNS_PACKET_SIZE_MAX); + if (r < 0) + return r; + + /* mDNS answers must have the Authoritative Answer bit set, see RFC 6762, section 18.4. */ + c_or_aa = s->protocol == DNS_PROTOCOL_MDNS; + + DNS_PACKET_HEADER(p)->id = id; + DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS( + 1 /* qr */, + 0 /* opcode */, + c_or_aa, + 0 /* tc */, + tentative, + 0 /* (ra) */, + 0 /* (ad) */, + 0 /* (cd) */, + rcode)); + + r = dns_packet_append_question(p, q); + if (r < 0) + return r; + DNS_PACKET_HEADER(p)->qdcount = htobe16(dns_question_size(q)); + + r = dns_packet_append_answer(p, answer, &n_answer); + if (r < 0) + return r; + DNS_PACKET_HEADER(p)->ancount = htobe16(n_answer); + + r = dns_packet_append_answer(p, soa, &n_soa); + if (r < 0) + return r; + DNS_PACKET_HEADER(p)->arcount = htobe16(n_soa); + + *ret = TAKE_PTR(p); + + return 0; +} + +static void dns_scope_verify_conflicts(DnsScope *s, DnsPacket *p) { + DnsResourceRecord *rr; + DnsResourceKey *key; + + assert(s); + assert(p); + + DNS_QUESTION_FOREACH(key, p->question) + dns_zone_verify_conflicts(&s->zone, key); + + DNS_ANSWER_FOREACH(rr, p->answer) + dns_zone_verify_conflicts(&s->zone, rr->key); +} + +void dns_scope_process_query(DnsScope *s, DnsStream *stream, DnsPacket *p) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; + _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL; + DnsResourceKey *key = NULL; + bool tentative = false; + int r; + + assert(s); + assert(p); + + if (p->protocol != DNS_PROTOCOL_LLMNR) + return; + + if (p->ipproto == IPPROTO_UDP) { + /* Don't accept UDP queries directed to anything but + * the LLMNR multicast addresses. See RFC 4795, + * section 2.5. */ + + if (p->family == AF_INET && !in4_addr_equal(&p->destination.in, &LLMNR_MULTICAST_IPV4_ADDRESS)) + return; + + if (p->family == AF_INET6 && !in6_addr_equal(&p->destination.in6, &LLMNR_MULTICAST_IPV6_ADDRESS)) + return; + } + + r = dns_packet_extract(p); + if (r < 0) { + log_debug_errno(r, "Failed to extract resource records from incoming packet: %m"); + return; + } + + if (DNS_PACKET_LLMNR_C(p)) { + /* Somebody notified us about a possible conflict */ + dns_scope_verify_conflicts(s, p); + return; + } + + if (dns_question_size(p->question) != 1) + return (void) log_debug("Received LLMNR query without question or multiple questions, ignoring."); + + key = dns_question_first_key(p->question); + + r = dns_zone_lookup(&s->zone, key, 0, &answer, &soa, &tentative); + if (r < 0) { + log_debug_errno(r, "Failed to look up key: %m"); + return; + } + if (r == 0) + return; + + if (answer) + dns_answer_order_by_scope(answer, in_addr_is_link_local(p->family, &p->sender) > 0); + + r = dns_scope_make_reply_packet(s, DNS_PACKET_ID(p), DNS_RCODE_SUCCESS, p->question, answer, soa, tentative, &reply); + if (r < 0) { + log_debug_errno(r, "Failed to build reply packet: %m"); + return; + } + + if (stream) { + r = dns_stream_write_packet(stream, reply); + if (r < 0) { + log_debug_errno(r, "Failed to enqueue reply packet: %m"); + return; + } + + /* Let's take an extra reference on this stream, so that it stays around after returning. The reference + * will be dangling until the stream is disconnected, and the default completion handler of the stream + * will then unref the stream and destroy it */ + if (DNS_STREAM_QUEUED(stream)) + dns_stream_ref(stream); + } else { + int fd; + + if (!ratelimit_below(&s->ratelimit)) + return; + + if (p->family == AF_INET) + fd = manager_llmnr_ipv4_udp_fd(s->manager); + else if (p->family == AF_INET6) + fd = manager_llmnr_ipv6_udp_fd(s->manager); + else { + log_debug("Unknown protocol"); + return; + } + if (fd < 0) { + log_debug_errno(fd, "Failed to get reply socket: %m"); + return; + } + + /* Note that we always immediately reply to all LLMNR + * requests, and do not wait any time, since we + * verified uniqueness for all records. Also see RFC + * 4795, Section 2.7 */ + + r = manager_send(s->manager, fd, p->ifindex, p->family, &p->sender, p->sender_port, NULL, reply); + if (r < 0) { + log_debug_errno(r, "Failed to send reply packet: %m"); + return; + } + } +} + +DnsTransaction *dns_scope_find_transaction( + DnsScope *scope, + DnsResourceKey *key, + uint64_t query_flags) { + + DnsTransaction *first; + + assert(scope); + assert(key); + + /* Iterate through the list of transactions with a matching key */ + first = hashmap_get(scope->transactions_by_key, key); + LIST_FOREACH(transactions_by_key, t, first) { + + /* These four flags must match exactly: we cannot use a validated response for a + * non-validating client, and we cannot use a non-validated response for a validating + * client. Similar, if the sources don't match things aren't usable either. */ + if (((query_flags ^ t->query_flags) & + (SD_RESOLVED_NO_VALIDATE| + SD_RESOLVED_NO_ZONE| + SD_RESOLVED_NO_TRUST_ANCHOR| + SD_RESOLVED_NO_NETWORK)) != 0) + continue; + + /* We can reuse a primary query if a regular one is requested, but not vice versa */ + if ((query_flags & SD_RESOLVED_REQUIRE_PRIMARY) && + !(t->query_flags & SD_RESOLVED_REQUIRE_PRIMARY)) + continue; + + /* Don't reuse a transaction that allowed caching when we got told not to use it */ + if ((query_flags & SD_RESOLVED_NO_CACHE) && + !(t->query_flags & SD_RESOLVED_NO_CACHE)) + continue; + + /* If we are asked to clamp ttls an the existing transaction doesn't do it, we can't + * reuse */ + if ((query_flags & SD_RESOLVED_CLAMP_TTL) && + !(t->query_flags & SD_RESOLVED_CLAMP_TTL)) + continue; + + return t; + } + + return NULL; +} + +static int dns_scope_make_conflict_packet( + DnsScope *s, + DnsResourceRecord *rr, + DnsPacket **ret) { + + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + int r; + + assert(s); + assert(rr); + assert(ret); + + r = dns_packet_new(&p, s->protocol, 0, DNS_PACKET_SIZE_MAX); + if (r < 0) + return r; + + DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS( + 0 /* qr */, + 0 /* opcode */, + 1 /* conflict */, + 0 /* tc */, + 0 /* t */, + 0 /* (ra) */, + 0 /* (ad) */, + 0 /* (cd) */, + 0)); + + /* For mDNS, the transaction ID should always be 0 */ + if (s->protocol != DNS_PROTOCOL_MDNS) + random_bytes(&DNS_PACKET_HEADER(p)->id, sizeof(uint16_t)); + + DNS_PACKET_HEADER(p)->qdcount = htobe16(1); + DNS_PACKET_HEADER(p)->arcount = htobe16(1); + + r = dns_packet_append_key(p, rr->key, 0, NULL); + if (r < 0) + return r; + + r = dns_packet_append_rr(p, rr, 0, NULL, NULL); + if (r < 0) + return r; + + *ret = TAKE_PTR(p); + + return 0; +} + +static int on_conflict_dispatch(sd_event_source *es, usec_t usec, void *userdata) { + DnsScope *scope = ASSERT_PTR(userdata); + int r; + + assert(es); + + scope->conflict_event_source = sd_event_source_disable_unref(scope->conflict_event_source); + + for (;;) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + + key = ordered_hashmap_first_key(scope->conflict_queue); + if (!key) + break; + + rr = ordered_hashmap_remove(scope->conflict_queue, key); + assert(rr); + + r = dns_scope_make_conflict_packet(scope, rr, &p); + if (r < 0) { + log_error_errno(r, "Failed to make conflict packet: %m"); + return 0; + } + + r = dns_scope_emit_udp(scope, -1, AF_UNSPEC, p); + if (r < 0) + log_debug_errno(r, "Failed to send conflict packet: %m"); + } + + return 0; +} + +int dns_scope_notify_conflict(DnsScope *scope, DnsResourceRecord *rr) { + int r; + + assert(scope); + assert(rr); + + /* We don't send these queries immediately. Instead, we queue + * them, and send them after some jitter delay. */ + r = ordered_hashmap_ensure_allocated(&scope->conflict_queue, &dns_resource_key_hash_ops); + if (r < 0) { + log_oom(); + return r; + } + + /* We only place one RR per key in the conflict + * messages, not all of them. That should be enough to + * indicate where there might be a conflict */ + r = ordered_hashmap_put(scope->conflict_queue, rr->key, rr); + if (IN_SET(r, 0, -EEXIST)) + return 0; + if (r < 0) + return log_debug_errno(r, "Failed to queue conflicting RR: %m"); + + dns_resource_key_ref(rr->key); + dns_resource_record_ref(rr); + + if (scope->conflict_event_source) + return 0; + + r = sd_event_add_time_relative( + scope->manager->event, + &scope->conflict_event_source, + CLOCK_BOOTTIME, + random_u64_range(LLMNR_JITTER_INTERVAL_USEC), + 0, + on_conflict_dispatch, scope); + if (r < 0) + return log_debug_errno(r, "Failed to add conflict dispatch event: %m"); + + (void) sd_event_source_set_description(scope->conflict_event_source, "scope-conflict"); + + return 0; +} + +void dns_scope_check_conflicts(DnsScope *scope, DnsPacket *p) { + DnsResourceRecord *rr; + int r; + + assert(scope); + assert(p); + + if (!IN_SET(p->protocol, DNS_PROTOCOL_LLMNR, DNS_PROTOCOL_MDNS)) + return; + + if (DNS_PACKET_RRCOUNT(p) <= 0) + return; + + if (p->protocol == DNS_PROTOCOL_LLMNR) { + if (DNS_PACKET_LLMNR_C(p) != 0) + return; + + if (DNS_PACKET_LLMNR_T(p) != 0) + return; + } + + if (manager_packet_from_local_address(scope->manager, p)) + return; + + r = dns_packet_extract(p); + if (r < 0) { + log_debug_errno(r, "Failed to extract packet: %m"); + return; + } + + log_debug("Checking for conflicts..."); + + DNS_ANSWER_FOREACH(rr, p->answer) { + /* No conflict if it is DNS-SD RR used for service enumeration. */ + if (dns_resource_key_is_dnssd_ptr(rr->key)) + continue; + + /* Check for conflicts against the local zone. If we + * found one, we won't check any further */ + r = dns_zone_check_conflicts(&scope->zone, rr); + if (r != 0) + continue; + + /* Check for conflicts against the local cache. If so, + * send out an advisory query, to inform everybody */ + r = dns_cache_check_conflicts(&scope->cache, rr, p->family, &p->sender); + if (r <= 0) + continue; + + dns_scope_notify_conflict(scope, rr); + } +} + +void dns_scope_dump(DnsScope *s, FILE *f) { + assert(s); + + if (!f) + f = stdout; + + fputs("[Scope protocol=", f); + fputs(dns_protocol_to_string(s->protocol), f); + + if (s->link) { + fputs(" interface=", f); + fputs(s->link->ifname, f); + } + + if (s->family != AF_UNSPEC) { + fputs(" family=", f); + fputs(af_to_name(s->family), f); + } + + fputs("]\n", f); + + if (!dns_zone_is_empty(&s->zone)) { + fputs("ZONE:\n", f); + dns_zone_dump(&s->zone, f); + } + + if (!dns_cache_is_empty(&s->cache)) { + fputs("CACHE:\n", f); + dns_cache_dump(&s->cache, f); + } +} + +DnsSearchDomain *dns_scope_get_search_domains(DnsScope *s) { + assert(s); + + if (s->protocol != DNS_PROTOCOL_DNS) + return NULL; + + if (s->link) + return s->link->search_domains; + + return s->manager->search_domains; +} + +bool dns_scope_name_wants_search_domain(DnsScope *s, const char *name) { + assert(s); + + if (s->protocol != DNS_PROTOCOL_DNS) + return false; + + return dns_name_is_single_label(name); +} + +bool dns_scope_network_good(DnsScope *s) { + /* Checks whether the network is in good state for lookups on this scope. For mDNS/LLMNR/Classic DNS scopes + * bound to links this is easy, as they don't even exist if the link isn't in a suitable state. For the global + * DNS scope we check whether there are any links that are up and have an address. + * + * Note that Linux routing is complex and even systems that superficially have no IPv4 address might + * be able to route IPv4 (and similar for IPv6), hence let's make a check here independent of address + * family. */ + + if (s->link) + return true; + + return manager_routable(s->manager); +} + +int dns_scope_ifindex(DnsScope *s) { + assert(s); + + if (s->link) + return s->link->ifindex; + + return 0; +} + +static int on_announcement_timeout(sd_event_source *s, usec_t usec, void *userdata) { + DnsScope *scope = userdata; + + assert(s); + + scope->announce_event_source = sd_event_source_disable_unref(scope->announce_event_source); + + (void) dns_scope_announce(scope, false); + return 0; +} + +int dns_scope_announce(DnsScope *scope, bool goodbye) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + _cleanup_set_free_ Set *types = NULL; + DnsZoneItem *z; + unsigned size = 0; + char *service_type; + int r; + + if (!scope) + return 0; + + if (scope->protocol != DNS_PROTOCOL_MDNS) + return 0; + + r = sd_event_get_state(scope->manager->event); + if (r < 0) + return log_debug_errno(r, "Failed to get event loop state: %m"); + + /* If this is called on exit, through manager_free() -> link_free(), then we cannot announce. */ + if (r == SD_EVENT_FINISHED) + return 0; + + /* Check if we're done with probing. */ + LIST_FOREACH(transactions_by_scope, t, scope->transactions) + if (DNS_TRANSACTION_IS_LIVE(t->state)) + return 0; + + /* Check if there're services pending conflict resolution. */ + if (manager_next_dnssd_names(scope->manager)) + return 0; /* we reach this point only if changing hostname didn't help */ + + /* Calculate answer's size. */ + HASHMAP_FOREACH(z, scope->zone.by_key) { + if (z->state != DNS_ZONE_ITEM_ESTABLISHED) + continue; + + if (z->rr->key->type == DNS_TYPE_PTR && + !dns_zone_contains_name(&scope->zone, z->rr->ptr.name)) { + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + log_debug("Skip PTR RR <%s> since its counterparts seem to be withdrawn", dns_resource_key_to_string(z->rr->key, key_str, sizeof key_str)); + z->state = DNS_ZONE_ITEM_WITHDRAWN; + continue; + } + + /* Collect service types for _services._dns-sd._udp.local RRs in a set */ + if (!scope->announced && + dns_resource_key_is_dnssd_ptr(z->rr->key)) { + if (!set_contains(types, dns_resource_key_name(z->rr->key))) { + r = set_ensure_put(&types, &dns_name_hash_ops, dns_resource_key_name(z->rr->key)); + if (r < 0) + return log_debug_errno(r, "Failed to add item to set: %m"); + } + } + + LIST_FOREACH(by_key, i, z) + size++; + } + + answer = dns_answer_new(size + set_size(types)); + if (!answer) + return log_oom(); + + /* Second iteration, actually add RRs to the answer. */ + HASHMAP_FOREACH(z, scope->zone.by_key) + LIST_FOREACH (by_key, i, z) { + DnsAnswerFlags flags; + + if (i->state != DNS_ZONE_ITEM_ESTABLISHED) + continue; + + if (dns_resource_key_is_dnssd_ptr(i->rr->key)) + flags = goodbye ? DNS_ANSWER_GOODBYE : 0; + else + flags = goodbye ? (DNS_ANSWER_GOODBYE|DNS_ANSWER_CACHE_FLUSH) : DNS_ANSWER_CACHE_FLUSH; + + r = dns_answer_add(answer, i->rr, 0, flags, NULL); + if (r < 0) + return log_debug_errno(r, "Failed to add RR to announce: %m"); + } + + /* Since all the active services are in the zone make them discoverable now. */ + SET_FOREACH(service_type, types) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR, + "_services._dns-sd._udp.local"); + if (!rr) + return log_oom(); + + rr->ptr.name = strdup(service_type); + if (!rr->ptr.name) + return log_oom(); + + rr->ttl = MDNS_DEFAULT_TTL; + + r = dns_zone_put(&scope->zone, scope, rr, false); + if (r < 0) + log_warning_errno(r, "Failed to add DNS-SD PTR record to MDNS zone, ignoring: %m"); + + r = dns_answer_add(answer, rr, 0, 0, NULL); + if (r < 0) + return log_debug_errno(r, "Failed to add RR to announce: %m"); + } + + if (dns_answer_isempty(answer)) + return 0; + + r = dns_scope_make_reply_packet(scope, 0, DNS_RCODE_SUCCESS, NULL, answer, NULL, false, &p); + if (r < 0) + return log_debug_errno(r, "Failed to build reply packet: %m"); + + r = dns_scope_emit_udp(scope, -1, AF_UNSPEC, p); + if (r < 0) + return log_debug_errno(r, "Failed to send reply packet: %m"); + + /* In section 8.3 of RFC6762: "The Multicast DNS responder MUST send at least two unsolicited + * responses, one second apart." */ + if (!scope->announced) { + scope->announced = true; + + r = sd_event_add_time_relative( + scope->manager->event, + &scope->announce_event_source, + CLOCK_BOOTTIME, + MDNS_ANNOUNCE_DELAY, + 0, + on_announcement_timeout, scope); + if (r < 0) + return log_debug_errno(r, "Failed to schedule second announcement: %m"); + + (void) sd_event_source_set_description(scope->announce_event_source, "mdns-announce"); + } + + return 0; +} + +int dns_scope_add_dnssd_services(DnsScope *scope) { + DnssdService *service; + int r; + + assert(scope); + + if (hashmap_size(scope->manager->dnssd_services) == 0) + return 0; + + scope->announced = false; + + HASHMAP_FOREACH(service, scope->manager->dnssd_services) { + service->withdrawn = false; + + r = dns_zone_put(&scope->zone, scope, service->ptr_rr, false); + if (r < 0) + log_warning_errno(r, "Failed to add PTR record to MDNS zone: %m"); + + r = dns_zone_put(&scope->zone, scope, service->srv_rr, true); + if (r < 0) + log_warning_errno(r, "Failed to add SRV record to MDNS zone: %m"); + + LIST_FOREACH(items, txt_data, service->txt_data_items) { + r = dns_zone_put(&scope->zone, scope, txt_data->rr, true); + if (r < 0) + log_warning_errno(r, "Failed to add TXT record to MDNS zone: %m"); + } + } + + return 0; +} + +int dns_scope_remove_dnssd_services(DnsScope *scope) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + DnssdService *service; + int r; + + assert(scope); + + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_PTR, + "_services._dns-sd._udp.local"); + if (!key) + return log_oom(); + + r = dns_zone_remove_rrs_by_key(&scope->zone, key); + if (r < 0) + return r; + + HASHMAP_FOREACH(service, scope->manager->dnssd_services) { + dns_zone_remove_rr(&scope->zone, service->ptr_rr); + dns_zone_remove_rr(&scope->zone, service->srv_rr); + LIST_FOREACH(items, txt_data, service->txt_data_items) + dns_zone_remove_rr(&scope->zone, txt_data->rr); + } + + return 0; +} + +static bool dns_scope_has_route_only_domains(DnsScope *scope) { + DnsSearchDomain *first; + bool route_only = false; + + assert(scope); + assert(scope->protocol == DNS_PROTOCOL_DNS); + + /* Returns 'true' if this scope is suitable for queries to specific domains only. For that we check + * if there are any route-only domains on this interface, as a heuristic to discern VPN-style links + * from non-VPN-style links. Returns 'false' for all other cases, i.e. if the scope is intended to + * take queries to arbitrary domains, i.e. has no routing domains set. */ + + if (scope->link) + first = scope->link->search_domains; + else + first = scope->manager->search_domains; + + LIST_FOREACH(domains, domain, first) { + /* "." means "any domain", thus the interface takes any kind of traffic. Thus, we exit early + * here, as it doesn't really matter whether this link has any route-only domains or not, + * "~." really trumps everything and clearly indicates that this interface shall receive all + * traffic it can get. */ + if (dns_name_is_root(DNS_SEARCH_DOMAIN_NAME(domain))) + return false; + + if (domain->route_only) + route_only = true; + } + + return route_only; +} + +bool dns_scope_is_default_route(DnsScope *scope) { + assert(scope); + + /* Only use DNS scopes as default routes */ + if (scope->protocol != DNS_PROTOCOL_DNS) + return false; + + /* The global DNS scope is always suitable as default route */ + if (!scope->link) + return true; + + /* Honour whatever is explicitly configured. This is really the best approach, and trumps any + * automatic logic. */ + if (scope->link->default_route >= 0) + return scope->link->default_route; + + /* Otherwise check if we have any route-only domains, as a sensible heuristic: if so, let's not + * volunteer as default route. */ + return !dns_scope_has_route_only_domains(scope); +} diff --git a/src/resolve/resolved-dns-scope.h b/src/resolve/resolved-dns-scope.h new file mode 100644 index 0000000..1f9d22b --- /dev/null +++ b/src/resolve/resolved-dns-scope.h @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "list.h" +#include "ratelimit.h" + +typedef struct DnsQueryCandidate DnsQueryCandidate; +typedef struct DnsScope DnsScope; + +#include "resolved-dns-cache.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-query.h" +#include "resolved-dns-search-domain.h" +#include "resolved-dns-server.h" +#include "resolved-dns-stream.h" +#include "resolved-dns-zone.h" + +typedef enum DnsScopeMatch { + DNS_SCOPE_NO, + DNS_SCOPE_MAYBE, + DNS_SCOPE_YES_BASE, /* Add the number of matching labels to this */ + DNS_SCOPE_YES_END = DNS_SCOPE_YES_BASE + DNS_N_LABELS_MAX, + _DNS_SCOPE_MATCH_MAX, + _DNS_SCOPE_MATCH_INVALID = -EINVAL, +} DnsScopeMatch; + +struct DnsScope { + Manager *manager; + + DnsProtocol protocol; + int family; + + /* Copied at scope creation time from the link/manager */ + DnssecMode dnssec_mode; + DnsOverTlsMode dns_over_tls_mode; + + Link *link; + + DnsCache cache; + DnsZone zone; + + OrderedHashmap *conflict_queue; + sd_event_source *conflict_event_source; + + sd_event_source *announce_event_source; + + RateLimit ratelimit; + + usec_t resend_timeout; + usec_t max_rtt; + + LIST_HEAD(DnsQueryCandidate, query_candidates); + + /* Note that we keep track of ongoing transactions in two ways: once in a hashmap, indexed by the rr + * key, and once in a linked list. We use the hashmap to quickly find transactions we can reuse for a + * key. But note that there might be multiple transactions for the same key (because the associated + * query flags might differ in incompatible ways: e.g. we may not reuse a non-validating transaction + * as validating. Hence we maintain a per-key list of transactions, which we iterate through to find + * one we can reuse with matching flags. */ + Hashmap *transactions_by_key; + LIST_HEAD(DnsTransaction, transactions); + + LIST_FIELDS(DnsScope, scopes); + + bool announced; +}; + +int dns_scope_new(Manager *m, DnsScope **ret, Link *l, DnsProtocol p, int family); +DnsScope* dns_scope_free(DnsScope *s); + +void dns_scope_packet_received(DnsScope *s, usec_t rtt); +void dns_scope_packet_lost(DnsScope *s, usec_t usec); + +int dns_scope_emit_udp(DnsScope *s, int fd, int af, DnsPacket *p); +int dns_scope_socket_tcp(DnsScope *s, int family, const union in_addr_union *address, DnsServer *server, uint16_t port, union sockaddr_union *ret_socket_address); +int dns_scope_socket_udp(DnsScope *s, DnsServer *server); + +DnsScopeMatch dns_scope_good_domain(DnsScope *s, DnsQuery *q); +bool dns_scope_good_key(DnsScope *s, const DnsResourceKey *key); + +DnsServer *dns_scope_get_dns_server(DnsScope *s); +unsigned dns_scope_get_n_dns_servers(DnsScope *s); +void dns_scope_next_dns_server(DnsScope *s, DnsServer *if_current); + +int dns_scope_llmnr_membership(DnsScope *s, bool b); +int dns_scope_mdns_membership(DnsScope *s, bool b); + +int dns_scope_make_reply_packet(DnsScope *s, uint16_t id, int rcode, DnsQuestion *q, DnsAnswer *answer, DnsAnswer *soa, bool tentative, DnsPacket **ret); +void dns_scope_process_query(DnsScope *s, DnsStream *stream, DnsPacket *p); + +DnsTransaction *dns_scope_find_transaction(DnsScope *scope, DnsResourceKey *key, uint64_t query_flags); + +int dns_scope_notify_conflict(DnsScope *scope, DnsResourceRecord *rr); +void dns_scope_check_conflicts(DnsScope *scope, DnsPacket *p); + +void dns_scope_dump(DnsScope *s, FILE *f); + +DnsSearchDomain *dns_scope_get_search_domains(DnsScope *s); + +bool dns_scope_name_wants_search_domain(DnsScope *s, const char *name); + +bool dns_scope_network_good(DnsScope *s); + +int dns_scope_ifindex(DnsScope *s); + +int dns_scope_announce(DnsScope *scope, bool goodbye); + +int dns_scope_add_dnssd_services(DnsScope *scope); +int dns_scope_remove_dnssd_services(DnsScope *scope); + +bool dns_scope_is_default_route(DnsScope *scope); diff --git a/src/resolve/resolved-dns-search-domain.c b/src/resolve/resolved-dns-search-domain.c new file mode 100644 index 0000000..647c0bd --- /dev/null +++ b/src/resolve/resolved-dns-search-domain.c @@ -0,0 +1,199 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "dns-domain.h" +#include "resolved-dns-search-domain.h" +#include "resolved-link.h" +#include "resolved-manager.h" + +int dns_search_domain_new( + Manager *m, + DnsSearchDomain **ret, + DnsSearchDomainType type, + Link *l, + const char *name) { + + _cleanup_free_ char *normalized = NULL; + DnsSearchDomain *d; + int r; + + assert(m); + assert((type == DNS_SEARCH_DOMAIN_LINK) == !!l); + assert(name); + + r = dns_name_normalize(name, 0, &normalized); + if (r < 0) + return r; + + if (l) { + if (l->n_search_domains >= LINK_SEARCH_DOMAINS_MAX) + return -E2BIG; + } else { + if (m->n_search_domains >= MANAGER_SEARCH_DOMAINS_MAX) + return -E2BIG; + } + + d = new(DnsSearchDomain, 1); + if (!d) + return -ENOMEM; + + *d = (DnsSearchDomain) { + .n_ref = 1, + .manager = m, + .type = type, + .name = TAKE_PTR(normalized), + }; + + switch (type) { + + case DNS_SEARCH_DOMAIN_LINK: + d->link = l; + LIST_APPEND(domains, l->search_domains, d); + l->n_search_domains++; + break; + + case DNS_SEARCH_DOMAIN_SYSTEM: + LIST_APPEND(domains, m->search_domains, d); + m->n_search_domains++; + break; + + default: + assert_not_reached(); + } + + d->linked = true; + + if (ret) + *ret = d; + + return 0; +} + +static DnsSearchDomain* dns_search_domain_free(DnsSearchDomain *d) { + assert(d); + + free(d->name); + return mfree(d); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsSearchDomain, dns_search_domain, dns_search_domain_free); + +void dns_search_domain_unlink(DnsSearchDomain *d) { + assert(d); + assert(d->manager); + + if (!d->linked) + return; + + switch (d->type) { + + case DNS_SEARCH_DOMAIN_LINK: + assert(d->link); + assert(d->link->n_search_domains > 0); + LIST_REMOVE(domains, d->link->search_domains, d); + d->link->n_search_domains--; + break; + + case DNS_SEARCH_DOMAIN_SYSTEM: + assert(d->manager->n_search_domains > 0); + LIST_REMOVE(domains, d->manager->search_domains, d); + d->manager->n_search_domains--; + break; + } + + d->linked = false; + + dns_search_domain_unref(d); +} + +void dns_search_domain_move_back_and_unmark(DnsSearchDomain *d) { + DnsSearchDomain *tail; + + assert(d); + + if (!d->marked) + return; + + d->marked = false; + + if (!d->linked || !d->domains_next) + return; + + switch (d->type) { + + case DNS_SEARCH_DOMAIN_LINK: + assert(d->link); + LIST_FIND_TAIL(domains, d, tail); + LIST_REMOVE(domains, d->link->search_domains, d); + LIST_INSERT_AFTER(domains, d->link->search_domains, tail, d); + break; + + case DNS_SEARCH_DOMAIN_SYSTEM: + LIST_FIND_TAIL(domains, d, tail); + LIST_REMOVE(domains, d->manager->search_domains, d); + LIST_INSERT_AFTER(domains, d->manager->search_domains, tail, d); + break; + + default: + assert_not_reached(); + } +} + +void dns_search_domain_unlink_all(DnsSearchDomain *first) { + DnsSearchDomain *next; + + if (!first) + return; + + next = first->domains_next; + dns_search_domain_unlink(first); + + dns_search_domain_unlink_all(next); +} + +bool dns_search_domain_unlink_marked(DnsSearchDomain *first) { + DnsSearchDomain *next; + bool changed; + + if (!first) + return false; + + next = first->domains_next; + + if (first->marked) { + dns_search_domain_unlink(first); + changed = true; + } else + changed = false; + + return dns_search_domain_unlink_marked(next) || changed; +} + +void dns_search_domain_mark_all(DnsSearchDomain *first) { + if (!first) + return; + + first->marked = true; + dns_search_domain_mark_all(first->domains_next); +} + +int dns_search_domain_find(DnsSearchDomain *first, const char *name, DnsSearchDomain **ret) { + int r; + + assert(name); + assert(ret); + + LIST_FOREACH(domains, d, first) { + + r = dns_name_equal(name, d->name); + if (r < 0) + return r; + if (r > 0) { + *ret = d; + return 1; + } + } + + *ret = NULL; + return 0; +} diff --git a/src/resolve/resolved-dns-search-domain.h b/src/resolve/resolved-dns-search-domain.h new file mode 100644 index 0000000..f0d96ac --- /dev/null +++ b/src/resolve/resolved-dns-search-domain.h @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "list.h" +#include "macro.h" + +typedef struct DnsSearchDomain DnsSearchDomain; +typedef struct Link Link; +typedef struct Manager Manager; + +typedef enum DnsSearchDomainType { + DNS_SEARCH_DOMAIN_SYSTEM, + DNS_SEARCH_DOMAIN_LINK, +} DnsSearchDomainType; + +struct DnsSearchDomain { + Manager *manager; + + unsigned n_ref; + + DnsSearchDomainType type; + Link *link; + + char *name; + + bool marked:1; + bool route_only:1; + + bool linked:1; + LIST_FIELDS(DnsSearchDomain, domains); +}; + +int dns_search_domain_new( + Manager *m, + DnsSearchDomain **ret, + DnsSearchDomainType type, + Link *link, + const char *name); + +DnsSearchDomain* dns_search_domain_ref(DnsSearchDomain *d); +DnsSearchDomain* dns_search_domain_unref(DnsSearchDomain *d); + +void dns_search_domain_unlink(DnsSearchDomain *d); +void dns_search_domain_move_back_and_unmark(DnsSearchDomain *d); + +void dns_search_domain_unlink_all(DnsSearchDomain *first); +bool dns_search_domain_unlink_marked(DnsSearchDomain *first); +void dns_search_domain_mark_all(DnsSearchDomain *first); + +int dns_search_domain_find(DnsSearchDomain *first, const char *name, DnsSearchDomain **ret); + +static inline const char* DNS_SEARCH_DOMAIN_NAME(DnsSearchDomain *d) { + return d ? d->name : NULL; +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsSearchDomain*, dns_search_domain_unref); diff --git a/src/resolve/resolved-dns-server.c b/src/resolve/resolved-dns-server.c new file mode 100644 index 0000000..8ff513f --- /dev/null +++ b/src/resolve/resolved-dns-server.c @@ -0,0 +1,1097 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-messages.h" + +#include "alloc-util.h" +#include "resolved-bus.h" +#include "resolved-dns-server.h" +#include "resolved-dns-stub.h" +#include "resolved-manager.h" +#include "resolved-resolv-conf.h" +#include "siphash24.h" +#include "string-table.h" +#include "string-util.h" + +/* The amount of time to wait before retrying with a full feature set */ +#define DNS_SERVER_FEATURE_GRACE_PERIOD_MAX_USEC (6 * USEC_PER_HOUR) +#define DNS_SERVER_FEATURE_GRACE_PERIOD_MIN_USEC (5 * USEC_PER_MINUTE) + +/* The number of times we will attempt a certain feature set before degrading */ +#define DNS_SERVER_FEATURE_RETRY_ATTEMPTS 3 + +int dns_server_new( + Manager *m, + DnsServer **ret, + DnsServerType type, + Link *l, + int family, + const union in_addr_union *in_addr, + uint16_t port, + int ifindex, + const char *server_name) { + + _cleanup_free_ char *name = NULL; + DnsServer *s; + + assert(m); + assert((type == DNS_SERVER_LINK) == !!l); + assert(in_addr); + + if (!IN_SET(family, AF_INET, AF_INET6)) + return -EAFNOSUPPORT; + + if (l) { + if (l->n_dns_servers >= LINK_DNS_SERVERS_MAX) + return -E2BIG; + } else { + if (m->n_dns_servers >= MANAGER_DNS_SERVERS_MAX) + return -E2BIG; + } + + if (!isempty(server_name)) { + name = strdup(server_name); + if (!name) + return -ENOMEM; + } + + s = new(DnsServer, 1); + if (!s) + return -ENOMEM; + + *s = (DnsServer) { + .n_ref = 1, + .manager = m, + .type = type, + .family = family, + .address = *in_addr, + .port = port, + .ifindex = ifindex, + .server_name = TAKE_PTR(name), + }; + + dns_server_reset_features(s); + + switch (type) { + + case DNS_SERVER_LINK: + s->link = l; + LIST_APPEND(servers, l->dns_servers, s); + l->n_dns_servers++; + break; + + case DNS_SERVER_SYSTEM: + LIST_APPEND(servers, m->dns_servers, s); + m->n_dns_servers++; + break; + + case DNS_SERVER_FALLBACK: + LIST_APPEND(servers, m->fallback_dns_servers, s); + m->n_dns_servers++; + break; + + default: + assert_not_reached(); + } + + s->linked = true; + + /* A new DNS server that isn't fallback is added and the one + * we used so far was a fallback one? Then let's try to pick + * the new one */ + if (type != DNS_SERVER_FALLBACK && + m->current_dns_server && + m->current_dns_server->type == DNS_SERVER_FALLBACK) + manager_set_dns_server(m, NULL); + + if (ret) + *ret = s; + + return 0; +} + +static DnsServer* dns_server_free(DnsServer *s) { + assert(s); + + dns_server_unref_stream(s); + +#if ENABLE_DNS_OVER_TLS + dnstls_server_free(s); +#endif + + free(s->server_string); + free(s->server_string_full); + free(s->server_name); + return mfree(s); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsServer, dns_server, dns_server_free); + +void dns_server_unlink(DnsServer *s) { + assert(s); + assert(s->manager); + + /* This removes the specified server from the linked list of + * servers, but any server might still stay around if it has + * refs, for example from an ongoing transaction. */ + + if (!s->linked) + return; + + switch (s->type) { + + case DNS_SERVER_LINK: + assert(s->link); + assert(s->link->n_dns_servers > 0); + LIST_REMOVE(servers, s->link->dns_servers, s); + s->link->n_dns_servers--; + break; + + case DNS_SERVER_SYSTEM: + assert(s->manager->n_dns_servers > 0); + LIST_REMOVE(servers, s->manager->dns_servers, s); + s->manager->n_dns_servers--; + break; + + case DNS_SERVER_FALLBACK: + assert(s->manager->n_dns_servers > 0); + LIST_REMOVE(servers, s->manager->fallback_dns_servers, s); + s->manager->n_dns_servers--; + break; + default: + assert_not_reached(); + } + + s->linked = false; + + if (s->link && s->link->current_dns_server == s) + link_set_dns_server(s->link, NULL); + + if (s->manager->current_dns_server == s) + manager_set_dns_server(s->manager, NULL); + + /* No need to keep a default stream around anymore */ + dns_server_unref_stream(s); + + dns_server_unref(s); +} + +void dns_server_move_back_and_unmark(DnsServer *s) { + DnsServer *tail; + + assert(s); + + if (!s->marked) + return; + + s->marked = false; + + if (!s->linked || !s->servers_next) + return; + + /* Move us to the end of the list, so that the order is + * strictly kept, if we are not at the end anyway. */ + + switch (s->type) { + + case DNS_SERVER_LINK: + assert(s->link); + LIST_FIND_TAIL(servers, s, tail); + LIST_REMOVE(servers, s->link->dns_servers, s); + LIST_INSERT_AFTER(servers, s->link->dns_servers, tail, s); + break; + + case DNS_SERVER_SYSTEM: + LIST_FIND_TAIL(servers, s, tail); + LIST_REMOVE(servers, s->manager->dns_servers, s); + LIST_INSERT_AFTER(servers, s->manager->dns_servers, tail, s); + break; + + case DNS_SERVER_FALLBACK: + LIST_FIND_TAIL(servers, s, tail); + LIST_REMOVE(servers, s->manager->fallback_dns_servers, s); + LIST_INSERT_AFTER(servers, s->manager->fallback_dns_servers, tail, s); + break; + + default: + assert_not_reached(); + } +} + +static void dns_server_verified(DnsServer *s, DnsServerFeatureLevel level) { + assert(s); + + if (s->verified_feature_level > level) + return; + + if (s->verified_feature_level != level) { + log_debug("Verified we get a response at feature level %s from DNS server %s.", + dns_server_feature_level_to_string(level), + strna(dns_server_string_full(s))); + s->verified_feature_level = level; + } + + assert_se(sd_event_now(s->manager->event, CLOCK_BOOTTIME, &s->verified_usec) >= 0); +} + +static void dns_server_reset_counters(DnsServer *s) { + assert(s); + + s->n_failed_udp = 0; + s->n_failed_tcp = 0; + s->n_failed_tls = 0; + s->packet_truncated = false; + s->packet_invalid = false; + s->verified_usec = 0; + + /* Note that we do not reset s->packet_bad_opt and s->packet_rrsig_missing here. We reset them only when the + * grace period ends, but not when lowering the possible feature level, as a lower level feature level should + * not make RRSIGs appear or OPT appear, but rather make them disappear. If the reappear anyway, then that's + * indication for a differently broken OPT/RRSIG implementation, and we really don't want to support that + * either. + * + * This is particularly important to deal with certain Belkin routers which break OPT for certain lookups (A), + * but pass traffic through for others (AAAA). If we detect the broken behaviour on one lookup we should not + * re-enable it for another, because we cannot validate things anyway, given that the RRSIG/OPT data will be + * incomplete. */ +} + +void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t fragsize) { + assert(s); + + if (protocol == IPPROTO_UDP) { + if (s->possible_feature_level == level) + s->n_failed_udp = 0; + } else if (protocol == IPPROTO_TCP) { + if (DNS_SERVER_FEATURE_LEVEL_IS_TLS(level)) { + if (s->possible_feature_level == level) + s->n_failed_tls = 0; + } else { + if (s->possible_feature_level == level) + s->n_failed_tcp = 0; + + /* Successful TCP connections are only useful to verify the TCP feature level. */ + level = DNS_SERVER_FEATURE_LEVEL_TCP; + } + } + + /* If the RRSIG data is missing, then we can only validate EDNS0 at max */ + if (s->packet_rrsig_missing && level >= DNS_SERVER_FEATURE_LEVEL_DO) + level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN : DNS_SERVER_FEATURE_LEVEL_EDNS0; + + /* If the OPT RR got lost, then we can only validate UDP at max */ + if (s->packet_bad_opt && level >= DNS_SERVER_FEATURE_LEVEL_EDNS0) + level = DNS_SERVER_FEATURE_LEVEL_EDNS0 - 1; + + dns_server_verified(s, level); + + /* Remember the size of the largest UDP packet fragment we received from a server, we know that we + * can always announce support for packets with at least this size. */ + if (protocol == IPPROTO_UDP && s->received_udp_fragment_max < fragsize) + s->received_udp_fragment_max = fragsize; +} + +void dns_server_packet_lost(DnsServer *s, int protocol, DnsServerFeatureLevel level) { + assert(s); + assert(s->manager); + + if (s->possible_feature_level != level) + return; + + if (protocol == IPPROTO_UDP) + s->n_failed_udp++; + else if (protocol == IPPROTO_TCP) { + if (DNS_SERVER_FEATURE_LEVEL_IS_TLS(level)) + s->n_failed_tls++; + else + s->n_failed_tcp++; + } +} + +void dns_server_packet_truncated(DnsServer *s, DnsServerFeatureLevel level) { + assert(s); + + /* Invoked whenever we get a packet with TC bit set. */ + + if (s->possible_feature_level != level) + return; + + s->packet_truncated = true; +} + +void dns_server_packet_rrsig_missing(DnsServer *s, DnsServerFeatureLevel level) { + assert(s); + + if (level < DNS_SERVER_FEATURE_LEVEL_DO) + return; + + /* If the RRSIG RRs are missing, we have to downgrade what we previously verified */ + if (s->verified_feature_level >= DNS_SERVER_FEATURE_LEVEL_DO) + s->verified_feature_level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN : DNS_SERVER_FEATURE_LEVEL_EDNS0; + + s->packet_rrsig_missing = true; +} + +void dns_server_packet_bad_opt(DnsServer *s, DnsServerFeatureLevel level) { + assert(s); + + if (level < DNS_SERVER_FEATURE_LEVEL_EDNS0) + return; + + /* If the OPT RR got lost, we have to downgrade what we previously verified */ + if (s->verified_feature_level >= DNS_SERVER_FEATURE_LEVEL_EDNS0) + s->verified_feature_level = DNS_SERVER_FEATURE_LEVEL_EDNS0-1; + + s->packet_bad_opt = true; +} + +void dns_server_packet_rcode_downgrade(DnsServer *s, DnsServerFeatureLevel level) { + assert(s); + + /* Invoked whenever we got a FORMERR, SERVFAIL or NOTIMP rcode from a server and downgrading the feature level + * for the transaction made it go away. In this case we immediately downgrade to the feature level that made + * things work. */ + + if (s->verified_feature_level > level) + s->verified_feature_level = level; + + if (s->possible_feature_level > level) { + s->possible_feature_level = level; + dns_server_reset_counters(s); + log_debug("Downgrading transaction feature level fixed an RCODE error, downgrading server %s too.", strna(dns_server_string_full(s))); + } +} + +void dns_server_packet_invalid(DnsServer *s, DnsServerFeatureLevel level) { + assert(s); + + /* Invoked whenever we got a packet we couldn't parse at all */ + + if (s->possible_feature_level != level) + return; + + s->packet_invalid = true; +} + +void dns_server_packet_do_off(DnsServer *s, DnsServerFeatureLevel level) { + assert(s); + + /* Invoked whenever the DO flag was not copied from our request to the response. */ + + if (s->possible_feature_level != level) + return; + + s->packet_do_off = true; +} + +void dns_server_packet_udp_fragmented(DnsServer *s, size_t fragsize) { + assert(s); + + /* Invoked whenever we got a fragmented UDP packet. Let's do two things: keep track of the largest + * fragment we ever received from the server, and remember this, so that we can use it to lower the + * advertised packet size in EDNS0 */ + + if (s->received_udp_fragment_max < fragsize) + s->received_udp_fragment_max = fragsize; + + s->packet_fragmented = true; +} + +static bool dns_server_grace_period_expired(DnsServer *s) { + usec_t ts; + + assert(s); + assert(s->manager); + + if (s->verified_usec == 0) + return false; + + assert_se(sd_event_now(s->manager->event, CLOCK_BOOTTIME, &ts) >= 0); + + if (s->verified_usec + s->features_grace_period_usec > ts) + return false; + + s->features_grace_period_usec = MIN(s->features_grace_period_usec * 2, DNS_SERVER_FEATURE_GRACE_PERIOD_MAX_USEC); + + return true; +} + +DnsServerFeatureLevel dns_server_possible_feature_level(DnsServer *s) { + DnsServerFeatureLevel best; + + assert(s); + + /* Determine the best feature level we care about. If DNSSEC mode is off there's no point in using anything + * better than EDNS0, hence don't even try. */ + if (dns_server_get_dnssec_mode(s) != DNSSEC_NO) + best = dns_server_get_dns_over_tls_mode(s) == DNS_OVER_TLS_NO ? + DNS_SERVER_FEATURE_LEVEL_DO : + DNS_SERVER_FEATURE_LEVEL_TLS_DO; + else + best = dns_server_get_dns_over_tls_mode(s) == DNS_OVER_TLS_NO ? + DNS_SERVER_FEATURE_LEVEL_EDNS0 : + DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN; + + /* Clamp the feature level the highest level we care about. The DNSSEC mode might have changed since the last + * time, hence let's downgrade if we are still at a higher level. */ + if (s->possible_feature_level > best) + s->possible_feature_level = best; + + if (s->possible_feature_level < best && dns_server_grace_period_expired(s)) { + + s->possible_feature_level = best; + + dns_server_reset_counters(s); + + s->packet_bad_opt = false; + s->packet_rrsig_missing = false; + + log_info("Grace period over, resuming full feature set (%s) for DNS server %s.", + dns_server_feature_level_to_string(s->possible_feature_level), + strna(dns_server_string_full(s))); + + dns_server_flush_cache(s); + + } else if (s->possible_feature_level <= s->verified_feature_level) + s->possible_feature_level = s->verified_feature_level; + else { + DnsServerFeatureLevel p = s->possible_feature_level; + int log_level = LOG_WARNING; + + if (s->n_failed_tcp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS && + s->possible_feature_level == DNS_SERVER_FEATURE_LEVEL_TCP) { + + /* We are at the TCP (lowest) level, and we tried a couple of TCP connections, and it didn't + * work. Upgrade back to UDP again. */ + log_debug("Reached maximum number of failed TCP connection attempts, trying UDP again..."); + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_UDP; + + } else if (s->n_failed_tls > 0 && + DNS_SERVER_FEATURE_LEVEL_IS_TLS(s->possible_feature_level) && + dns_server_get_dns_over_tls_mode(s) != DNS_OVER_TLS_YES) { + + /* We tried to connect using DNS-over-TLS, and it didn't work. Downgrade to plaintext UDP + * if we don't require DNS-over-TLS */ + + log_debug("Server doesn't support DNS-over-TLS, downgrading protocol..."); + s->possible_feature_level--; + + } else if (s->packet_invalid && + s->possible_feature_level > DNS_SERVER_FEATURE_LEVEL_UDP && + s->possible_feature_level != DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN) { + + /* Downgrade from DO to EDNS0 + from EDNS0 to UDP, from TLS+DO to plain TLS. Or in + * other words, if we receive a packet we cannot parse jump to the next lower feature + * level that actually has an influence on the packet layout (and not just the + * transport). */ + + log_debug("Got invalid packet from server, downgrading protocol..."); + s->possible_feature_level = + s->possible_feature_level == DNS_SERVER_FEATURE_LEVEL_TLS_DO ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN : + DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) ? DNS_SERVER_FEATURE_LEVEL_EDNS0 : + DNS_SERVER_FEATURE_LEVEL_UDP; + + } else if (s->packet_bad_opt && + DNS_SERVER_FEATURE_LEVEL_IS_EDNS0(s->possible_feature_level) && + dns_server_get_dnssec_mode(s) != DNSSEC_YES && + dns_server_get_dns_over_tls_mode(s) != DNS_OVER_TLS_YES) { + + /* A reply to one of our EDNS0 queries didn't carry a valid OPT RR, then downgrade to + * below EDNS0 levels. After all, some servers generate different responses with and + * without OPT RR in the request. Example: + * + * https://open.nlnetlabs.nl/pipermail/dnssec-trigger/2014-November/000376.html + * + * If we are in strict DNSSEC or DoT mode, we don't do this kind of downgrade + * however, as both modes imply EDNS0 to work (DNSSEC strictly requires it, and DoT + * only in our implementation). */ + + log_debug("Server doesn't support EDNS(0) properly, downgrading feature level..."); + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_UDP; + + /* Users often don't control the DNS server they use so let's not complain too loudly + * when we can't use EDNS because the DNS server doesn't support it. */ + log_level = LOG_NOTICE; + + } else if (s->packet_do_off && + DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) && + dns_server_get_dnssec_mode(s) != DNSSEC_YES) { + + /* The server didn't copy the DO bit from request to response, thus DNSSEC is not + * correctly implemented, let's downgrade if that's allowed. */ + + log_debug("Detected server didn't copy DO flag from request to response, downgrading feature level..."); + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(s->possible_feature_level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN : + DNS_SERVER_FEATURE_LEVEL_EDNS0; + + } else if (s->packet_rrsig_missing && + DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) && + dns_server_get_dnssec_mode(s) != DNSSEC_YES) { + + /* RRSIG data was missing on an EDNS0 packet with DO bit set. This means the server + * doesn't augment responses with DNSSEC RRs. If so, let's better not ask the server + * for it anymore, after all some servers generate different replies depending if an + * OPT RR is in the query or not. If we are in strict DNSSEC mode, don't allow such + * downgrades however, since a DNSSEC feature level is a requirement for strict + * DNSSEC mode. */ + + log_debug("Detected server responses lack RRSIG records, downgrading feature level..."); + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(s->possible_feature_level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN : + DNS_SERVER_FEATURE_LEVEL_EDNS0; + + } else if (s->n_failed_udp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS && + DNS_SERVER_FEATURE_LEVEL_IS_UDP(s->possible_feature_level) && + ((s->possible_feature_level != DNS_SERVER_FEATURE_LEVEL_DO) || dns_server_get_dnssec_mode(s) != DNSSEC_YES)) { + + /* We lost too many UDP packets in a row, and are on an UDP feature level. If the + * packets are lost, maybe the server cannot parse them, hence downgrading sounds + * like a good idea. We might downgrade all the way down to TCP this way. + * + * If strict DNSSEC mode is used we won't downgrade below DO level however, as packet loss + * might have many reasons, a broken DNSSEC implementation being only one reason. And if the + * user is strict on DNSSEC, then let's assume that DNSSEC is not the fault here. */ + + log_debug("Lost too many UDP packets, downgrading feature level..."); + if (s->possible_feature_level == DNS_SERVER_FEATURE_LEVEL_DO) /* skip over TLS_PLAIN */ + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_EDNS0; + else + s->possible_feature_level--; + + } else if (s->n_failed_tcp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS && + s->packet_truncated && + s->possible_feature_level > DNS_SERVER_FEATURE_LEVEL_UDP && + DNS_SERVER_FEATURE_LEVEL_IS_UDP(s->possible_feature_level) && + (!DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) || dns_server_get_dnssec_mode(s) != DNSSEC_YES)) { + + /* We got too many TCP connection failures in a row, we had at least one truncated + * packet, and are on feature level above UDP. By downgrading things and getting rid + * of DNSSEC or EDNS0 data we hope to make the packet smaller, so that it still + * works via UDP given that TCP appears not to be a fallback. Note that if we are + * already at the lowest UDP level, we don't go further down, since that's TCP, and + * TCP failed too often after all. */ + + log_debug("Got too many failed TCP connection failures and truncated UDP packets, downgrading feature level..."); + + if (DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level)) + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_EDNS0; /* Go DNSSEC → EDNS0 */ + else + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_UDP; /* Go EDNS0 → UDP */ + } + + if (p != s->possible_feature_level) { + + /* We changed the feature level, reset the counting */ + dns_server_reset_counters(s); + + log_full(log_level, "Using degraded feature set %s instead of %s for DNS server %s.", + dns_server_feature_level_to_string(s->possible_feature_level), + dns_server_feature_level_to_string(p), strna(dns_server_string_full(s))); + } + } + + return s->possible_feature_level; +} + +int dns_server_adjust_opt(DnsServer *server, DnsPacket *packet, DnsServerFeatureLevel level) { + size_t packet_size, udp_size; + bool edns_do; + int r; + + assert(server); + assert(packet); + assert(packet->protocol == DNS_PROTOCOL_DNS); + + /* Fix the OPT field in the packet to match our current feature level. */ + + r = dns_packet_truncate_opt(packet); + if (r < 0) + return r; + + if (level < DNS_SERVER_FEATURE_LEVEL_EDNS0) + return 0; + + edns_do = level >= DNS_SERVER_FEATURE_LEVEL_DO; + + udp_size = udp_header_size(server->family); + + if (in_addr_is_localhost(server->family, &server->address) > 0) + packet_size = 65536 - udp_size; /* force linux loopback MTU if localhost address */ + else { + /* Use the MTU pointing to the server, subtract the IP/UDP header size */ + packet_size = LESS_BY(dns_server_get_mtu(server), udp_size); + + /* On the Internet we want to avoid fragmentation for security reasons. If we saw + * fragmented packets, the above was too large, let's clamp it to the largest + * fragment we saw */ + if (server->packet_fragmented) + packet_size = MIN(server->received_udp_fragment_max, packet_size); + + /* Let's not pick ridiculously large sizes, i.e. not more than 4K. No one appears + * to ever use such large sized on the Internet IRL, hence let's not either. */ + packet_size = MIN(packet_size, 4096U); + } + + /* Strictly speaking we quite possibly can receive larger datagrams than the MTU (since the + * MTU is for egress, not for ingress), but more often than not the value is symmetric, and + * we want something that does the right thing in the majority of cases, and not just in the + * theoretical edge case. */ + + /* Safety clamp, never advertise less than 512 or more than 65535 */ + packet_size = CLAMP(packet_size, + DNS_PACKET_UNICAST_SIZE_MAX, + DNS_PACKET_SIZE_MAX); + + log_debug("Announcing packet size %zu in egress EDNS(0) packet.", packet_size); + + return dns_packet_append_opt(packet, packet_size, edns_do, /* include_rfc6975 = */ true, NULL, 0, NULL); +} + +int dns_server_ifindex(const DnsServer *s) { + assert(s); + + /* For loopback addresses, go via the loopback interface, regardless which interface this is linked + * to. */ + if (in_addr_is_localhost(s->family, &s->address)) + return LOOPBACK_IFINDEX; + + /* The link ifindex always takes precedence */ + if (s->link) + return s->link->ifindex; + + if (s->ifindex > 0) + return s->ifindex; + + return 0; +} + +uint16_t dns_server_port(const DnsServer *s) { + assert(s); + + if (s->port > 0) + return s->port; + + return 53; +} + +const char *dns_server_string(DnsServer *server) { + assert(server); + + if (!server->server_string) + (void) in_addr_ifindex_to_string(server->family, &server->address, dns_server_ifindex(server), &server->server_string); + + return server->server_string; +} + +const char *dns_server_string_full(DnsServer *server) { + assert(server); + + if (!server->server_string_full) + (void) in_addr_port_ifindex_name_to_string( + server->family, + &server->address, + server->port, + dns_server_ifindex(server), + server->server_name, + &server->server_string_full); + + return server->server_string_full; +} + +bool dns_server_dnssec_supported(DnsServer *server) { + assert(server); + + /* Returns whether the server supports DNSSEC according to what we know about it */ + + if (dns_server_get_dnssec_mode(server) == DNSSEC_YES) /* If strict DNSSEC mode is enabled, always assume DNSSEC mode is supported. */ + return true; + + if (!DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(server->possible_feature_level)) + return false; + + if (server->packet_bad_opt) + return false; + + if (server->packet_rrsig_missing) + return false; + + if (server->packet_do_off) + return false; + + /* DNSSEC servers need to support TCP properly (see RFC5966), if they don't, we assume DNSSEC is borked too */ + if (server->n_failed_tcp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS) + return false; + + return true; +} + +void dns_server_warn_downgrade(DnsServer *server) { + assert(server); + + if (server->warned_downgrade) + return; + + log_struct(LOG_NOTICE, + "MESSAGE_ID=" SD_MESSAGE_DNSSEC_DOWNGRADE_STR, + LOG_MESSAGE("Server %s does not support DNSSEC, downgrading to non-DNSSEC mode.", + strna(dns_server_string_full(server))), + "DNS_SERVER=%s", strna(dns_server_string_full(server)), + "DNS_SERVER_FEATURE_LEVEL=%s", dns_server_feature_level_to_string(server->possible_feature_level)); + + server->warned_downgrade = true; +} + +size_t dns_server_get_mtu(DnsServer *s) { + assert(s); + + if (s->link && s->link->mtu != 0) + return s->link->mtu; + + return manager_find_mtu(s->manager); +} + +static void dns_server_hash_func(const DnsServer *s, struct siphash *state) { + assert(s); + + siphash24_compress(&s->family, sizeof(s->family), state); + siphash24_compress(&s->address, FAMILY_ADDRESS_SIZE(s->family), state); + siphash24_compress(&s->port, sizeof(s->port), state); + siphash24_compress(&s->ifindex, sizeof(s->ifindex), state); + siphash24_compress_string(s->server_name, state); +} + +static int dns_server_compare_func(const DnsServer *x, const DnsServer *y) { + int r; + + r = CMP(x->family, y->family); + if (r != 0) + return r; + + r = memcmp(&x->address, &y->address, FAMILY_ADDRESS_SIZE(x->family)); + if (r != 0) + return r; + + r = CMP(x->port, y->port); + if (r != 0) + return r; + + r = CMP(x->ifindex, y->ifindex); + if (r != 0) + return r; + + return streq_ptr(x->server_name, y->server_name); +} + +DEFINE_HASH_OPS(dns_server_hash_ops, DnsServer, dns_server_hash_func, dns_server_compare_func); + +void dns_server_unlink_all(DnsServer *first) { + DnsServer *next; + + if (!first) + return; + + next = first->servers_next; + dns_server_unlink(first); + + dns_server_unlink_all(next); +} + +bool dns_server_unlink_marked(DnsServer *server) { + bool changed = false; + + while (server) { + DnsServer *next; + + next = server->servers_next; + + if (server->marked) { + dns_server_unlink(server); + changed = true; + } + + server = next; + } + + return changed; +} + +void dns_server_mark_all(DnsServer *server) { + while (server) { + server->marked = true; + server = server->servers_next; + } +} + +DnsServer *dns_server_find(DnsServer *first, int family, const union in_addr_union *in_addr, uint16_t port, int ifindex, const char *name) { + LIST_FOREACH(servers, s, first) + if (s->family == family && + in_addr_equal(family, &s->address, in_addr) > 0 && + s->port == port && + s->ifindex == ifindex && + streq_ptr(s->server_name, name)) + return s; + + return NULL; +} + +DnsServer *manager_get_first_dns_server(Manager *m, DnsServerType t) { + assert(m); + + switch (t) { + + case DNS_SERVER_SYSTEM: + return m->dns_servers; + + case DNS_SERVER_FALLBACK: + return m->fallback_dns_servers; + + default: + return NULL; + } +} + +DnsServer *manager_set_dns_server(Manager *m, DnsServer *s) { + assert(m); + + if (m->current_dns_server == s) + return s; + + /* Let's log about the server switch, at debug level. Except if we switch from a non-fallback server + * to a fallback server or back, since that is noteworthy and possibly a configuration issue */ + if (s) + log_full((s->type == DNS_SERVER_FALLBACK) != (m->current_dns_server && m->current_dns_server->type == DNS_SERVER_FALLBACK) ? LOG_NOTICE : LOG_DEBUG, + "Switching to %s DNS server %s.", dns_server_type_to_string(s->type), strna(dns_server_string_full(s))); + + dns_server_unref(m->current_dns_server); + m->current_dns_server = dns_server_ref(s); + + if (m->unicast_scope) + dns_cache_flush(&m->unicast_scope->cache); + + (void) manager_send_changed(m, "CurrentDNSServer"); + + return s; +} + +DnsServer *manager_get_dns_server(Manager *m) { + Link *l; + assert(m); + + /* Try to read updates resolv.conf */ + manager_read_resolv_conf(m); + + /* If no DNS server was chosen so far, pick the first one */ + if (!m->current_dns_server || + /* In case m->current_dns_server != m->dns_servers */ + manager_server_is_stub(m, m->current_dns_server)) + manager_set_dns_server(m, m->dns_servers); + + while (m->current_dns_server && + manager_server_is_stub(m, m->current_dns_server)) { + manager_next_dns_server(m, NULL); + if (m->current_dns_server == m->dns_servers) + manager_set_dns_server(m, NULL); + } + + if (!m->current_dns_server) { + bool found = false; + + /* No DNS servers configured, let's see if there are + * any on any links. If not, we use the fallback + * servers */ + + HASHMAP_FOREACH(l, m->links) + if (l->dns_servers) { + found = true; + break; + } + + if (!found) + manager_set_dns_server(m, m->fallback_dns_servers); + } + + return m->current_dns_server; +} + +void manager_next_dns_server(Manager *m, DnsServer *if_current) { + assert(m); + + /* If the DNS server is already a different one than the one specified in 'if_current' don't do anything */ + if (if_current && m->current_dns_server != if_current) + return; + + /* If there's currently no DNS server set, then the next manager_get_dns_server() will find one */ + if (!m->current_dns_server) + return; + + /* Change to the next one, but make sure to follow the linked list only if the server is still + * linked. */ + if (m->current_dns_server->linked && m->current_dns_server->servers_next) { + manager_set_dns_server(m, m->current_dns_server->servers_next); + return; + } + + /* If there was no next one, then start from the beginning of the list */ + if (m->current_dns_server->type == DNS_SERVER_FALLBACK) + manager_set_dns_server(m, m->fallback_dns_servers); + else + manager_set_dns_server(m, m->dns_servers); +} + +DnssecMode dns_server_get_dnssec_mode(DnsServer *s) { + assert(s); + + if (s->link) + return link_get_dnssec_mode(s->link); + + return manager_get_dnssec_mode(s->manager); +} + +DnsOverTlsMode dns_server_get_dns_over_tls_mode(DnsServer *s) { + assert(s); + + if (s->link) + return link_get_dns_over_tls_mode(s->link); + + return manager_get_dns_over_tls_mode(s->manager); +} + +void dns_server_flush_cache(DnsServer *s) { + DnsServer *current; + DnsScope *scope; + + assert(s); + + /* Flush the cache of the scope this server belongs to */ + + current = s->link ? s->link->current_dns_server : s->manager->current_dns_server; + if (current != s) + return; + + scope = s->link ? s->link->unicast_scope : s->manager->unicast_scope; + if (!scope) + return; + + dns_cache_flush(&scope->cache); +} + +void dns_server_reset_features(DnsServer *s) { + assert(s); + + s->verified_feature_level = _DNS_SERVER_FEATURE_LEVEL_INVALID; + s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_BEST; + + s->received_udp_fragment_max = DNS_PACKET_UNICAST_SIZE_MAX; + + s->packet_bad_opt = false; + s->packet_rrsig_missing = false; + s->packet_do_off = false; + + s->features_grace_period_usec = DNS_SERVER_FEATURE_GRACE_PERIOD_MIN_USEC; + + s->warned_downgrade = false; + + dns_server_reset_counters(s); + + /* Let's close the default stream, so that we reprobe with the new features */ + dns_server_unref_stream(s); +} + +void dns_server_reset_features_all(DnsServer *s) { + LIST_FOREACH(servers, i, s) + dns_server_reset_features(i); +} + +void dns_server_dump(DnsServer *s, FILE *f) { + assert(s); + + if (!f) + f = stdout; + + fputs("[Server ", f); + fputs(strna(dns_server_string_full(s)), f); + fputs(" type=", f); + fputs(dns_server_type_to_string(s->type), f); + + if (s->type == DNS_SERVER_LINK) { + assert(s->link); + + fputs(" interface=", f); + fputs(s->link->ifname, f); + } + + fputs("]\n", f); + + fputs("\tVerified feature level: ", f); + fputs(strna(dns_server_feature_level_to_string(s->verified_feature_level)), f); + fputc('\n', f); + + fputs("\tPossible feature level: ", f); + fputs(strna(dns_server_feature_level_to_string(s->possible_feature_level)), f); + fputc('\n', f); + + fputs("\tDNSSEC Mode: ", f); + fputs(strna(dnssec_mode_to_string(dns_server_get_dnssec_mode(s))), f); + fputc('\n', f); + + fputs("\tCan do DNSSEC: ", f); + fputs(yes_no(dns_server_dnssec_supported(s)), f); + fputc('\n', f); + + fprintf(f, + "\tMaximum UDP fragment size received: %zu\n" + "\tFailed UDP attempts: %u\n" + "\tFailed TCP attempts: %u\n" + "\tSeen truncated packet: %s\n" + "\tSeen OPT RR getting lost: %s\n" + "\tSeen RRSIG RR missing: %s\n" + "\tSeen invalid packet: %s\n" + "\tServer dropped DO flag: %s\n", + s->received_udp_fragment_max, + s->n_failed_udp, + s->n_failed_tcp, + yes_no(s->packet_truncated), + yes_no(s->packet_bad_opt), + yes_no(s->packet_rrsig_missing), + yes_no(s->packet_invalid), + yes_no(s->packet_do_off)); +} + +void dns_server_unref_stream(DnsServer *s) { + DnsStream *ref; + + assert(s); + + /* Detaches the default stream of this server. Some special care needs to be taken here, as that stream and + * this server reference each other. First, take the stream out of the server. It's destructor will check if it + * is registered with us, hence let's invalidate this separately, so that it is already unregistered. */ + ref = TAKE_PTR(s->stream); + + /* And then, unref it */ + dns_stream_unref(ref); +} + +DnsScope *dns_server_scope(DnsServer *s) { + assert(s); + assert((s->type == DNS_SERVER_LINK) == !!s->link); + + if (s->link) + return s->link->unicast_scope; + + return s->manager->unicast_scope; +} + +static const char* const dns_server_type_table[_DNS_SERVER_TYPE_MAX] = { + [DNS_SERVER_SYSTEM] = "system", + [DNS_SERVER_FALLBACK] = "fallback", + [DNS_SERVER_LINK] = "link", +}; +DEFINE_STRING_TABLE_LOOKUP(dns_server_type, DnsServerType); + +static const char* const dns_server_feature_level_table[_DNS_SERVER_FEATURE_LEVEL_MAX] = { + [DNS_SERVER_FEATURE_LEVEL_TCP] = "TCP", + [DNS_SERVER_FEATURE_LEVEL_UDP] = "UDP", + [DNS_SERVER_FEATURE_LEVEL_EDNS0] = "UDP+EDNS0", + [DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN] = "TLS+EDNS0", + [DNS_SERVER_FEATURE_LEVEL_DO] = "UDP+EDNS0+DO", + [DNS_SERVER_FEATURE_LEVEL_TLS_DO] = "TLS+EDNS0+DO", +}; +DEFINE_STRING_TABLE_LOOKUP(dns_server_feature_level, DnsServerFeatureLevel); diff --git a/src/resolve/resolved-dns-server.h b/src/resolve/resolved-dns-server.h new file mode 100644 index 0000000..f939b53 --- /dev/null +++ b/src/resolve/resolved-dns-server.h @@ -0,0 +1,174 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "in-addr-util.h" +#include "list.h" +#include "resolve-util.h" +#include "time-util.h" + +typedef struct DnsScope DnsScope; +typedef struct DnsServer DnsServer; +typedef struct DnsStream DnsStream; +typedef struct DnsPacket DnsPacket; +typedef struct Link Link; +typedef struct Manager Manager; + +#include "resolved-dnstls.h" + +typedef enum DnsServerType { + DNS_SERVER_SYSTEM, + DNS_SERVER_FALLBACK, + DNS_SERVER_LINK, + _DNS_SERVER_TYPE_MAX, + _DNS_SERVER_TYPE_INVALID = -EINVAL, +} DnsServerType; + +const char* dns_server_type_to_string(DnsServerType i) _const_; +DnsServerType dns_server_type_from_string(const char *s) _pure_; + +typedef enum DnsServerFeatureLevel { + DNS_SERVER_FEATURE_LEVEL_TCP, + DNS_SERVER_FEATURE_LEVEL_UDP, + DNS_SERVER_FEATURE_LEVEL_EDNS0, + DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN, + DNS_SERVER_FEATURE_LEVEL_DO, + DNS_SERVER_FEATURE_LEVEL_TLS_DO, + _DNS_SERVER_FEATURE_LEVEL_MAX, + _DNS_SERVER_FEATURE_LEVEL_INVALID = -EINVAL, +} DnsServerFeatureLevel; + +#define DNS_SERVER_FEATURE_LEVEL_WORST 0 +#define DNS_SERVER_FEATURE_LEVEL_BEST (_DNS_SERVER_FEATURE_LEVEL_MAX - 1) +#define DNS_SERVER_FEATURE_LEVEL_IS_EDNS0(x) ((x) >= DNS_SERVER_FEATURE_LEVEL_EDNS0) +#define DNS_SERVER_FEATURE_LEVEL_IS_TLS(x) IN_SET(x, DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN, DNS_SERVER_FEATURE_LEVEL_TLS_DO) +#define DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(x) ((x) >= DNS_SERVER_FEATURE_LEVEL_DO) +#define DNS_SERVER_FEATURE_LEVEL_IS_UDP(x) IN_SET(x, DNS_SERVER_FEATURE_LEVEL_UDP, DNS_SERVER_FEATURE_LEVEL_EDNS0, DNS_SERVER_FEATURE_LEVEL_DO) + +const char* dns_server_feature_level_to_string(DnsServerFeatureLevel i) _const_; +DnsServerFeatureLevel dns_server_feature_level_from_string(const char *s) _pure_; + +struct DnsServer { + Manager *manager; + + unsigned n_ref; + + DnsServerType type; + Link *link; + + int family; + union in_addr_union address; + int ifindex; /* for IPv6 link-local DNS servers */ + uint16_t port; + char *server_name; + + char *server_string; + char *server_string_full; + + /* The long-lived stream towards this server. */ + DnsStream *stream; + +#if ENABLE_DNS_OVER_TLS + DnsTlsServerData dnstls_data; +#endif + + DnsServerFeatureLevel verified_feature_level; + DnsServerFeatureLevel possible_feature_level; + + size_t received_udp_fragment_max; /* largest packet or fragment (without IP/UDP header) we saw so far */ + + unsigned n_failed_udp; + unsigned n_failed_tcp; + unsigned n_failed_tls; + + bool packet_truncated:1; /* Set when TC bit was set on reply */ + bool packet_bad_opt:1; /* Set when OPT was missing or otherwise bad on reply */ + bool packet_rrsig_missing:1; /* Set when RRSIG was missing */ + bool packet_invalid:1; /* Set when we failed to parse a reply */ + bool packet_do_off:1; /* Set when the server didn't copy DNSSEC DO flag from request to response */ + bool packet_fragmented:1; /* Set when we ever saw a fragmented packet */ + + usec_t verified_usec; + usec_t features_grace_period_usec; + + /* Whether we already warned about downgrading to non-DNSSEC mode for this server */ + bool warned_downgrade:1; + + /* Used when GC'ing old DNS servers when configuration changes. */ + bool marked:1; + + /* If linked is set, then this server appears in the servers linked list */ + bool linked:1; + LIST_FIELDS(DnsServer, servers); +}; + +int dns_server_new( + Manager *m, + DnsServer **ret, + DnsServerType type, + Link *link, + int family, + const union in_addr_union *address, + uint16_t port, + int ifindex, + const char *server_string); + +DnsServer* dns_server_ref(DnsServer *s); +DnsServer* dns_server_unref(DnsServer *s); + +void dns_server_unlink(DnsServer *s); +void dns_server_move_back_and_unmark(DnsServer *s); + +void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t fragsize); +void dns_server_packet_lost(DnsServer *s, int protocol, DnsServerFeatureLevel level); +void dns_server_packet_truncated(DnsServer *s, DnsServerFeatureLevel level); +void dns_server_packet_rrsig_missing(DnsServer *s, DnsServerFeatureLevel level); +void dns_server_packet_bad_opt(DnsServer *s, DnsServerFeatureLevel level); +void dns_server_packet_rcode_downgrade(DnsServer *s, DnsServerFeatureLevel level); +void dns_server_packet_invalid(DnsServer *s, DnsServerFeatureLevel level); +void dns_server_packet_do_off(DnsServer *s, DnsServerFeatureLevel level); +void dns_server_packet_udp_fragmented(DnsServer *s, size_t fragsize); + +DnsServerFeatureLevel dns_server_possible_feature_level(DnsServer *s); + +int dns_server_adjust_opt(DnsServer *server, DnsPacket *packet, DnsServerFeatureLevel level); + +const char *dns_server_string(DnsServer *server); +const char *dns_server_string_full(DnsServer *server); +int dns_server_ifindex(const DnsServer *s); +uint16_t dns_server_port(const DnsServer *s); + +bool dns_server_dnssec_supported(DnsServer *server); + +void dns_server_warn_downgrade(DnsServer *server); + +DnsServer *dns_server_find(DnsServer *first, int family, const union in_addr_union *in_addr, uint16_t port, int ifindex, const char *name); + +void dns_server_unlink_all(DnsServer *first); +bool dns_server_unlink_marked(DnsServer *first); +void dns_server_mark_all(DnsServer *first); + +DnsServer *manager_get_first_dns_server(Manager *m, DnsServerType t); + +DnsServer *manager_set_dns_server(Manager *m, DnsServer *s); +DnsServer *manager_get_dns_server(Manager *m); +void manager_next_dns_server(Manager *m, DnsServer *if_current); + +DnssecMode dns_server_get_dnssec_mode(DnsServer *s); +DnsOverTlsMode dns_server_get_dns_over_tls_mode(DnsServer *s); + +size_t dns_server_get_mtu(DnsServer *s); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsServer*, dns_server_unref); + +extern const struct hash_ops dns_server_hash_ops; + +void dns_server_flush_cache(DnsServer *s); + +void dns_server_reset_features(DnsServer *s); +void dns_server_reset_features_all(DnsServer *s); + +void dns_server_dump(DnsServer *s, FILE *f); + +void dns_server_unref_stream(DnsServer *s); + +DnsScope *dns_server_scope(DnsServer *s); diff --git a/src/resolve/resolved-dns-stream.c b/src/resolve/resolved-dns-stream.c new file mode 100644 index 0000000..95fbb56 --- /dev/null +++ b/src/resolve/resolved-dns-stream.c @@ -0,0 +1,595 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/tcp.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "fd-util.h" +#include "io-util.h" +#include "macro.h" +#include "missing_network.h" +#include "resolved-dns-stream.h" +#include "resolved-manager.h" + +#define DNS_STREAMS_MAX 128 + +#define DNS_QUERIES_PER_STREAM 32 + +static void dns_stream_stop(DnsStream *s) { + assert(s); + + s->io_event_source = sd_event_source_disable_unref(s->io_event_source); + s->timeout_event_source = sd_event_source_disable_unref(s->timeout_event_source); + s->fd = safe_close(s->fd); + + /* Disconnect us from the server object if we are now not usable anymore */ + dns_stream_detach(s); +} + +static int dns_stream_update_io(DnsStream *s) { + uint32_t f = 0; + + assert(s); + + if (s->write_packet && s->n_written < sizeof(s->write_size) + s->write_packet->size) + f |= EPOLLOUT; + else if (!ordered_set_isempty(s->write_queue)) { + dns_packet_unref(s->write_packet); + s->write_packet = ordered_set_steal_first(s->write_queue); + s->write_size = htobe16(s->write_packet->size); + s->n_written = 0; + f |= EPOLLOUT; + } + + /* Let's read a packet if we haven't queued any yet. Except if we already hit a limit of parallel + * queries for this connection. */ + if ((!s->read_packet || s->n_read < sizeof(s->read_size) + s->read_packet->size) && + set_size(s->queries) < DNS_QUERIES_PER_STREAM) + f |= EPOLLIN; + + s->requested_events = f; + +#if ENABLE_DNS_OVER_TLS + /* For handshake and clean closing purposes, TLS can override requested events */ + if (s->dnstls_events != 0) + f = s->dnstls_events; +#endif + + return sd_event_source_set_io_events(s->io_event_source, f); +} + +static int dns_stream_complete(DnsStream *s, int error) { + _cleanup_(dns_stream_unrefp) _unused_ DnsStream *ref = dns_stream_ref(s); /* Protect stream while we process it */ + + assert(s); + assert(error >= 0); + + /* Error is > 0 when the connection failed for some reason in the network stack. It's == 0 if we sent + * and received exactly one packet each (in the LLMNR client case). */ + +#if ENABLE_DNS_OVER_TLS + if (s->encrypted) { + int r; + + r = dnstls_stream_shutdown(s, error); + if (r != -EAGAIN) + dns_stream_stop(s); + } else +#endif + dns_stream_stop(s); + + dns_stream_detach(s); + + if (s->complete) + s->complete(s, error); + else /* the default action if no completion function is set is to close the stream */ + dns_stream_unref(s); + + return 0; +} + +static int dns_stream_identify(DnsStream *s) { + CMSG_BUFFER_TYPE(CMSG_SPACE(MAXSIZE(struct in_pktinfo, struct in6_pktinfo)) + + CMSG_SPACE(int) + /* for the TTL */ + + EXTRA_CMSG_SPACE /* kernel appears to require extra space */) control; + struct msghdr mh = {}; + struct cmsghdr *cmsg; + socklen_t sl; + int r; + + assert(s); + + if (s->identified) + return 0; + + /* Query the local side */ + s->local_salen = sizeof(s->local); + r = getsockname(s->fd, &s->local.sa, &s->local_salen); + if (r < 0) + return -errno; + if (s->local.sa.sa_family == AF_INET6 && s->ifindex <= 0) + s->ifindex = s->local.in6.sin6_scope_id; + + /* Query the remote side */ + s->peer_salen = sizeof(s->peer); + r = getpeername(s->fd, &s->peer.sa, &s->peer_salen); + if (r < 0) + return -errno; + if (s->peer.sa.sa_family == AF_INET6 && s->ifindex <= 0) + s->ifindex = s->peer.in6.sin6_scope_id; + + /* Check consistency */ + assert(s->peer.sa.sa_family == s->local.sa.sa_family); + assert(IN_SET(s->peer.sa.sa_family, AF_INET, AF_INET6)); + + /* Query connection meta information */ + sl = sizeof(control); + if (s->peer.sa.sa_family == AF_INET) { + r = getsockopt(s->fd, IPPROTO_IP, IP_PKTOPTIONS, &control, &sl); + if (r < 0) + return -errno; + } else if (s->peer.sa.sa_family == AF_INET6) { + + r = getsockopt(s->fd, IPPROTO_IPV6, IPV6_2292PKTOPTIONS, &control, &sl); + if (r < 0) + return -errno; + } else + return -EAFNOSUPPORT; + + mh.msg_control = &control; + mh.msg_controllen = sl; + + CMSG_FOREACH(cmsg, &mh) { + + if (cmsg->cmsg_level == IPPROTO_IPV6) { + assert(s->peer.sa.sa_family == AF_INET6); + + switch (cmsg->cmsg_type) { + + case IPV6_PKTINFO: { + struct in6_pktinfo *i = (struct in6_pktinfo*) CMSG_DATA(cmsg); + + if (s->ifindex <= 0) + s->ifindex = i->ipi6_ifindex; + break; + } + + case IPV6_HOPLIMIT: + s->ttl = *(int *) CMSG_DATA(cmsg); + break; + } + + } else if (cmsg->cmsg_level == IPPROTO_IP) { + assert(s->peer.sa.sa_family == AF_INET); + + switch (cmsg->cmsg_type) { + + case IP_PKTINFO: { + struct in_pktinfo *i = (struct in_pktinfo*) CMSG_DATA(cmsg); + + if (s->ifindex <= 0) + s->ifindex = i->ipi_ifindex; + break; + } + + case IP_TTL: + s->ttl = *(int *) CMSG_DATA(cmsg); + break; + } + } + } + + /* The Linux kernel sets the interface index to the loopback + * device if the connection came from the local host since it + * avoids the routing table in such a case. Let's unset the + * interface index in such a case. */ + if (s->ifindex == LOOPBACK_IFINDEX) + s->ifindex = 0; + + /* If we don't know the interface index still, we look for the + * first local interface with a matching address. Yuck! */ + if (s->ifindex <= 0) + s->ifindex = manager_find_ifindex(s->manager, s->local.sa.sa_family, sockaddr_in_addr(&s->local.sa)); + + if (s->protocol == DNS_PROTOCOL_LLMNR && s->ifindex > 0) { + /* Make sure all packets for this connection are sent on the same interface */ + r = socket_set_unicast_if(s->fd, s->local.sa.sa_family, s->ifindex); + if (r < 0) + log_debug_errno(errno, "Failed to invoke IP_UNICAST_IF/IPV6_UNICAST_IF: %m"); + } + + s->identified = true; + + return 0; +} + +ssize_t dns_stream_writev(DnsStream *s, const struct iovec *iov, size_t iovcnt, int flags) { + ssize_t m; + + assert(s); + assert(iov); + +#if ENABLE_DNS_OVER_TLS + if (s->encrypted && !(flags & DNS_STREAM_WRITE_TLS_DATA)) + return dnstls_stream_writev(s, iov, iovcnt); +#endif + + if (s->tfo_salen > 0) { + struct msghdr hdr = { + .msg_iov = (struct iovec*) iov, + .msg_iovlen = iovcnt, + .msg_name = &s->tfo_address.sa, + .msg_namelen = s->tfo_salen + }; + + m = sendmsg(s->fd, &hdr, MSG_FASTOPEN); + if (m < 0) { + if (errno == EOPNOTSUPP) { + s->tfo_salen = 0; + if (connect(s->fd, &s->tfo_address.sa, s->tfo_salen) < 0) + return -errno; + + return -EAGAIN; + } + if (errno == EINPROGRESS) + return -EAGAIN; + + return -errno; + } else + s->tfo_salen = 0; /* connection is made */ + } else { + m = writev(s->fd, iov, iovcnt); + if (m < 0) + return -errno; + } + + return m; +} + +static ssize_t dns_stream_read(DnsStream *s, void *buf, size_t count) { + ssize_t ss; + +#if ENABLE_DNS_OVER_TLS + if (s->encrypted) + ss = dnstls_stream_read(s, buf, count); + else +#endif + { + ss = read(s->fd, buf, count); + if (ss < 0) + return -errno; + } + + return ss; +} + +static int on_stream_timeout(sd_event_source *es, usec_t usec, void *userdata) { + DnsStream *s = ASSERT_PTR(userdata); + + return dns_stream_complete(s, ETIMEDOUT); +} + +static DnsPacket *dns_stream_take_read_packet(DnsStream *s) { + assert(s); + + /* Note, dns_stream_update() should be called after this is called. When this is called, the + * stream may be already full and the EPOLLIN flag is dropped from the stream IO event source. + * Even this makes a room to read in the stream, this does not call dns_stream_update(), hence + * EPOLLIN flag is not set automatically. So, to read further packets from the stream, + * dns_stream_update() must be called explicitly. Currently, this is only called from + * on_stream_io(), and there dns_stream_update() is called. */ + + if (!s->read_packet) + return NULL; + + if (s->n_read < sizeof(s->read_size)) + return NULL; + + if (s->n_read < sizeof(s->read_size) + be16toh(s->read_size)) + return NULL; + + s->n_read = 0; + return TAKE_PTR(s->read_packet); +} + +static int on_stream_io(sd_event_source *es, int fd, uint32_t revents, void *userdata) { + _cleanup_(dns_stream_unrefp) DnsStream *s = dns_stream_ref(userdata); /* Protect stream while we process it */ + bool progressed = false; + int r; + + assert(s); + +#if ENABLE_DNS_OVER_TLS + if (s->encrypted) { + r = dnstls_stream_on_io(s, revents); + if (r == DNSTLS_STREAM_CLOSED) + return 0; + if (r == -EAGAIN) + return dns_stream_update_io(s); + if (r < 0) + return dns_stream_complete(s, -r); + + r = dns_stream_update_io(s); + if (r < 0) + return r; + } +#endif + + /* only identify after connecting */ + if (s->tfo_salen == 0) { + r = dns_stream_identify(s); + if (r < 0) + return dns_stream_complete(s, -r); + } + + if ((revents & EPOLLOUT) && + s->write_packet && + s->n_written < sizeof(s->write_size) + s->write_packet->size) { + + struct iovec iov[] = { + IOVEC_MAKE(&s->write_size, sizeof(s->write_size)), + IOVEC_MAKE(DNS_PACKET_DATA(s->write_packet), s->write_packet->size), + }; + + IOVEC_INCREMENT(iov, ELEMENTSOF(iov), s->n_written); + + ssize_t ss = dns_stream_writev(s, iov, ELEMENTSOF(iov), 0); + if (ss < 0) { + if (!ERRNO_IS_TRANSIENT(ss)) + return dns_stream_complete(s, -ss); + } else { + progressed = true; + s->n_written += ss; + } + + /* Are we done? If so, disable the event source for EPOLLOUT */ + if (s->n_written >= sizeof(s->write_size) + s->write_packet->size) { + r = dns_stream_update_io(s); + if (r < 0) + return dns_stream_complete(s, -r); + } + } + + while ((revents & (EPOLLIN|EPOLLHUP|EPOLLRDHUP)) && + (!s->read_packet || + s->n_read < sizeof(s->read_size) + s->read_packet->size)) { + + if (s->n_read < sizeof(s->read_size)) { + ssize_t ss; + + ss = dns_stream_read(s, (uint8_t*) &s->read_size + s->n_read, sizeof(s->read_size) - s->n_read); + if (ss < 0) { + if (!ERRNO_IS_TRANSIENT(ss)) + return dns_stream_complete(s, -ss); + break; + } else if (ss == 0) + return dns_stream_complete(s, ECONNRESET); + else { + progressed = true; + s->n_read += ss; + } + } + + if (s->n_read >= sizeof(s->read_size)) { + + if (be16toh(s->read_size) < DNS_PACKET_HEADER_SIZE) + return dns_stream_complete(s, EBADMSG); + + if (s->n_read < sizeof(s->read_size) + be16toh(s->read_size)) { + ssize_t ss; + + if (!s->read_packet) { + r = dns_packet_new(&s->read_packet, s->protocol, be16toh(s->read_size), DNS_PACKET_SIZE_MAX); + if (r < 0) + return dns_stream_complete(s, -r); + + s->read_packet->size = be16toh(s->read_size); + s->read_packet->ipproto = IPPROTO_TCP; + s->read_packet->family = s->peer.sa.sa_family; + s->read_packet->ttl = s->ttl; + s->read_packet->ifindex = s->ifindex; + s->read_packet->timestamp = now(CLOCK_BOOTTIME); + + if (s->read_packet->family == AF_INET) { + s->read_packet->sender.in = s->peer.in.sin_addr; + s->read_packet->sender_port = be16toh(s->peer.in.sin_port); + s->read_packet->destination.in = s->local.in.sin_addr; + s->read_packet->destination_port = be16toh(s->local.in.sin_port); + } else { + assert(s->read_packet->family == AF_INET6); + s->read_packet->sender.in6 = s->peer.in6.sin6_addr; + s->read_packet->sender_port = be16toh(s->peer.in6.sin6_port); + s->read_packet->destination.in6 = s->local.in6.sin6_addr; + s->read_packet->destination_port = be16toh(s->local.in6.sin6_port); + + if (s->read_packet->ifindex == 0) + s->read_packet->ifindex = s->peer.in6.sin6_scope_id; + if (s->read_packet->ifindex == 0) + s->read_packet->ifindex = s->local.in6.sin6_scope_id; + } + } + + ss = dns_stream_read(s, + (uint8_t*) DNS_PACKET_DATA(s->read_packet) + s->n_read - sizeof(s->read_size), + sizeof(s->read_size) + be16toh(s->read_size) - s->n_read); + if (ss < 0) { + if (!ERRNO_IS_TRANSIENT(ss)) + return dns_stream_complete(s, -ss); + break; + } else if (ss == 0) + return dns_stream_complete(s, ECONNRESET); + else + s->n_read += ss; + } + + /* Are we done? If so, call the packet handler and re-enable EPOLLIN for the + * event source if necessary. */ + _cleanup_(dns_packet_unrefp) DnsPacket *p = dns_stream_take_read_packet(s); + if (p) { + assert(s->on_packet); + r = s->on_packet(s, p); + if (r < 0) + return r; + + r = dns_stream_update_io(s); + if (r < 0) + return dns_stream_complete(s, -r); + + s->packet_received = true; + + /* If we just disabled the read event, stop reading */ + if (!FLAGS_SET(s->requested_events, EPOLLIN)) + break; + } + } + } + + /* Complete the stream if finished reading and writing one packet, and there's nothing + * else left to write. */ + if (s->type == DNS_STREAM_LLMNR_SEND && s->packet_received && + !FLAGS_SET(s->requested_events, EPOLLOUT)) + return dns_stream_complete(s, 0); + + /* If we did something, let's restart the timeout event source */ + if (progressed && s->timeout_event_source) { + r = sd_event_source_set_time_relative(s->timeout_event_source, DNS_STREAM_ESTABLISHED_TIMEOUT_USEC); + if (r < 0) + log_warning_errno(errno, "Couldn't restart TCP connection timeout, ignoring: %m"); + } + + return 0; +} + +static DnsStream *dns_stream_free(DnsStream *s) { + DnsPacket *p; + + assert(s); + + dns_stream_stop(s); + + if (s->manager) { + LIST_REMOVE(streams, s->manager->dns_streams, s); + s->manager->n_dns_streams[s->type]--; + } + +#if ENABLE_DNS_OVER_TLS + if (s->encrypted) + dnstls_stream_free(s); +#endif + + ORDERED_SET_FOREACH(p, s->write_queue) + dns_packet_unref(ordered_set_remove(s->write_queue, p)); + + dns_packet_unref(s->write_packet); + dns_packet_unref(s->read_packet); + dns_server_unref(s->server); + + ordered_set_free(s->write_queue); + + return mfree(s); +} + +DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsStream, dns_stream, dns_stream_free); + +int dns_stream_new( + Manager *m, + DnsStream **ret, + DnsStreamType type, + DnsProtocol protocol, + int fd, + const union sockaddr_union *tfo_address, + int (on_packet)(DnsStream*, DnsPacket*), + int (complete)(DnsStream*, int), /* optional */ + usec_t connect_timeout_usec) { + + _cleanup_(dns_stream_unrefp) DnsStream *s = NULL; + int r; + + assert(m); + assert(ret); + assert(type >= 0); + assert(type < _DNS_STREAM_TYPE_MAX); + assert(protocol >= 0); + assert(protocol < _DNS_PROTOCOL_MAX); + assert(fd >= 0); + assert(on_packet); + + if (m->n_dns_streams[type] > DNS_STREAMS_MAX) + return -EBUSY; + + s = new(DnsStream, 1); + if (!s) + return -ENOMEM; + + *s = (DnsStream) { + .n_ref = 1, + .fd = -1, + .protocol = protocol, + .type = type, + }; + + r = ordered_set_ensure_allocated(&s->write_queue, &dns_packet_hash_ops); + if (r < 0) + return r; + + r = sd_event_add_io(m->event, &s->io_event_source, fd, EPOLLIN, on_stream_io, s); + if (r < 0) + return r; + + (void) sd_event_source_set_description(s->io_event_source, "dns-stream-io"); + + r = sd_event_add_time_relative( + m->event, + &s->timeout_event_source, + CLOCK_BOOTTIME, + connect_timeout_usec, 0, + on_stream_timeout, s); + if (r < 0) + return r; + + (void) sd_event_source_set_description(s->timeout_event_source, "dns-stream-timeout"); + + LIST_PREPEND(streams, m->dns_streams, s); + m->n_dns_streams[type]++; + s->manager = m; + + s->fd = fd; + s->on_packet = on_packet; + s->complete = complete; + + if (tfo_address) { + s->tfo_address = *tfo_address; + s->tfo_salen = tfo_address->sa.sa_family == AF_INET6 ? sizeof(tfo_address->in6) : sizeof(tfo_address->in); + } + + *ret = TAKE_PTR(s); + + return 0; +} + +int dns_stream_write_packet(DnsStream *s, DnsPacket *p) { + int r; + + assert(s); + assert(p); + + r = ordered_set_put(s->write_queue, p); + if (r < 0) + return r; + + dns_packet_ref(p); + + return dns_stream_update_io(s); +} + +void dns_stream_detach(DnsStream *s) { + assert(s); + + if (!s->server) + return; + + if (s->server->stream != s) + return; + + dns_server_unref_stream(s->server); +} diff --git a/src/resolve/resolved-dns-stream.h b/src/resolve/resolved-dns-stream.h new file mode 100644 index 0000000..ba4a59e --- /dev/null +++ b/src/resolve/resolved-dns-stream.h @@ -0,0 +1,128 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-event.h" + +#include "ordered-set.h" +#include "socket-util.h" + +typedef struct DnsServer DnsServer; +typedef struct DnsStream DnsStream; +typedef struct DnsTransaction DnsTransaction; +typedef struct Manager Manager; +typedef struct DnsStubListenerExtra DnsStubListenerExtra; + +#include "resolved-dns-packet.h" +#include "resolved-dnstls.h" + +/* Various timeouts for establishing TCP connections. First the default time-out for that. */ +#define DNS_STREAM_DEFAULT_TIMEOUT_USEC (10 * USEC_PER_SEC) + +/* In the DNS stub, be more friendly for incoming connections, than we are to ourselves for outgoing ones */ +#define DNS_STREAM_STUB_TIMEOUT_USEC (30 * USEC_PER_SEC) + +/* In opportunistic TLS mode, lower timeouts */ +#define DNS_STREAM_OPPORTUNISTIC_TLS_TIMEOUT_USEC (3 * USEC_PER_SEC) + +/* Once connections are established apply this timeout once nothing happens anymore */ +#define DNS_STREAM_ESTABLISHED_TIMEOUT_USEC (10 * USEC_PER_SEC) + +typedef enum DnsStreamType { + DNS_STREAM_LOOKUP, /* Outgoing connection to a classic DNS server */ + DNS_STREAM_LLMNR_SEND, /* Outgoing LLMNR TCP lookup */ + DNS_STREAM_LLMNR_RECV, /* Incoming LLMNR TCP lookup */ + DNS_STREAM_STUB, /* Incoming DNS stub connection */ + _DNS_STREAM_TYPE_MAX, + _DNS_STREAM_TYPE_INVALID = -EINVAL, +} DnsStreamType; + +#define DNS_STREAM_WRITE_TLS_DATA 1 + +/* Streams are used by three subsystems: + * + * 1. The normal transaction logic when doing a DNS or LLMNR lookup via TCP + * 2. The LLMNR logic when accepting a TCP-based lookup + * 3. The DNS stub logic when accepting a TCP-based lookup + */ + +struct DnsStream { + Manager *manager; + unsigned n_ref; + + DnsStreamType type; + DnsProtocol protocol; + + int fd; + union sockaddr_union peer; + socklen_t peer_salen; + union sockaddr_union local; + socklen_t local_salen; + int ifindex; + uint32_t ttl; + bool identified; + bool packet_received; /* At least one packet is received. Used by LLMNR. */ + uint32_t requested_events; + + /* only when using TCP fast open */ + union sockaddr_union tfo_address; + socklen_t tfo_salen; + +#if ENABLE_DNS_OVER_TLS + DnsTlsStreamData dnstls_data; + uint32_t dnstls_events; +#endif + + sd_event_source *io_event_source; + sd_event_source *timeout_event_source; + + be16_t write_size, read_size; + DnsPacket *write_packet, *read_packet; + size_t n_written, n_read; + OrderedSet *write_queue; + + int (*on_packet)(DnsStream *s, DnsPacket *p); + int (*complete)(DnsStream *s, int error); + + LIST_HEAD(DnsTransaction, transactions); /* when used by the transaction logic */ + DnsServer *server; /* when used by the transaction logic */ + Set *queries; /* when used by the DNS stub logic */ + + /* used when DNS-over-TLS is enabled */ + bool encrypted:1; + + DnsStubListenerExtra *stub_listener_extra; + + LIST_FIELDS(DnsStream, streams); +}; + +int dns_stream_new( + Manager *m, + DnsStream **ret, + DnsStreamType type, + DnsProtocol protocol, + int fd, + const union sockaddr_union *tfo_address, + int (on_packet)(DnsStream*, DnsPacket*), + int (complete)(DnsStream*, int), /* optional */ + usec_t connect_timeout_usec); +#if ENABLE_DNS_OVER_TLS +int dns_stream_connect_tls(DnsStream *s, void *tls_session); +#endif +DnsStream *dns_stream_unref(DnsStream *s); +DnsStream *dns_stream_ref(DnsStream *s); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsStream*, dns_stream_unref); + +int dns_stream_write_packet(DnsStream *s, DnsPacket *p); +ssize_t dns_stream_writev(DnsStream *s, const struct iovec *iov, size_t iovcnt, int flags); + +static inline bool DNS_STREAM_QUEUED(DnsStream *s) { + assert(s); + + if (s->fd < 0) /* already stopped? */ + return false; + + return !!s->write_packet; +} + +void dns_stream_detach(DnsStream *s); diff --git a/src/resolve/resolved-dns-stub.c b/src/resolve/resolved-dns-stub.c new file mode 100644 index 0000000..11e24b8 --- /dev/null +++ b/src/resolve/resolved-dns-stub.c @@ -0,0 +1,1427 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if_arp.h> +#include <netinet/tcp.h> + +#include "capability-util.h" +#include "errno-util.h" +#include "fd-util.h" +#include "missing_network.h" +#include "missing_socket.h" +#include "resolved-dns-stub.h" +#include "socket-netlink.h" +#include "socket-util.h" +#include "stdio-util.h" +#include "string-table.h" + +/* The MTU of the loopback device is 64K on Linux, advertise that as maximum datagram size, but subtract the Ethernet, + * IP and UDP header sizes */ +#define ADVERTISE_DATAGRAM_SIZE_MAX (65536U-14U-20U-8U) + +/* On the extra stubs, use a more conservative choice */ +#define ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX DNS_PACKET_UNICAST_SIZE_LARGE_MAX + +static int manager_dns_stub_fd_extra(Manager *m, DnsStubListenerExtra *l, int type); +static int manager_dns_stub_fd(Manager *m, int family, const union in_addr_union *listen_address, int type); + +static void dns_stub_listener_extra_hash_func(const DnsStubListenerExtra *a, struct siphash *state) { + assert(a); + + siphash24_compress(&a->mode, sizeof(a->mode), state); + siphash24_compress(&a->family, sizeof(a->family), state); + siphash24_compress(&a->address, FAMILY_ADDRESS_SIZE(a->family), state); + siphash24_compress(&a->port, sizeof(a->port), state); +} + +static int dns_stub_listener_extra_compare_func(const DnsStubListenerExtra *a, const DnsStubListenerExtra *b) { + int r; + + assert(a); + assert(b); + + r = CMP(a->mode, b->mode); + if (r != 0) + return r; + + r = CMP(a->family, b->family); + if (r != 0) + return r; + + r = memcmp(&a->address, &b->address, FAMILY_ADDRESS_SIZE(a->family)); + if (r != 0) + return r; + + return CMP(a->port, b->port); +} + +DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR( + dns_stub_listener_extra_hash_ops, + DnsStubListenerExtra, + dns_stub_listener_extra_hash_func, + dns_stub_listener_extra_compare_func, + dns_stub_listener_extra_free); + +int dns_stub_listener_extra_new( + Manager *m, + DnsStubListenerExtra **ret) { + + DnsStubListenerExtra *l; + + l = new(DnsStubListenerExtra, 1); + if (!l) + return -ENOMEM; + + *l = (DnsStubListenerExtra) { + .manager = m, + }; + + *ret = TAKE_PTR(l); + return 0; +} + +DnsStubListenerExtra *dns_stub_listener_extra_free(DnsStubListenerExtra *p) { + if (!p) + return NULL; + + p->udp_event_source = sd_event_source_disable_unref(p->udp_event_source); + p->tcp_event_source = sd_event_source_disable_unref(p->tcp_event_source); + + hashmap_free(p->queries_by_packet); + + return mfree(p); +} + +static void stub_packet_hash_func(const DnsPacket *p, struct siphash *state) { + assert(p); + + siphash24_compress(&p->protocol, sizeof(p->protocol), state); + siphash24_compress(&p->family, sizeof(p->family), state); + siphash24_compress(&p->sender, sizeof(p->sender), state); + siphash24_compress(&p->ipproto, sizeof(p->ipproto), state); + siphash24_compress(&p->sender_port, sizeof(p->sender_port), state); + siphash24_compress(DNS_PACKET_HEADER(p), sizeof(DnsPacketHeader), state); + + /* We don't bother hashing the full packet here, just the header */ +} + +static int stub_packet_compare_func(const DnsPacket *x, const DnsPacket *y) { + int r; + + r = CMP(x->protocol, y->protocol); + if (r != 0) + return r; + + r = CMP(x->family, y->family); + if (r != 0) + return r; + + r = memcmp(&x->sender, &y->sender, sizeof(x->sender)); + if (r != 0) + return r; + + r = CMP(x->ipproto, y->ipproto); + if (r != 0) + return r; + + r = CMP(x->sender_port, y->sender_port); + if (r != 0) + return r; + + return memcmp(DNS_PACKET_HEADER(x), DNS_PACKET_HEADER(y), sizeof(DnsPacketHeader)); +} + +DEFINE_HASH_OPS(stub_packet_hash_ops, DnsPacket, stub_packet_hash_func, stub_packet_compare_func); + +static int reply_add_with_rrsig( + DnsAnswer **reply, + DnsResourceRecord *rr, + int ifindex, + DnsAnswerFlags flags, + DnsResourceRecord *rrsig, + bool with_rrsig) { + int r; + + assert(reply); + assert(rr); + + r = dns_answer_add_extend(reply, rr, ifindex, flags, rrsig); + if (r < 0) + return r; + + if (with_rrsig && rrsig) { + r = dns_answer_add_extend(reply, rrsig, ifindex, flags, NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int dns_stub_collect_answer_by_question( + DnsAnswer **reply, + DnsAnswer *answer, + DnsQuestion *question, + bool with_rrsig) { /* Add RRSIG RR matching each RR */ + + DnsAnswerItem *item; + int r; + + assert(reply); + + /* Copies all RRs from 'answer' into 'reply', if they match 'question'. */ + + DNS_ANSWER_FOREACH_ITEM(item, answer) { + + /* We have a question, let's see if this RR matches it */ + r = dns_question_matches_rr(question, item->rr, NULL); + if (r < 0) + return r; + if (!r) { + /* Maybe there's a CNAME/DNAME in here? If so, that's an answer too */ + r = dns_question_matches_cname_or_dname(question, item->rr, NULL); + if (r < 0) + return r; + if (!r) + continue; + } + + /* Mask the section info, we want the primary answers to always go without section + * info, so that it is added to the answer section when we synthesize a reply. */ + + r = reply_add_with_rrsig( + reply, + item->rr, + item->ifindex, + item->flags & ~DNS_ANSWER_MASK_SECTIONS, + item->rrsig, + with_rrsig); + if (r < 0) + return r; + } + + return 0; +} + +static int dns_stub_collect_answer_by_section( + DnsAnswer **reply, + DnsAnswer *answer, + DnsAnswerFlags section, + DnsAnswer *exclude1, + DnsAnswer *exclude2, + bool with_dnssec) { /* Include DNSSEC RRs. RRSIG, NSEC, … */ + + DnsAnswerItem *item; + int r; + + assert(reply); + + /* Copies all RRs from 'answer' into 'reply', if they originate from the specified section. Also, + * avoid any RRs listed in 'exclude'. */ + + DNS_ANSWER_FOREACH_ITEM(item, answer) { + + if (dns_answer_contains(exclude1, item->rr) || + dns_answer_contains(exclude2, item->rr)) + continue; + + if (!with_dnssec && + dns_type_is_dnssec(item->rr->key->type)) + continue; + + if (((item->flags ^ section) & DNS_ANSWER_MASK_SECTIONS) != 0) + continue; + + r = reply_add_with_rrsig( + reply, + item->rr, + item->ifindex, + item->flags, + item->rrsig, + with_dnssec); + if (r < 0) + return r; + } + + return 0; +} + +static int dns_stub_assign_sections( + DnsQuery *q, + DnsQuestion *question, + bool edns0_do) { + + int r; + + assert(q); + assert(question); + + /* Let's assign the 'answer' RRs we collected to their respective sections in the reply datagram. We + * try to reproduce a section assignment similar to what the upstream DNS server responded to us. We + * use the DNS_ANSWER_SECTION_xyz flags to match things up, which is where the original upstream's + * packet section assignment is stored in the DnsAnswer object. Not all RRs in the 'answer' objects + * come with section information though (for example, because they were synthesized locally, and not + * from a DNS packet). To deal with that we extend the assignment logic a bit: anything from the + * 'answer' object that directly matches the original question is always put in the ANSWER section, + * regardless if it carries section info, or what that section info says. Then, anything from the + * 'answer' objects that is from the ANSWER or AUTHORITY sections, and wasn't already added to the + * ANSWER section is placed in the AUTHORITY section. Everything else from either object is added to + * the ADDITIONAL section. */ + + /* Include all RRs that directly answer the question in the answer section */ + r = dns_stub_collect_answer_by_question( + &q->reply_answer, + q->answer, + question, + edns0_do); + if (r < 0) + return r; + + /* Include all RRs that originate from the authority sections, and aren't already listed in the + * answer section, in the authority section */ + r = dns_stub_collect_answer_by_section( + &q->reply_authoritative, + q->answer, + DNS_ANSWER_SECTION_AUTHORITY, + q->reply_answer, NULL, + edns0_do); + if (r < 0) + return r; + + /* Include all RRs that originate from the answer or additional sections in the additional section + * (except if already listed in the other two sections). Also add all RRs with no section marking. */ + r = dns_stub_collect_answer_by_section( + &q->reply_additional, + q->answer, + DNS_ANSWER_SECTION_ANSWER, + q->reply_answer, q->reply_authoritative, + edns0_do); + if (r < 0) + return r; + r = dns_stub_collect_answer_by_section( + &q->reply_additional, + q->answer, + DNS_ANSWER_SECTION_ADDITIONAL, + q->reply_answer, q->reply_authoritative, + edns0_do); + if (r < 0) + return r; + r = dns_stub_collect_answer_by_section( + &q->reply_additional, + q->answer, + 0, + q->reply_answer, q->reply_authoritative, + edns0_do); + if (r < 0) + return r; + + return 0; +} + +static int dns_stub_make_reply_packet( + DnsPacket **ret, + size_t max_size, + DnsQuestion *q, + bool *ret_truncated) { + + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + bool tc = false; + int r; + + assert(ret); + + r = dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, max_size); + if (r < 0) + return r; + + r = dns_packet_append_question(p, q); + if (r == -EMSGSIZE) + tc = true; + else if (r < 0) + return r; + + if (ret_truncated) + *ret_truncated = tc; + else if (tc) + return -EMSGSIZE; + + DNS_PACKET_HEADER(p)->qdcount = htobe16(dns_question_size(q)); + + *ret = TAKE_PTR(p); + return 0; +} + +static int dns_stub_add_reply_packet_body( + DnsPacket *p, + DnsAnswer *answer, + DnsAnswer *authoritative, + DnsAnswer *additional, + bool edns0_do, /* Client expects DNSSEC RRs? */ + bool *truncated) { + + unsigned n_answer = 0, n_authoritative = 0, n_additional = 0; + bool tc = false; + int r; + + assert(p); + + /* Add the three sections to the packet. If the answer section doesn't fit we'll signal that as + * truncation. If the authoritative section doesn't fit and we are in DNSSEC mode, also signal + * truncation. In all other cases where things don't fit don't signal truncation, as for those cases + * the dropped RRs should not be essential. */ + + r = dns_packet_append_answer(p, answer, &n_answer); + if (r == -EMSGSIZE) + tc = true; + else if (r < 0) + return r; + else { + r = dns_packet_append_answer(p, authoritative, &n_authoritative); + if (r == -EMSGSIZE) { + if (edns0_do) + tc = true; + } else if (r < 0) + return r; + else { + r = dns_packet_append_answer(p, additional, &n_additional); + if (r < 0 && r != -EMSGSIZE) + return r; + } + } + + if (tc) { + if (!truncated) + return -EMSGSIZE; + + *truncated = true; + } + + DNS_PACKET_HEADER(p)->ancount = htobe16(n_answer); + DNS_PACKET_HEADER(p)->nscount = htobe16(n_authoritative); + DNS_PACKET_HEADER(p)->arcount = htobe16(n_additional); + return 0; +} + +static const char *nsid_string(void) { + static char buffer[SD_ID128_STRING_MAX + STRLEN(".resolved.systemd.io")] = ""; + sd_id128_t id; + int r; + + /* Let's generate a string that we can use as RFC5001 NSID identifier. The string shall identify us + * as systemd-resolved, and return a different string for each resolved instance without leaking host + * identity. Hence let's use a fixed suffix that identifies resolved, and a prefix generated from the + * machine ID but from which the machine ID cannot be determined. + * + * Clients can use this to determine whether an answer is originating locally or is proxied from + * upstream. */ + + if (!isempty(buffer)) + return buffer; + + r = sd_id128_get_machine_app_specific( + SD_ID128_MAKE(ed,d3,12,5d,16,b9,41,f9,a1,49,5f,ab,15,62,ab,27), + &id); + if (r < 0) { + log_debug_errno(r, "Failed to determine machine ID, ignoring: %m"); + return NULL; + } + + xsprintf(buffer, SD_ID128_FORMAT_STR ".resolved.systemd.io", SD_ID128_FORMAT_VAL(id)); + return buffer; +} + +static int dns_stub_finish_reply_packet( + DnsPacket *p, + uint16_t id, + int rcode, + bool tc, /* set the Truncated bit? */ + bool aa, /* set the Authoritative Answer bit? */ + bool rd, /* set the Recursion Desired bit? */ + bool add_opt, /* add an OPT RR to this packet? */ + bool edns0_do, /* set the EDNS0 DNSSEC OK bit? */ + bool ad, /* set the DNSSEC authenticated data bit? */ + bool cd, /* set the DNSSEC checking disabled bit? */ + uint16_t max_udp_size, /* The maximum UDP datagram size to advertise to clients */ + bool nsid) { /* whether to add NSID */ + + int r; + + assert(p); + + if (add_opt) { + r = dns_packet_append_opt(p, max_udp_size, edns0_do, /* include_rfc6975 = */ false, nsid ? nsid_string() : NULL, rcode, NULL); + if (r == -EMSGSIZE) /* Hit the size limit? then indicate truncation */ + tc = true; + else if (r < 0) + return r; + } else { + /* If the client can't to EDNS0, don't do DO either */ + edns0_do = false; + + /* If we don't do EDNS, clamp the rcode to 4 bit */ + if (rcode > 0xF) + rcode = DNS_RCODE_SERVFAIL; + } + + /* Don't set the CD bit unless DO is on, too */ + if (!edns0_do) + cd = false; + + /* Note that we allow the AD bit to be set even if client didn't signal DO, as per RFC 6840, section + * 5.7 */ + + DNS_PACKET_HEADER(p)->id = id; + + DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS( + 1 /* qr */, + 0 /* opcode */, + aa /* aa */, + tc /* tc */, + rd /* rd */, + 1 /* ra */, + ad /* ad */, + cd /* cd */, + rcode)); + + return 0; +} + +static bool address_is_proxy(int family, const union in_addr_union *a) { + assert(a); + + /* Returns true if the specified address is the DNS "proxy" stub, i.e. where we unconditionally enable bypass mode */ + + if (family != AF_INET) + return false; + + return be32toh(a->in.s_addr) == INADDR_DNS_PROXY_STUB; +} + +static int find_socket_fd( + Manager *m, + DnsStubListenerExtra *l, + int family, + const union in_addr_union *listen_address, + int type) { + + assert(m); + + /* Finds the right socket to use for sending. If we know the extra listener, otherwise go via the + * address to send from */ + if (l) + return manager_dns_stub_fd_extra(m, l, type); + + return manager_dns_stub_fd(m, family, listen_address, type); +} + +static int dns_stub_send( + Manager *m, + DnsStubListenerExtra *l, + DnsStream *s, + DnsPacket *p, + DnsPacket *reply) { + + int r; + + assert(m); + assert(p); + assert(reply); + + if (s) + r = dns_stream_write_packet(s, reply); + else { + int fd, ifindex; + + fd = find_socket_fd(m, l, p->family, &p->destination, SOCK_DGRAM); + if (fd < 0) + return fd; + + if (address_is_proxy(p->family, &p->destination)) + /* Force loopback iface if this is the loopback proxy stub + * and ifindex was normalized to 0 by manager_recv(). */ + ifindex = p->ifindex ?: LOOPBACK_IFINDEX; + else + /* Force loopback iface if this is the main listener stub. */ + ifindex = l ? p->ifindex : LOOPBACK_IFINDEX; + + /* Note that it is essential here that we explicitly choose the source IP address for this + * packet. This is because otherwise the kernel will choose it automatically based on the + * routing table and will thus pick 127.0.0.1 rather than 127.0.0.53/54. */ + r = manager_send(m, + fd, + ifindex, + p->family, &p->sender, p->sender_port, &p->destination, + reply); + } + if (r < 0) + return log_debug_errno(r, "Failed to send reply packet: %m"); + + return 0; +} + +static int dns_stub_reply_with_edns0_do(DnsQuery *q) { + assert(q); + + /* Reply with DNSSEC DO set? Only if client supports it; and we did any DNSSEC verification + * ourselves, or consider the data fully authenticated because we generated it locally, or the client + * set cd */ + + return DNS_PACKET_DO(q->request_packet) && + (q->answer_dnssec_result >= 0 || /* we did proper DNSSEC validation … */ + dns_query_fully_authenticated(q) || /* … or we considered it authentic otherwise … */ + DNS_PACKET_CD(q->request_packet)); /* … or client set CD */ +} + +static void dns_stub_suppress_duplicate_section_rrs(DnsQuery *q) { + /* If we follow a CNAME/DNAME chain we might end up populating our sections with redundant RRs + * because we built up the sections from multiple reply packets (one from each CNAME/DNAME chain + * element). E.g. it could be that an RR that was included in the first reply's additional section + * ends up being relevant as main answer in a subsequent reply in the chain. Let's clean this up, and + * remove everything in the "higher priority" sections from the "lower priority" sections. + * + * Note that this removal matches by RR keys instead of the full RRs. This is because RRsets should + * always end up in one section fully or not at all, but never be split among sections. + * + * Specifically: we remove ANSWER section RRs from the AUTHORITATIVE and ADDITIONAL sections, as well + * as AUTHORITATIVE section RRs from the ADDITIONAL section. */ + + dns_answer_remove_by_answer_keys(&q->reply_authoritative, q->reply_answer); + dns_answer_remove_by_answer_keys(&q->reply_additional, q->reply_answer); + dns_answer_remove_by_answer_keys(&q->reply_additional, q->reply_authoritative); +} + +static int dns_stub_send_reply( + DnsQuery *q, + int rcode) { + + _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL; + bool truncated, edns0_do; + int r; + + assert(q); + + edns0_do = dns_stub_reply_with_edns0_do(q); /* let's check if we shall reply with EDNS0 DO? */ + + r = dns_stub_make_reply_packet( + &reply, + DNS_PACKET_PAYLOAD_SIZE_MAX(q->request_packet), + q->request_packet->question, + &truncated); + if (r < 0) + return log_debug_errno(r, "Failed to build reply packet: %m"); + + dns_stub_suppress_duplicate_section_rrs(q); + + r = dns_stub_add_reply_packet_body( + reply, + q->reply_answer, + q->reply_authoritative, + q->reply_additional, + edns0_do, + &truncated); + if (r < 0) + return log_debug_errno(r, "Failed to append reply packet body: %m"); + + r = dns_stub_finish_reply_packet( + reply, + DNS_PACKET_ID(q->request_packet), + rcode, + truncated, + dns_query_fully_authoritative(q), + DNS_PACKET_RD(q->request_packet), + !!q->request_packet->opt, + edns0_do, + (DNS_PACKET_AD(q->request_packet) || DNS_PACKET_DO(q->request_packet)) && dns_query_fully_authenticated(q), + DNS_PACKET_CD(q->request_packet), + q->stub_listener_extra ? ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX : ADVERTISE_DATAGRAM_SIZE_MAX, + dns_packet_has_nsid_request(q->request_packet) > 0 && !q->stub_listener_extra); + if (r < 0) + return log_debug_errno(r, "Failed to build failure packet: %m"); + + return dns_stub_send(q->manager, q->stub_listener_extra, q->request_stream, q->request_packet, reply); +} + +static int dns_stub_send_failure( + Manager *m, + DnsStubListenerExtra *l, + DnsStream *s, + DnsPacket *p, + int rcode, + bool authenticated) { + + _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL; + bool truncated; + int r; + + assert(m); + assert(p); + + r = dns_stub_make_reply_packet( + &reply, + DNS_PACKET_PAYLOAD_SIZE_MAX(p), + p->question, + &truncated); + if (r < 0) + return log_debug_errno(r, "Failed to make failure packet: %m"); + + r = dns_stub_finish_reply_packet( + reply, + DNS_PACKET_ID(p), + rcode, + truncated, + false, + DNS_PACKET_RD(p), + !!p->opt, + DNS_PACKET_DO(p), + (DNS_PACKET_AD(p) || DNS_PACKET_DO(p)) && authenticated, + DNS_PACKET_CD(p), + l ? ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX : ADVERTISE_DATAGRAM_SIZE_MAX, + dns_packet_has_nsid_request(p) > 0 && !l); + if (r < 0) + return log_debug_errno(r, "Failed to build failure packet: %m"); + + return dns_stub_send(m, l, s, p, reply); +} + +static int dns_stub_patch_bypass_reply_packet( + DnsPacket **ret, /* Where to place the patched packet */ + DnsPacket *original, /* The packet to patch */ + DnsPacket *request) { /* The packet the patched packet shall look like a reply to */ + _cleanup_(dns_packet_unrefp) DnsPacket *c = NULL; + int r; + + assert(ret); + assert(original); + assert(request); + + r = dns_packet_dup(&c, original); + if (r < 0) + return r; + + /* Extract the packet, so that we know where the OPT field is */ + r = dns_packet_extract(c); + if (r < 0) + return r; + + /* Copy over the original client request ID, so that we can make the upstream query look like our own reply. */ + DNS_PACKET_HEADER(c)->id = DNS_PACKET_HEADER(request)->id; + + /* Patch in our own maximum datagram size, if EDNS0 was on */ + r = dns_packet_patch_max_udp_size(c, ADVERTISE_DATAGRAM_SIZE_MAX); + if (r < 0) + return r; + + /* Lower all TTLs by the time passed since we received the datagram. */ + if (timestamp_is_set(original->timestamp)) { + r = dns_packet_patch_ttls(c, original->timestamp); + if (r < 0) + return r; + } + + /* Our upstream connection might have supported larger DNS requests than our downstream one, hence + * set the TC bit if our reply is larger than what the client supports, and truncate. */ + if (c->size > DNS_PACKET_PAYLOAD_SIZE_MAX(request)) { + log_debug("Artificially truncating stub response, as advertised size of client is smaller than upstream one."); + dns_packet_truncate(c, DNS_PACKET_PAYLOAD_SIZE_MAX(request)); + DNS_PACKET_HEADER(c)->flags = htobe16(be16toh(DNS_PACKET_HEADER(c)->flags) | DNS_PACKET_FLAG_TC); + } + + *ret = TAKE_PTR(c); + return 0; +} + +static void dns_stub_query_complete(DnsQuery *query) { + _cleanup_(dns_query_freep) DnsQuery *q = query; + int r; + + assert(q); + assert(q->request_packet); + + if (q->question_bypass) { + /* This is a bypass reply. If so, let's propagate the upstream packet, if we have it and it + * is regular DNS. (We can't do this if the upstream packet is LLMNR or mDNS, since the + * packets are not 100% compatible.) */ + + if (q->answer_full_packet && + q->answer_full_packet->protocol == DNS_PROTOCOL_DNS) { + _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL; + + r = dns_stub_patch_bypass_reply_packet(&reply, q->answer_full_packet, q->request_packet); + if (r < 0) + log_debug_errno(r, "Failed to patch bypass reply packet: %m"); + else + (void) dns_stub_send(q->manager, q->stub_listener_extra, q->request_stream, q->request_packet, reply); + + return; + } + } + + /* Take all data from the current reply, and merge it into the three reply sections we are building + * up. We do this before processing CNAME redirects, so that we gradually build up our sections, and + * and keep adding all RRs in the CNAME chain. */ + r = dns_stub_assign_sections( + q, + dns_query_question_for_protocol(q, DNS_PROTOCOL_DNS), + dns_stub_reply_with_edns0_do(q)); + if (r < 0) + return (void) log_debug_errno(r, "Failed to assign sections: %m"); + + switch (q->state) { + + case DNS_TRANSACTION_SUCCESS: { + bool first = true; + + for (;;) { + int cname_result; + + cname_result = dns_query_process_cname_one(q); + if (cname_result == -ELOOP) { /* CNAME loop, let's send what we already have */ + log_debug("Detected CNAME loop, returning what we already have."); + (void) dns_stub_send_reply(q, q->answer_rcode); + break; + } + if (cname_result < 0) { + log_debug_errno(cname_result, "Failed to process CNAME: %m"); + break; + } + + if (cname_result == DNS_QUERY_NOMATCH) { + /* This answer doesn't contain any RR that would answer our question + * positively, i.e. neither directly nor via CNAME. */ + + if (first) /* We never followed a CNAME and the answer doesn't match our + * question at all? Then this is final, the empty answer is the + * answer. */ + break; + + /* Otherwise, we already followed a CNAME once within this packet, and the + * packet doesn't answer our question. In that case let's restart the query, + * now with the redirected question. We'll */ + r = dns_query_go(q); + if (r < 0) + return (void) log_debug_errno(r, "Failed to restart query: %m"); + + TAKE_PTR(q); + return; + } + + r = dns_stub_assign_sections( + q, + dns_query_question_for_protocol(q, DNS_PROTOCOL_DNS), + dns_stub_reply_with_edns0_do(q)); + if (r < 0) + return (void) log_debug_errno(r, "Failed to assign sections: %m"); + + if (cname_result == DNS_QUERY_MATCH) /* A match? Then we are done, let's return what we got */ + break; + + /* We followed a CNAME. and collected the RRs that answer the redirected question + * successfully. Let's not try to do this again. */ + assert(cname_result == DNS_QUERY_CNAME); + first = false; + } + + _fallthrough_; + } + + case DNS_TRANSACTION_RCODE_FAILURE: + (void) dns_stub_send_reply(q, q->answer_rcode); + break; + + case DNS_TRANSACTION_NOT_FOUND: + (void) dns_stub_send_reply(q, DNS_RCODE_NXDOMAIN); + break; + + case DNS_TRANSACTION_TIMEOUT: + case DNS_TRANSACTION_ATTEMPTS_MAX_REACHED: + /* Propagate a timeout as a no packet, i.e. that the client also gets a timeout */ + break; + + case DNS_TRANSACTION_NO_SERVERS: + case DNS_TRANSACTION_INVALID_REPLY: + case DNS_TRANSACTION_ERRNO: + case DNS_TRANSACTION_ABORTED: + case DNS_TRANSACTION_DNSSEC_FAILED: + case DNS_TRANSACTION_NO_TRUST_ANCHOR: + case DNS_TRANSACTION_RR_TYPE_UNSUPPORTED: + case DNS_TRANSACTION_NETWORK_DOWN: + case DNS_TRANSACTION_NO_SOURCE: + case DNS_TRANSACTION_STUB_LOOP: + (void) dns_stub_send_reply(q, DNS_RCODE_SERVFAIL); + break; + + case DNS_TRANSACTION_NULL: + case DNS_TRANSACTION_PENDING: + case DNS_TRANSACTION_VALIDATING: + default: + assert_not_reached(); + } +} + +static int dns_stub_stream_complete(DnsStream *s, int error) { + assert(s); + + log_debug_errno(error, "DNS TCP connection terminated, destroying queries: %m"); + + for (;;) { + DnsQuery *q; + + q = set_first(s->queries); + if (!q) + break; + + dns_query_free(q); + } + + /* This drops the implicit ref we keep around since it was allocated, as incoming stub connections + * should be kept as long as the client wants to. */ + dns_stream_unref(s); + return 0; +} + +static void dns_stub_process_query(Manager *m, DnsStubListenerExtra *l, DnsStream *s, DnsPacket *p) { + uint64_t protocol_flags = SD_RESOLVED_PROTOCOLS_ALL; + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + Hashmap **queries_by_packet; + DnsQuery *existing; + bool bypass = false; + int r; + + assert(m); + assert(p); + assert(p->protocol == DNS_PROTOCOL_DNS); + + if (!l && /* l == NULL if this is the main stub */ + !address_is_proxy(p->family, &p->destination) && /* don't restrict needlessly for 127.0.0.54 */ + (in_addr_is_localhost(p->family, &p->sender) <= 0 || + in_addr_is_localhost(p->family, &p->destination) <= 0)) { + log_warning("Got packet on unexpected (i.e. non-localhost) IP range, ignoring."); + return; + } + + if (manager_packet_from_our_transaction(m, p)) { + log_debug("Got our own packet looped back, ignoring."); + return; + } + + queries_by_packet = l ? &l->queries_by_packet : &m->stub_queries_by_packet; + existing = hashmap_get(*queries_by_packet, p); + if (existing && dns_packet_equal(existing->request_packet, p)) { + log_debug("Got repeat packet from client, ignoring."); + return; + } + + r = dns_packet_extract(p); + if (r < 0) { + log_debug_errno(r, "Failed to extract resources from incoming packet, ignoring packet: %m"); + dns_stub_send_failure(m, l, s, p, DNS_RCODE_FORMERR, false); + return; + } + + if (!DNS_PACKET_VERSION_SUPPORTED(p)) { + log_debug("Got EDNS OPT field with unsupported version number."); + dns_stub_send_failure(m, l, s, p, DNS_RCODE_BADVERS, false); + return; + } + + if (dns_type_is_obsolete(dns_question_first_key(p->question)->type)) { + log_debug("Got message with obsolete key type, refusing."); + dns_stub_send_failure(m, l, s, p, DNS_RCODE_REFUSED, false); + return; + } + + if (dns_type_is_zone_transer(dns_question_first_key(p->question)->type)) { + log_debug("Got request for zone transfer, refusing."); + dns_stub_send_failure(m, l, s, p, DNS_RCODE_REFUSED, false); + return; + } + + if (!DNS_PACKET_RD(p)) { + /* If the "rd" bit is off (i.e. recursion was not requested), then refuse operation */ + log_debug("Got request with recursion disabled, refusing."); + dns_stub_send_failure(m, l, s, p, DNS_RCODE_REFUSED, false); + return; + } + + r = hashmap_ensure_allocated(queries_by_packet, &stub_packet_hash_ops); + if (r < 0) { + log_oom(); + return; + } + + if (address_is_proxy(p->family, &p->destination)) { + _cleanup_free_ char *dipa = NULL; + + r = in_addr_to_string(p->family, &p->destination, &dipa); + if (r < 0) + return (void) log_error_errno(r, "Failed to format destination address: %m"); + + log_debug("Got request to DNS proxy address 127.0.0.54, enabling bypass logic."); + bypass = true; + protocol_flags = SD_RESOLVED_DNS|SD_RESOLVED_NO_ZONE; /* Turn off mDNS/LLMNR for proxy stub. */ + } else if ((DNS_PACKET_DO(p) && DNS_PACKET_CD(p))) { + log_debug("Got request with DNSSEC checking disabled, enabling bypass logic."); + bypass = true; + } + + if (bypass) + r = dns_query_new(m, &q, NULL, NULL, p, 0, + protocol_flags| + SD_RESOLVED_NO_CNAME| + SD_RESOLVED_NO_SEARCH| + SD_RESOLVED_NO_VALIDATE| + SD_RESOLVED_REQUIRE_PRIMARY| + SD_RESOLVED_CLAMP_TTL); + else + r = dns_query_new(m, &q, p->question, p->question, NULL, 0, + protocol_flags| + SD_RESOLVED_NO_SEARCH| + (DNS_PACKET_DO(p) ? SD_RESOLVED_REQUIRE_PRIMARY : 0)| + SD_RESOLVED_CLAMP_TTL); + if (r < 0) { + log_error_errno(r, "Failed to generate query object: %m"); + dns_stub_send_failure(m, l, s, p, DNS_RCODE_SERVFAIL, false); + return; + } + + q->request_packet = dns_packet_ref(p); + q->request_stream = dns_stream_ref(s); /* make sure the stream stays around until we can send a reply through it */ + q->stub_listener_extra = l; + q->complete = dns_stub_query_complete; + + if (s) { + /* Remember which queries belong to this stream, so that we can cancel them when the stream + * is disconnected early */ + + r = set_ensure_put(&s->queries, NULL, q); + if (r < 0) { + log_oom(); + return; + } + assert(r > 0); + } + + /* Add the query to the hash table we use to determine repeat packets now. We don't care about + * failures here, since in the worst case we'll not recognize duplicate incoming requests, which + * isn't particularly bad. */ + (void) hashmap_put(*queries_by_packet, q->request_packet, q); + + r = dns_query_go(q); + if (r < 0) { + log_error_errno(r, "Failed to start query: %m"); + dns_stub_send_failure(m, l, s, p, DNS_RCODE_SERVFAIL, false); + return; + } + + log_debug("Processing query..."); + TAKE_PTR(q); +} + +static int on_dns_stub_packet_internal(sd_event_source *s, int fd, uint32_t revents, Manager *m, DnsStubListenerExtra *l) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + int r; + + r = manager_recv(m, fd, DNS_PROTOCOL_DNS, &p); + if (r <= 0) + return r; + + if (dns_packet_validate_query(p) > 0) { + log_debug("Got DNS stub UDP query packet for id %u", DNS_PACKET_ID(p)); + + dns_stub_process_query(m, l, NULL, p); + } else + log_debug("Invalid DNS stub UDP packet, ignoring."); + + return 0; +} + +static int on_dns_stub_packet(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + return on_dns_stub_packet_internal(s, fd, revents, userdata, NULL); +} + +static int on_dns_stub_packet_extra(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + DnsStubListenerExtra *l = ASSERT_PTR(userdata); + + return on_dns_stub_packet_internal(s, fd, revents, l->manager, l); +} + +static int on_dns_stub_stream_packet(DnsStream *s, DnsPacket *p) { + assert(s); + assert(s->manager); + assert(p); + + if (dns_packet_validate_query(p) > 0) { + log_debug("Got DNS stub TCP query packet for id %u", DNS_PACKET_ID(p)); + + dns_stub_process_query(s->manager, s->stub_listener_extra, s, p); + } else + log_debug("Invalid DNS stub TCP packet, ignoring."); + + return 0; +} + +static int on_dns_stub_stream_internal(sd_event_source *s, int fd, uint32_t revents, Manager *m, DnsStubListenerExtra *l) { + DnsStream *stream; + int cfd, r; + + cfd = accept4(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC); + if (cfd < 0) { + if (ERRNO_IS_ACCEPT_AGAIN(errno)) + return 0; + + return -errno; + } + + r = dns_stream_new(m, &stream, DNS_STREAM_STUB, DNS_PROTOCOL_DNS, cfd, NULL, + on_dns_stub_stream_packet, dns_stub_stream_complete, DNS_STREAM_STUB_TIMEOUT_USEC); + if (r < 0) { + safe_close(cfd); + return r; + } + + stream->stub_listener_extra = l; + + /* We let the reference to the stream dangle here, it will be dropped later by the complete callback. */ + + return 0; +} + +static int on_dns_stub_stream(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + return on_dns_stub_stream_internal(s, fd, revents, userdata, NULL); +} + +static int on_dns_stub_stream_extra(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + DnsStubListenerExtra *l = ASSERT_PTR(userdata); + + return on_dns_stub_stream_internal(s, fd, revents, l->manager, l); +} + +static int set_dns_stub_common_socket_options(int fd, int family) { + int r; + + assert(fd >= 0); + assert(IN_SET(family, AF_INET, AF_INET6)); + + r = setsockopt_int(fd, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return r; + + r = socket_set_recvpktinfo(fd, family, true); + if (r < 0) + return r; + + r = socket_set_recvttl(fd, family, true); + if (r < 0) + return r; + + return 0; +} + +static int set_dns_stub_common_tcp_socket_options(int fd) { + int r; + + assert(fd >= 0); + + r = setsockopt_int(fd, IPPROTO_TCP, TCP_FASTOPEN, 5); /* Everybody appears to pick qlen=5, let's do the same here. */ + if (r < 0) + log_debug_errno(r, "Failed to enable TCP_FASTOPEN on TCP listening socket, ignoring: %m"); + + r = setsockopt_int(fd, IPPROTO_TCP, TCP_NODELAY, true); + if (r < 0) + log_debug_errno(r, "Failed to enable TCP_NODELAY mode, ignoring: %m"); + + return 0; +} + +static int manager_dns_stub_fd( + Manager *m, + int family, + const union in_addr_union *listen_addr, + int type) { + + sd_event_source **event_source; + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + int r; + + assert(m); + assert(listen_addr); + + if (type == SOCK_DGRAM) + event_source = address_is_proxy(family, listen_addr) ? &m->dns_proxy_stub_udp_event_source : &m->dns_stub_udp_event_source; + else if (type == SOCK_STREAM) + event_source = address_is_proxy(family, listen_addr) ? &m->dns_proxy_stub_tcp_event_source : &m->dns_stub_tcp_event_source; + else + return -EPROTONOSUPPORT; + + if (*event_source) + return sd_event_source_get_io_fd(*event_source); + + fd = socket(family, type | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd < 0) + return -errno; + + r = set_dns_stub_common_socket_options(fd, family); + if (r < 0) + return r; + + if (type == SOCK_STREAM) { + r = set_dns_stub_common_tcp_socket_options(fd); + if (r < 0) + return r; + } + + /* Set slightly different socket options for the non-proxy and the proxy binding. The former we want + * to be accessible only from the local host, for the latter it's OK if people use NAT redirects or + * so to redirect external traffic to it. */ + + if (!address_is_proxy(family, listen_addr)) { + /* Make sure no traffic from outside the local host can leak to onto this socket */ + r = socket_bind_to_ifindex(fd, LOOPBACK_IFINDEX); + if (r < 0) + return r; + + r = socket_set_ttl(fd, family, 1); + if (r < 0) + return r; + } else if (type == SOCK_DGRAM) { + /* Turn off Path MTU Discovery for UDP, for security reasons. See socket_disable_pmtud() for + * a longer discussion. (We only do this for sockets that are potentially externally + * accessible, i.e. the proxy stub one. For the non-proxy one we instead set the TTL to 1, + * see above, so that packets don't get routed at all.) */ + r = socket_disable_pmtud(fd, family); + if (r < 0) + log_debug_errno(r, "Failed to disable UDP PMTUD, ignoring: %m"); + + r = socket_set_recvfragsize(fd, family, true); + if (r < 0) + log_debug_errno(r, "Failed to enable fragment size reception, ignoring: %m"); + } + + r = sockaddr_set_in_addr(&sa, family, listen_addr, 53); + if (r < 0) + return r; + + if (bind(fd, &sa.sa, sizeof(sa.in)) < 0) + return -errno; + + if (type == SOCK_STREAM && + listen(fd, SOMAXCONN_DELUXE) < 0) + return -errno; + + r = sd_event_add_io(m->event, event_source, fd, EPOLLIN, + type == SOCK_DGRAM ? on_dns_stub_packet : on_dns_stub_stream, + m); + if (r < 0) + return r; + + r = sd_event_source_set_io_fd_own(*event_source, true); + if (r < 0) + return r; + + (void) sd_event_source_set_description(*event_source, + type == SOCK_DGRAM ? "dns-stub-udp" : "dns-stub-tcp"); + + return TAKE_FD(fd); +} + +static int manager_dns_stub_fd_extra(Manager *m, DnsStubListenerExtra *l, int type) { + _cleanup_free_ char *pretty = NULL; + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + int r; + + assert(m); + assert(l); + assert(IN_SET(type, SOCK_DGRAM, SOCK_STREAM)); + + sd_event_source **event_source = type == SOCK_DGRAM ? &l->udp_event_source : &l->tcp_event_source; + if (*event_source) + return sd_event_source_get_io_fd(*event_source); + + if (!have_effective_cap(CAP_NET_BIND_SERVICE) && dns_stub_listener_extra_port(l) < 1024) { + log_warning("Missing CAP_NET_BIND_SERVICE capability, not creating extra stub listener on port %hu.", + dns_stub_listener_extra_port(l)); + return 0; + } + + if (l->family == AF_INET) + sa = (union sockaddr_union) { + .in.sin_family = l->family, + .in.sin_port = htobe16(dns_stub_listener_extra_port(l)), + .in.sin_addr = l->address.in, + }; + else + sa = (union sockaddr_union) { + .in6.sin6_family = l->family, + .in6.sin6_port = htobe16(dns_stub_listener_extra_port(l)), + .in6.sin6_addr = l->address.in6, + }; + + fd = socket(l->family, type | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (fd < 0) { + r = -errno; + goto fail; + } + + r = set_dns_stub_common_socket_options(fd, l->family); + if (r < 0) + goto fail; + + if (type == SOCK_STREAM) { + r = set_dns_stub_common_tcp_socket_options(fd); + if (r < 0) + goto fail; + } + + /* Do not set IP_TTL for extra DNS stub listeners, as the address may not be local and in that case + * people may want ttl > 1. */ + + r = socket_set_freebind(fd, l->family, true); + if (r < 0) + goto fail; + + if (type == SOCK_DGRAM) { + r = socket_disable_pmtud(fd, l->family); + if (r < 0) + log_debug_errno(r, "Failed to disable UDP PMTUD, ignoring: %m"); + + r = socket_set_recvfragsize(fd, l->family, true); + if (r < 0) + log_debug_errno(r, "Failed to enable fragment size reception, ignoring: %m"); + } + + r = RET_NERRNO(bind(fd, &sa.sa, SOCKADDR_LEN(sa))); + if (r < 0) + goto fail; + + if (type == SOCK_STREAM && + listen(fd, SOMAXCONN_DELUXE) < 0) { + r = -errno; + goto fail; + } + + r = sd_event_add_io(m->event, event_source, fd, EPOLLIN, + type == SOCK_DGRAM ? on_dns_stub_packet_extra : on_dns_stub_stream_extra, + l); + if (r < 0) + goto fail; + + r = sd_event_source_set_io_fd_own(*event_source, true); + if (r < 0) + goto fail; + + (void) sd_event_source_set_description(*event_source, + type == SOCK_DGRAM ? "dns-stub-udp-extra" : "dns-stub-tcp-extra"); + + if (DEBUG_LOGGING) { + (void) in_addr_port_to_string(l->family, &l->address, l->port, &pretty); + log_debug("Listening on %s socket %s.", + type == SOCK_DGRAM ? "UDP" : "TCP", + strnull(pretty)); + } + + return TAKE_FD(fd); + +fail: + assert(r < 0); + (void) in_addr_port_to_string(l->family, &l->address, l->port, &pretty); + return log_warning_errno(r, + r == -EADDRINUSE ? "Another process is already listening on %s socket %s: %m" : + "Failed to listen on %s socket %s: %m", + type == SOCK_DGRAM ? "UDP" : "TCP", + strnull(pretty)); +} + +int manager_dns_stub_start(Manager *m) { + int r; + + assert(m); + + if (m->dns_stub_listener_mode == DNS_STUB_LISTENER_NO) + log_debug("Not creating stub listener."); + else if (!have_effective_cap(CAP_NET_BIND_SERVICE)) + log_warning("Missing CAP_NET_BIND_SERVICE capability, not creating stub listener on port 53."); + else { + static const struct { + uint32_t addr; + int socket_type; + } stub_sockets[] = { + { INADDR_DNS_STUB, SOCK_DGRAM }, + { INADDR_DNS_STUB, SOCK_STREAM }, + { INADDR_DNS_PROXY_STUB, SOCK_DGRAM }, + { INADDR_DNS_PROXY_STUB, SOCK_STREAM }, + }; + + log_debug("Creating stub listener using %s.", + m->dns_stub_listener_mode == DNS_STUB_LISTENER_UDP ? "UDP" : + m->dns_stub_listener_mode == DNS_STUB_LISTENER_TCP ? "TCP" : + "UDP/TCP"); + + for (size_t i = 0; i < ELEMENTSOF(stub_sockets); i++) { + union in_addr_union a = { + .in.s_addr = htobe32(stub_sockets[i].addr), + }; + + if (m->dns_stub_listener_mode == DNS_STUB_LISTENER_UDP && stub_sockets[i].socket_type == SOCK_STREAM) + continue; + if (m->dns_stub_listener_mode == DNS_STUB_LISTENER_TCP && stub_sockets[i].socket_type == SOCK_DGRAM) + continue; + + r = manager_dns_stub_fd(m, AF_INET, &a, stub_sockets[i].socket_type); + if (r < 0) { + _cleanup_free_ char *busy_socket = NULL; + + if (asprintf(&busy_socket, + "%s socket " IPV4_ADDRESS_FMT_STR ":53", + stub_sockets[i].socket_type == SOCK_DGRAM ? "UDP" : "TCP", + IPV4_ADDRESS_FMT_VAL(a.in)) < 0) + return log_oom(); + + if (IN_SET(r, -EADDRINUSE, -EPERM)) { + log_warning_errno(r, + r == -EADDRINUSE ? "Another process is already listening on %s.\n" + "Turning off local DNS stub support." : + "Failed to listen on %s: %m.\n" + "Turning off local DNS stub support.", + busy_socket); + manager_dns_stub_stop(m); + break; + } + + return log_error_errno(r, "Failed to listen on %s: %m", busy_socket); + } + } + } + + if (!ordered_set_isempty(m->dns_extra_stub_listeners)) { + DnsStubListenerExtra *l; + + log_debug("Creating extra stub listeners."); + + ORDERED_SET_FOREACH(l, m->dns_extra_stub_listeners) { + if (FLAGS_SET(l->mode, DNS_STUB_LISTENER_UDP)) + (void) manager_dns_stub_fd_extra(m, l, SOCK_DGRAM); + if (FLAGS_SET(l->mode, DNS_STUB_LISTENER_TCP)) + (void) manager_dns_stub_fd_extra(m, l, SOCK_STREAM); + } + } + + return 0; +} + +void manager_dns_stub_stop(Manager *m) { + assert(m); + + m->dns_stub_udp_event_source = sd_event_source_disable_unref(m->dns_stub_udp_event_source); + m->dns_stub_tcp_event_source = sd_event_source_disable_unref(m->dns_stub_tcp_event_source); + m->dns_proxy_stub_udp_event_source = sd_event_source_disable_unref(m->dns_proxy_stub_udp_event_source); + m->dns_proxy_stub_tcp_event_source = sd_event_source_disable_unref(m->dns_proxy_stub_tcp_event_source); +} + +static const char* const dns_stub_listener_mode_table[_DNS_STUB_LISTENER_MODE_MAX] = { + [DNS_STUB_LISTENER_NO] = "no", + [DNS_STUB_LISTENER_UDP] = "udp", + [DNS_STUB_LISTENER_TCP] = "tcp", + [DNS_STUB_LISTENER_YES] = "yes", +}; +DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(dns_stub_listener_mode, DnsStubListenerMode, DNS_STUB_LISTENER_YES); diff --git a/src/resolve/resolved-dns-stub.h b/src/resolve/resolved-dns-stub.h new file mode 100644 index 0000000..3b9bf65 --- /dev/null +++ b/src/resolve/resolved-dns-stub.h @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "hash-funcs.h" + +typedef struct DnsStubListenerExtra DnsStubListenerExtra; + +typedef enum DnsStubListenerMode { + DNS_STUB_LISTENER_NO, + DNS_STUB_LISTENER_UDP = 1 << 0, + DNS_STUB_LISTENER_TCP = 1 << 1, + DNS_STUB_LISTENER_YES = DNS_STUB_LISTENER_UDP | DNS_STUB_LISTENER_TCP, + _DNS_STUB_LISTENER_MODE_MAX, + _DNS_STUB_LISTENER_MODE_INVALID = -EINVAL, +} DnsStubListenerMode; + +#include "resolved-manager.h" + +struct DnsStubListenerExtra { + Manager *manager; + + DnsStubListenerMode mode; + + int family; + union in_addr_union address; + uint16_t port; + + sd_event_source *udp_event_source; + sd_event_source *tcp_event_source; + + Hashmap *queries_by_packet; +}; + +extern const struct hash_ops dns_stub_listener_extra_hash_ops; + +int dns_stub_listener_extra_new(Manager *m, DnsStubListenerExtra **ret); +DnsStubListenerExtra *dns_stub_listener_extra_free(DnsStubListenerExtra *p); +static inline uint16_t dns_stub_listener_extra_port(DnsStubListenerExtra *p) { + assert(p); + + return p->port > 0 ? p->port : 53; +} + +void manager_dns_stub_stop(Manager *m); +int manager_dns_stub_start(Manager *m); + +const char* dns_stub_listener_mode_to_string(DnsStubListenerMode p) _const_; +DnsStubListenerMode dns_stub_listener_mode_from_string(const char *s) _pure_; diff --git a/src/resolve/resolved-dns-synthesize.c b/src/resolve/resolved-dns-synthesize.c new file mode 100644 index 0000000..b3442ad --- /dev/null +++ b/src/resolve/resolved-dns-synthesize.c @@ -0,0 +1,484 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "env-util.h" +#include "hostname-util.h" +#include "local-addresses.h" +#include "missing_network.h" +#include "resolved-dns-synthesize.h" + +int dns_synthesize_ifindex(int ifindex) { + + /* When the caller asked for resolving on a specific + * interface, we synthesize the answer for that + * interface. However, if nothing specific was claimed and we + * only return localhost RRs, we synthesize the answer for + * localhost. */ + + if (ifindex > 0) + return ifindex; + + return LOOPBACK_IFINDEX; +} + +int dns_synthesize_family(uint64_t flags) { + + /* Picks an address family depending on set flags. This is + * purely for synthesized answers, where the family we return + * for the reply should match what was requested in the + * question, even though we are synthesizing the answer + * here. */ + + if (!(flags & SD_RESOLVED_DNS)) { + if (flags & (SD_RESOLVED_LLMNR_IPV4|SD_RESOLVED_MDNS_IPV4)) + return AF_INET; + if (flags & (SD_RESOLVED_LLMNR_IPV6|SD_RESOLVED_MDNS_IPV6)) + return AF_INET6; + } + + return AF_UNSPEC; +} + +DnsProtocol dns_synthesize_protocol(uint64_t flags) { + + /* Similar as dns_synthesize_family() but does this for the + * protocol. If resolving via DNS was requested, we claim it + * was DNS. Similar, if nothing specific was + * requested. However, if only resolving via LLMNR was + * requested we return that. */ + + if (flags & SD_RESOLVED_DNS) + return DNS_PROTOCOL_DNS; + if (flags & SD_RESOLVED_LLMNR) + return DNS_PROTOCOL_LLMNR; + if (flags & SD_RESOLVED_MDNS) + return DNS_PROTOCOL_MDNS; + + return DNS_PROTOCOL_DNS; +} + +static int synthesize_localhost_rr(Manager *m, const DnsResourceKey *key, int ifindex, DnsAnswer **answer) { + int r; + + assert(m); + assert(key); + assert(answer); + + r = dns_answer_reserve(answer, 2); + if (r < 0) + return r; + + if (IN_SET(key->type, DNS_TYPE_A, DNS_TYPE_ANY)) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_A, dns_resource_key_name(key)); + if (!rr) + return -ENOMEM; + + rr->a.in_addr.s_addr = htobe32(INADDR_LOOPBACK); + + r = dns_answer_add(*answer, rr, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return r; + } + + if (IN_SET(key->type, DNS_TYPE_AAAA, DNS_TYPE_ANY) && socket_ipv6_is_enabled()) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_AAAA, dns_resource_key_name(key)); + if (!rr) + return -ENOMEM; + + rr->aaaa.in6_addr = in6addr_loopback; + + r = dns_answer_add(*answer, rr, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int answer_add_ptr(DnsAnswer **answer, const char *from, const char *to, int ifindex, DnsAnswerFlags flags) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR, from); + if (!rr) + return -ENOMEM; + + rr->ptr.name = strdup(to); + if (!rr->ptr.name) + return -ENOMEM; + + return dns_answer_add(*answer, rr, ifindex, flags, NULL); +} + +static int synthesize_localhost_ptr(Manager *m, const DnsResourceKey *key, int ifindex, DnsAnswer **answer) { + int r; + + assert(m); + assert(key); + assert(answer); + + if (IN_SET(key->type, DNS_TYPE_PTR, DNS_TYPE_ANY)) { + r = dns_answer_reserve(answer, 1); + if (r < 0) + return r; + + r = answer_add_ptr(answer, dns_resource_key_name(key), "localhost", dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED); + if (r < 0) + return r; + } + + return 0; +} + +static int answer_add_addresses_rr( + DnsAnswer **answer, + const char *name, + struct local_address *addresses, + unsigned n_addresses) { + + unsigned j; + int r; + + assert(answer); + assert(name); + + r = dns_answer_reserve(answer, n_addresses); + if (r < 0) + return r; + + for (j = 0; j < n_addresses; j++) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + r = dns_resource_record_new_address(&rr, addresses[j].family, &addresses[j].address, name); + if (r < 0) + return r; + + r = dns_answer_add(*answer, rr, addresses[j].ifindex, DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return r; + } + + return 0; +} + +static int answer_add_addresses_ptr( + DnsAnswer **answer, + const char *name, + struct local_address *addresses, + unsigned n_addresses, + int af, const union in_addr_union *match) { + + bool added = false; + unsigned j; + int r; + + assert(answer); + assert(name); + + for (j = 0; j < n_addresses; j++) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + if (af != AF_UNSPEC) { + + if (addresses[j].family != af) + continue; + + if (match && !in_addr_equal(af, match, &addresses[j].address)) + continue; + } + + r = dns_answer_reserve(answer, 1); + if (r < 0) + return r; + + r = dns_resource_record_new_reverse(&rr, addresses[j].family, &addresses[j].address, name); + if (r < 0) + return r; + + r = dns_answer_add(*answer, rr, addresses[j].ifindex, DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return r; + + added = true; + } + + return added; +} + +static int synthesize_system_hostname_rr(Manager *m, const DnsResourceKey *key, int ifindex, DnsAnswer **answer) { + _cleanup_free_ struct local_address *addresses = NULL; + int n = 0, af; + + assert(m); + assert(key); + assert(answer); + + af = dns_type_to_af(key->type); + if (af >= 0) { + n = local_addresses(m->rtnl, ifindex, af, &addresses); + if (n < 0) + return n; + + if (n == 0) { + struct local_address buffer[2]; + + /* If we have no local addresses then use ::1 + * and 127.0.0.2 as local ones. */ + + if (IN_SET(af, AF_INET, AF_UNSPEC)) + buffer[n++] = (struct local_address) { + .family = AF_INET, + .ifindex = dns_synthesize_ifindex(ifindex), + .address.in.s_addr = htobe32(0x7F000002), + }; + + if (IN_SET(af, AF_INET6, AF_UNSPEC) && socket_ipv6_is_enabled()) + buffer[n++] = (struct local_address) { + .family = AF_INET6, + .ifindex = dns_synthesize_ifindex(ifindex), + .address.in6 = in6addr_loopback, + }; + + return answer_add_addresses_rr(answer, + dns_resource_key_name(key), + buffer, n); + } + } + + return answer_add_addresses_rr(answer, dns_resource_key_name(key), addresses, n); +} + +static int synthesize_system_hostname_ptr(Manager *m, int af, const union in_addr_union *address, int ifindex, DnsAnswer **answer) { + _cleanup_free_ struct local_address *addresses = NULL; + bool added = false; + int n, r; + + assert(m); + assert(address); + assert(answer); + + if (af == AF_INET && address->in.s_addr == htobe32(0x7F000002)) { + + /* Always map the IPv4 address 127.0.0.2 to the local hostname, in addition to "localhost": */ + + r = dns_answer_reserve(answer, 4); + if (r < 0) + return r; + + r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", m->full_hostname, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED); + if (r < 0) + return r; + + r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", m->llmnr_hostname, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED); + if (r < 0) + return r; + + r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", m->mdns_hostname, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED); + if (r < 0) + return r; + + r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", "localhost", dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED); + if (r < 0) + return r; + + return 1; + } + + n = local_addresses(m->rtnl, ifindex, af, &addresses); + if (n <= 0) + return n; + + r = answer_add_addresses_ptr(answer, m->full_hostname, addresses, n, af, address); + if (r < 0) + return r; + if (r > 0) + added = true; + + r = answer_add_addresses_ptr(answer, m->llmnr_hostname, addresses, n, af, address); + if (r < 0) + return r; + if (r > 0) + added = true; + + r = answer_add_addresses_ptr(answer, m->mdns_hostname, addresses, n, af, address); + if (r < 0) + return r; + if (r > 0) + added = true; + + return added; +} + +static int synthesize_gateway_rr( + Manager *m, + const DnsResourceKey *key, + int ifindex, + int (*lookup)(sd_netlink *context, int ifindex, int af, struct local_address **ret), /* either local_gateways() or local_outbound() */ + DnsAnswer **answer) { + _cleanup_free_ struct local_address *addresses = NULL; + int n = 0, af, r; + + assert(m); + assert(key); + assert(lookup); + assert(answer); + + af = dns_type_to_af(key->type); + if (af >= 0) { + n = lookup(m->rtnl, ifindex, af, &addresses); + if (n < 0) /* < 0 means: error */ + return n; + + if (n == 0) { /* == 0 means we have no gateway */ + /* See if there's a gateway on the other protocol */ + if (af == AF_INET) + n = lookup(m->rtnl, ifindex, AF_INET6, NULL); + else { + assert(af == AF_INET6); + n = lookup(m->rtnl, ifindex, AF_INET, NULL); + } + if (n <= 0) /* error (if < 0) or really no gateway at all (if == 0) */ + return n; + + /* We have a gateway on the other protocol. Let's return > 0 without adding any RR to + * the answer, i.e. synthesize NODATA (and not NXDOMAIN!) */ + return 1; + } + } + + r = answer_add_addresses_rr(answer, dns_resource_key_name(key), addresses, n); + if (r < 0) + return r; + + return 1; /* > 0 means: we have some gateway */ +} + +static int synthesize_gateway_ptr(Manager *m, int af, const union in_addr_union *address, int ifindex, DnsAnswer **answer) { + _cleanup_free_ struct local_address *addresses = NULL; + int n; + + assert(m); + assert(address); + assert(answer); + + n = local_gateways(m->rtnl, ifindex, af, &addresses); + if (n <= 0) + return n; + + return answer_add_addresses_ptr(answer, "_gateway", addresses, n, af, address); +} + +int dns_synthesize_answer( + Manager *m, + DnsQuestion *q, + int ifindex, + DnsAnswer **ret) { + + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnsResourceKey *key; + bool found = false, nxdomain = false; + int r; + + assert(m); + assert(q); + + DNS_QUESTION_FOREACH(key, q) { + union in_addr_union address; + const char *name; + int af; + + if (!IN_SET(key->class, DNS_CLASS_IN, DNS_CLASS_ANY)) + continue; + + name = dns_resource_key_name(key); + + if (dns_name_is_empty(name)) { + /* Do nothing. */ + + } else if (dns_name_dont_resolve(name)) { + /* Synthesize NXDOMAIN for some of the domains in RFC6303 + RFC6761 */ + nxdomain = true; + continue; + + } else if (is_localhost(name)) { + + r = synthesize_localhost_rr(m, key, ifindex, &answer); + if (r < 0) + return log_error_errno(r, "Failed to synthesize localhost RRs: %m"); + + } else if (manager_is_own_hostname(m, name)) { + + if (getenv_bool("SYSTEMD_RESOLVED_SYNTHESIZE_HOSTNAME") == 0) + continue; + r = synthesize_system_hostname_rr(m, key, ifindex, &answer); + if (r < 0) + return log_error_errno(r, "Failed to synthesize system hostname RRs: %m"); + + } else if (is_gateway_hostname(name)) { + + r = synthesize_gateway_rr(m, key, ifindex, local_gateways, &answer); + if (r < 0) + return log_error_errno(r, "Failed to synthesize gateway RRs: %m"); + if (r == 0) { /* if we have no gateway return NXDOMAIN */ + nxdomain = true; + continue; + } + + } else if (is_outbound_hostname(name)) { + + r = synthesize_gateway_rr(m, key, ifindex, local_outbounds, &answer); + if (r < 0) + return log_error_errno(r, "Failed to synthesize outbound RRs: %m"); + if (r == 0) { /* if we have no gateway return NXDOMAIN */ + nxdomain = true; + continue; + } + + } else if ((dns_name_endswith(name, "127.in-addr.arpa") > 0 && dns_name_equal(name, "2.0.0.127.in-addr.arpa") == 0) || + dns_name_equal(name, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa") > 0) { + + r = synthesize_localhost_ptr(m, key, ifindex, &answer); + if (r < 0) + return log_error_errno(r, "Failed to synthesize localhost PTR RRs: %m"); + + } else if (dns_name_address(name, &af, &address) > 0) { + int v, w; + + if (getenv_bool("SYSTEMD_RESOLVED_SYNTHESIZE_HOSTNAME") == 0) + continue; + + v = synthesize_system_hostname_ptr(m, af, &address, ifindex, &answer); + if (v < 0) + return log_error_errno(v, "Failed to synthesize system hostname PTR RR: %m"); + + w = synthesize_gateway_ptr(m, af, &address, ifindex, &answer); + if (w < 0) + return log_error_errno(w, "Failed to synthesize gateway hostname PTR RR: %m"); + + if (v == 0 && w == 0) /* This IP address is neither a local one nor a gateway */ + continue; + + /* Note that we never synthesize reverse PTR for _outbound, since those are local + * addresses and thus mapped to the local hostname anyway, hence they already have a + * mapping. */ + + } else + continue; + + found = true; + } + + if (found) { + + if (ret) + *ret = TAKE_PTR(answer); + + return 1; + } else if (nxdomain) + return -ENXIO; + + return 0; +} diff --git a/src/resolve/resolved-dns-synthesize.h b/src/resolve/resolved-dns-synthesize.h new file mode 100644 index 0000000..fb62458 --- /dev/null +++ b/src/resolve/resolved-dns-synthesize.h @@ -0,0 +1,12 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "resolved-dns-answer.h" +#include "resolved-dns-question.h" +#include "resolved-manager.h" + +int dns_synthesize_ifindex(int ifindex); +int dns_synthesize_family(uint64_t flags); +DnsProtocol dns_synthesize_protocol(uint64_t flags); + +int dns_synthesize_answer(Manager *m, DnsQuestion *q, int ifindex, DnsAnswer **ret); diff --git a/src/resolve/resolved-dns-transaction.c b/src/resolve/resolved-dns-transaction.c new file mode 100644 index 0000000..1850d45 --- /dev/null +++ b/src/resolve/resolved-dns-transaction.c @@ -0,0 +1,3666 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-messages.h" + +#include "af-list.h" +#include "alloc-util.h" +#include "dns-domain.h" +#include "errno-list.h" +#include "errno-util.h" +#include "fd-util.h" +#include "glyph-util.h" +#include "random-util.h" +#include "resolved-dns-cache.h" +#include "resolved-dns-transaction.h" +#include "resolved-dnstls.h" +#include "resolved-llmnr.h" +#include "string-table.h" + +#define TRANSACTIONS_MAX 4096 +#define TRANSACTION_TCP_TIMEOUT_USEC (10U*USEC_PER_SEC) + +/* After how much time to repeat classic DNS requests */ +#define DNS_TIMEOUT_USEC (SD_RESOLVED_QUERY_TIMEOUT_USEC / DNS_TRANSACTION_ATTEMPTS_MAX) + +static void dns_transaction_reset_answer(DnsTransaction *t) { + assert(t); + + t->received = dns_packet_unref(t->received); + t->answer = dns_answer_unref(t->answer); + t->answer_rcode = 0; + t->answer_dnssec_result = _DNSSEC_RESULT_INVALID; + t->answer_source = _DNS_TRANSACTION_SOURCE_INVALID; + t->answer_query_flags = 0; + t->answer_nsec_ttl = UINT32_MAX; + t->answer_errno = 0; +} + +static void dns_transaction_flush_dnssec_transactions(DnsTransaction *t) { + DnsTransaction *z; + + assert(t); + + while ((z = set_steal_first(t->dnssec_transactions))) { + set_remove(z->notify_transactions, t); + set_remove(z->notify_transactions_done, t); + dns_transaction_gc(z); + } +} + +static void dns_transaction_close_connection( + DnsTransaction *t, + bool use_graveyard) { /* Set use_graveyard = false when you know the connection is already + * dead, for example because you got a connection error back from the + * kernel. In that case there's no point in keeping the fd around, + * hence don't. */ + int r; + + assert(t); + + if (t->stream) { + /* Let's detach the stream from our transaction, in case something else keeps a reference to it. */ + LIST_REMOVE(transactions_by_stream, t->stream->transactions, t); + + /* Remove packet in case it's still in the queue */ + dns_packet_unref(ordered_set_remove(t->stream->write_queue, t->sent)); + + t->stream = dns_stream_unref(t->stream); + } + + t->dns_udp_event_source = sd_event_source_disable_unref(t->dns_udp_event_source); + + /* If we have an UDP socket where we sent a packet, but never received one, then add it to the socket + * graveyard, instead of closing it right away. That way it will stick around for a moment longer, + * and the reply we might still get from the server will be eaten up instead of resulting in an ICMP + * port unreachable error message. */ + + /* Skip the graveyard stuff when we're shutting down, since that requires running event loop */ + if (!t->scope->manager->event || sd_event_get_state(t->scope->manager->event) == SD_EVENT_FINISHED) + use_graveyard = false; + + if (use_graveyard && t->dns_udp_fd >= 0 && t->sent && !t->received) { + r = manager_add_socket_to_graveyard(t->scope->manager, t->dns_udp_fd); + if (r < 0) + log_debug_errno(r, "Failed to add UDP socket to graveyard, closing immediately: %m"); + else + TAKE_FD(t->dns_udp_fd); + } + + t->dns_udp_fd = safe_close(t->dns_udp_fd); +} + +static void dns_transaction_stop_timeout(DnsTransaction *t) { + assert(t); + + t->timeout_event_source = sd_event_source_disable_unref(t->timeout_event_source); +} + +DnsTransaction* dns_transaction_free(DnsTransaction *t) { + DnsQueryCandidate *c; + DnsZoneItem *i; + DnsTransaction *z; + + if (!t) + return NULL; + + log_debug("Freeing transaction %" PRIu16 ".", t->id); + + dns_transaction_close_connection(t, true); + dns_transaction_stop_timeout(t); + + dns_packet_unref(t->sent); + dns_transaction_reset_answer(t); + + dns_server_unref(t->server); + + if (t->scope) { + if (t->key) { + DnsTransaction *first; + + first = hashmap_get(t->scope->transactions_by_key, t->key); + LIST_REMOVE(transactions_by_key, first, t); + if (first) + hashmap_replace(t->scope->transactions_by_key, first->key, first); + else + hashmap_remove(t->scope->transactions_by_key, t->key); + } + + LIST_REMOVE(transactions_by_scope, t->scope->transactions, t); + + if (t->id != 0) + hashmap_remove(t->scope->manager->dns_transactions, UINT_TO_PTR(t->id)); + } + + while ((c = set_steal_first(t->notify_query_candidates))) + set_remove(c->transactions, t); + set_free(t->notify_query_candidates); + + while ((c = set_steal_first(t->notify_query_candidates_done))) + set_remove(c->transactions, t); + set_free(t->notify_query_candidates_done); + + while ((i = set_steal_first(t->notify_zone_items))) + i->probe_transaction = NULL; + set_free(t->notify_zone_items); + + while ((i = set_steal_first(t->notify_zone_items_done))) + i->probe_transaction = NULL; + set_free(t->notify_zone_items_done); + + while ((z = set_steal_first(t->notify_transactions))) + set_remove(z->dnssec_transactions, t); + set_free(t->notify_transactions); + + while ((z = set_steal_first(t->notify_transactions_done))) + set_remove(z->dnssec_transactions, t); + set_free(t->notify_transactions_done); + + dns_transaction_flush_dnssec_transactions(t); + set_free(t->dnssec_transactions); + + dns_answer_unref(t->validated_keys); + dns_resource_key_unref(t->key); + dns_packet_unref(t->bypass); + + return mfree(t); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsTransaction*, dns_transaction_free); + +DnsTransaction* dns_transaction_gc(DnsTransaction *t) { + assert(t); + + /* Returns !NULL if we can't gc yet. */ + + if (t->block_gc > 0) + return t; + + if (set_isempty(t->notify_query_candidates) && + set_isempty(t->notify_query_candidates_done) && + set_isempty(t->notify_zone_items) && + set_isempty(t->notify_zone_items_done) && + set_isempty(t->notify_transactions) && + set_isempty(t->notify_transactions_done)) + return dns_transaction_free(t); + + return t; +} + +static uint16_t pick_new_id(Manager *m) { + uint16_t new_id; + + /* Find a fresh, unused transaction id. Note that this loop is bounded because there's a limit on the + * number of transactions, and it's much lower than the space of IDs. */ + + assert_cc(TRANSACTIONS_MAX < 0xFFFF); + + do + random_bytes(&new_id, sizeof(new_id)); + while (new_id == 0 || + hashmap_get(m->dns_transactions, UINT_TO_PTR(new_id))); + + return new_id; +} + +static int key_ok( + DnsScope *scope, + DnsResourceKey *key) { + + /* Don't allow looking up invalid or pseudo RRs */ + if (!dns_type_is_valid_query(key->type)) + return -EINVAL; + if (dns_type_is_obsolete(key->type)) + return -EOPNOTSUPP; + + /* We only support the IN class */ + if (!IN_SET(key->class, DNS_CLASS_IN, DNS_CLASS_ANY)) + return -EOPNOTSUPP; + + /* Don't allows DNSSEC RRs to be looked up via LLMNR/mDNS. They don't really make sense + * there, and it speeds up our queries if we refuse this early */ + if (scope->protocol != DNS_PROTOCOL_DNS && + dns_type_is_dnssec(key->type)) + return -EOPNOTSUPP; + + return 0; +} + +int dns_transaction_new( + DnsTransaction **ret, + DnsScope *s, + DnsResourceKey *key, + DnsPacket *bypass, + uint64_t query_flags) { + + _cleanup_(dns_transaction_freep) DnsTransaction *t = NULL; + int r; + + assert(ret); + assert(s); + + if (key) { + assert(!bypass); + + r = key_ok(s, key); + if (r < 0) + return r; + } else { + DnsResourceKey *qk; + assert(bypass); + + r = dns_packet_validate_query(bypass); + if (r < 0) + return r; + + DNS_QUESTION_FOREACH(qk, bypass->question) { + r = key_ok(s, qk); + if (r < 0) + return r; + } + } + + if (hashmap_size(s->manager->dns_transactions) >= TRANSACTIONS_MAX) + return -EBUSY; + + r = hashmap_ensure_allocated(&s->manager->dns_transactions, NULL); + if (r < 0) + return r; + + if (key) { + r = hashmap_ensure_allocated(&s->transactions_by_key, &dns_resource_key_hash_ops); + if (r < 0) + return r; + } + + t = new(DnsTransaction, 1); + if (!t) + return -ENOMEM; + + *t = (DnsTransaction) { + .dns_udp_fd = -1, + .answer_source = _DNS_TRANSACTION_SOURCE_INVALID, + .answer_dnssec_result = _DNSSEC_RESULT_INVALID, + .answer_nsec_ttl = UINT32_MAX, + .key = dns_resource_key_ref(key), + .query_flags = query_flags, + .bypass = dns_packet_ref(bypass), + .current_feature_level = _DNS_SERVER_FEATURE_LEVEL_INVALID, + .clamp_feature_level_servfail = _DNS_SERVER_FEATURE_LEVEL_INVALID, + .clamp_feature_level_nxdomain = _DNS_SERVER_FEATURE_LEVEL_INVALID, + .id = pick_new_id(s->manager), + }; + + r = hashmap_put(s->manager->dns_transactions, UINT_TO_PTR(t->id), t); + if (r < 0) { + t->id = 0; + return r; + } + + if (t->key) { + DnsTransaction *first; + + first = hashmap_get(s->transactions_by_key, t->key); + LIST_PREPEND(transactions_by_key, first, t); + + r = hashmap_replace(s->transactions_by_key, first->key, first); + if (r < 0) { + LIST_REMOVE(transactions_by_key, first, t); + return r; + } + } + + LIST_PREPEND(transactions_by_scope, s->transactions, t); + t->scope = s; + + s->manager->n_transactions_total++; + + if (ret) + *ret = t; + + TAKE_PTR(t); + return 0; +} + +static void dns_transaction_shuffle_id(DnsTransaction *t) { + uint16_t new_id; + assert(t); + + /* Pick a new ID for this transaction. */ + + new_id = pick_new_id(t->scope->manager); + assert_se(hashmap_remove_and_put(t->scope->manager->dns_transactions, UINT_TO_PTR(t->id), UINT_TO_PTR(new_id), t) >= 0); + + log_debug("Transaction %" PRIu16 " is now %" PRIu16 ".", t->id, new_id); + t->id = new_id; + + /* Make sure we generate a new packet with the new ID */ + t->sent = dns_packet_unref(t->sent); +} + +static void dns_transaction_tentative(DnsTransaction *t, DnsPacket *p) { + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + DnsZoneItem *z; + + assert(t); + assert(p); + assert(t->scope->protocol == DNS_PROTOCOL_LLMNR); + + if (manager_packet_from_local_address(t->scope->manager, p) != 0) + return; + + log_debug("Transaction %" PRIu16 " for <%s> on scope %s on %s/%s got tentative packet from %s.", + t->id, + dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str), + dns_protocol_to_string(t->scope->protocol), + t->scope->link ? t->scope->link->ifname : "*", + af_to_name_short(t->scope->family), + IN_ADDR_TO_STRING(p->family, &p->sender)); + + /* RFC 4795, Section 4.1 says that the peer with the + * lexicographically smaller IP address loses */ + if (memcmp(&p->sender, &p->destination, FAMILY_ADDRESS_SIZE(p->family)) >= 0) { + log_debug("Peer has lexicographically larger IP address and thus lost in the conflict."); + return; + } + + log_debug("We have the lexicographically larger IP address and thus lost in the conflict."); + + t->block_gc++; + + while ((z = set_first(t->notify_zone_items))) { + /* First, make sure the zone item drops the reference + * to us */ + dns_zone_item_probe_stop(z); + + /* Secondly, report this as conflict, so that we might + * look for a different hostname */ + dns_zone_item_conflict(z); + } + t->block_gc--; + + dns_transaction_gc(t); +} + +void dns_transaction_complete(DnsTransaction *t, DnsTransactionState state) { + DnsQueryCandidate *c; + DnsZoneItem *z; + DnsTransaction *d; + const char *st; + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + assert(t); + assert(!DNS_TRANSACTION_IS_LIVE(state)); + + if (state == DNS_TRANSACTION_DNSSEC_FAILED) { + dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str); + + log_struct(LOG_NOTICE, + "MESSAGE_ID=" SD_MESSAGE_DNSSEC_FAILURE_STR, + LOG_MESSAGE("DNSSEC validation failed for question %s: %s", + key_str, dnssec_result_to_string(t->answer_dnssec_result)), + "DNS_TRANSACTION=%" PRIu16, t->id, + "DNS_QUESTION=%s", key_str, + "DNSSEC_RESULT=%s", dnssec_result_to_string(t->answer_dnssec_result), + "DNS_SERVER=%s", strna(dns_server_string_full(t->server)), + "DNS_SERVER_FEATURE_LEVEL=%s", dns_server_feature_level_to_string(t->server->possible_feature_level)); + } + + /* Note that this call might invalidate the query. Callers + * should hence not attempt to access the query or transaction + * after calling this function. */ + + if (state == DNS_TRANSACTION_ERRNO) + st = errno_to_name(t->answer_errno); + else + st = dns_transaction_state_to_string(state); + + log_debug("%s transaction %" PRIu16 " for <%s> on scope %s on %s/%s now complete with <%s> from %s (%s; %s).", + t->bypass ? "Bypass" : "Regular", + t->id, + dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str), + dns_protocol_to_string(t->scope->protocol), + t->scope->link ? t->scope->link->ifname : "*", + af_to_name_short(t->scope->family), + st, + t->answer_source < 0 ? "none" : dns_transaction_source_to_string(t->answer_source), + FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) ? "not validated" : + (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED) ? "authenticated" : "unsigned"), + FLAGS_SET(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL) ? "confidential" : "non-confidential"); + + t->state = state; + + dns_transaction_close_connection(t, true); + dns_transaction_stop_timeout(t); + + /* Notify all queries that are interested, but make sure the + * transaction isn't freed while we are still looking at it */ + t->block_gc++; + + SET_FOREACH_MOVE(c, t->notify_query_candidates_done, t->notify_query_candidates) + dns_query_candidate_notify(c); + SWAP_TWO(t->notify_query_candidates, t->notify_query_candidates_done); + + SET_FOREACH_MOVE(z, t->notify_zone_items_done, t->notify_zone_items) + dns_zone_item_notify(z); + SWAP_TWO(t->notify_zone_items, t->notify_zone_items_done); + if (t->probing && t->state == DNS_TRANSACTION_ATTEMPTS_MAX_REACHED) + (void) dns_scope_announce(t->scope, false); + + SET_FOREACH_MOVE(d, t->notify_transactions_done, t->notify_transactions) + dns_transaction_notify(d, t); + SWAP_TWO(t->notify_transactions, t->notify_transactions_done); + + t->block_gc--; + dns_transaction_gc(t); +} + +static void dns_transaction_complete_errno(DnsTransaction *t, int error) { + assert(t); + assert(error != 0); + + t->answer_errno = abs(error); + dns_transaction_complete(t, DNS_TRANSACTION_ERRNO); +} + +static int dns_transaction_pick_server(DnsTransaction *t) { + DnsServer *server; + + assert(t); + assert(t->scope->protocol == DNS_PROTOCOL_DNS); + + /* Pick a DNS server and a feature level for it. */ + + server = dns_scope_get_dns_server(t->scope); + if (!server) + return -ESRCH; + + /* If we changed the server invalidate the feature level clamping, as the new server might have completely + * different properties. */ + if (server != t->server) { + t->clamp_feature_level_servfail = _DNS_SERVER_FEATURE_LEVEL_INVALID; + t->clamp_feature_level_nxdomain = _DNS_SERVER_FEATURE_LEVEL_INVALID; + } + + t->current_feature_level = dns_server_possible_feature_level(server); + + /* Clamp the feature level if that is requested. */ + if (t->clamp_feature_level_servfail != _DNS_SERVER_FEATURE_LEVEL_INVALID && + t->current_feature_level > t->clamp_feature_level_servfail) + t->current_feature_level = t->clamp_feature_level_servfail; + if (t->clamp_feature_level_nxdomain != _DNS_SERVER_FEATURE_LEVEL_INVALID && + t->current_feature_level > t->clamp_feature_level_nxdomain) + t->current_feature_level = t->clamp_feature_level_nxdomain; + + log_debug("Using feature level %s for transaction %u.", dns_server_feature_level_to_string(t->current_feature_level), t->id); + + if (server == t->server) + return 0; + + dns_server_unref(t->server); + t->server = dns_server_ref(server); + + t->n_picked_servers ++; + + log_debug("Using DNS server %s for transaction %u.", strna(dns_server_string_full(t->server)), t->id); + + return 1; +} + +static void dns_transaction_retry(DnsTransaction *t, bool next_server) { + int r; + + assert(t); + + /* Retries the transaction as it is, possibly on a different server */ + + if (next_server && t->scope->protocol == DNS_PROTOCOL_DNS) + log_debug("Retrying transaction %" PRIu16 ", after switching servers.", t->id); + else + log_debug("Retrying transaction %" PRIu16 ".", t->id); + + /* Before we try again, switch to a new server. */ + if (next_server) + dns_scope_next_dns_server(t->scope, t->server); + + r = dns_transaction_go(t); + if (r < 0) + dns_transaction_complete_errno(t, r); +} + +static bool dns_transaction_limited_retry(DnsTransaction *t) { + assert(t); + + /* If we haven't tried all different servers yet, let's try again with a different server */ + + if (t->n_picked_servers >= dns_scope_get_n_dns_servers(t->scope)) + return false; + + dns_transaction_retry(t, /* next_server= */ true); + return true; +} + +static int dns_transaction_maybe_restart(DnsTransaction *t) { + int r; + + assert(t); + + /* Restarts the transaction, under a new ID if the feature level of the server changed since we first + * tried, without changing DNS server. Returns > 0 if the transaction was restarted, 0 if not. */ + + if (!t->server) + return 0; + + if (t->current_feature_level <= dns_server_possible_feature_level(t->server)) + return 0; + + /* The server's current feature level is lower than when we sent the original query. We learnt something from + the response or possibly an auxiliary DNSSEC response that we didn't know before. We take that as reason to + restart the whole transaction. This is a good idea to deal with servers that respond rubbish if we include + OPT RR or DO bit. One of these cases is documented here, for example: + https://open.nlnetlabs.nl/pipermail/dnssec-trigger/2014-November/000376.html */ + + log_debug("Server feature level is now lower than when we began our transaction. Restarting with new ID."); + dns_transaction_shuffle_id(t); + + r = dns_transaction_go(t); + if (r < 0) + return r; + + return 1; +} + +static void on_transaction_stream_error(DnsTransaction *t, int error) { + assert(t); + + dns_transaction_close_connection(t, true); + + if (ERRNO_IS_DISCONNECT(error)) { + if (t->scope->protocol == DNS_PROTOCOL_LLMNR) { + /* If the LLMNR/TCP connection failed, the host doesn't support LLMNR, and we cannot answer the + * question on this scope. */ + dns_transaction_complete(t, DNS_TRANSACTION_NOT_FOUND); + return; + } + + dns_transaction_retry(t, true); + return; + } + if (error != 0) + dns_transaction_complete_errno(t, error); +} + +static int dns_transaction_on_stream_packet(DnsTransaction *t, DnsStream *s, DnsPacket *p) { + bool encrypted; + + assert(t); + assert(s); + assert(p); + + encrypted = s->encrypted; + + dns_transaction_close_connection(t, true); + + if (dns_packet_validate_reply(p) <= 0) { + log_debug("Invalid TCP reply packet."); + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + return 0; + } + + dns_scope_check_conflicts(t->scope, p); + + t->block_gc++; + dns_transaction_process_reply(t, p, encrypted); + t->block_gc--; + + /* If the response wasn't useful, then complete the transition + * now. After all, we are the worst feature set now with TCP + * sockets, and there's really no point in retrying. */ + if (t->state == DNS_TRANSACTION_PENDING) + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + else + dns_transaction_gc(t); + + return 0; +} + +static int on_stream_complete(DnsStream *s, int error) { + assert(s); + + if (ERRNO_IS_DISCONNECT(error) && s->protocol != DNS_PROTOCOL_LLMNR) { + log_debug_errno(error, "Connection failure for DNS TCP stream: %m"); + + if (s->transactions) { + DnsTransaction *t; + + t = s->transactions; + dns_server_packet_lost(t->server, IPPROTO_TCP, t->current_feature_level); + } + } + + if (error != 0) { + /* First, detach the stream from the server. Otherwise, transactions attached to this stream + * may be restarted by on_transaction_stream_error() below with this stream. */ + dns_stream_detach(s); + + /* Do not use LIST_FOREACH() here, as + * on_transaction_stream_error() + * -> dns_transaction_complete_errno() + * -> dns_transaction_free() + * may free multiple transactions in the list. */ + DnsTransaction *t; + while ((t = s->transactions)) + on_transaction_stream_error(t, error); + } + + return 0; +} + +static int on_stream_packet(DnsStream *s, DnsPacket *p) { + DnsTransaction *t; + + assert(s); + assert(s->manager); + assert(p); + + t = hashmap_get(s->manager->dns_transactions, UINT_TO_PTR(DNS_PACKET_ID(p))); + if (t && t->stream == s) /* Validate that the stream we got this on actually is the stream the + * transaction was using. */ + return dns_transaction_on_stream_packet(t, s, p); + + /* Ignore incorrect transaction id as an old transaction can have been canceled. */ + log_debug("Received unexpected TCP reply packet with id %" PRIu16 ", ignoring.", DNS_PACKET_ID(p)); + return 0; +} + +static uint16_t dns_transaction_port(DnsTransaction *t) { + assert(t); + + if (t->server->port > 0) + return t->server->port; + + return DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level) ? 853 : 53; +} + +static int dns_transaction_emit_tcp(DnsTransaction *t) { + usec_t stream_timeout_usec = DNS_STREAM_DEFAULT_TIMEOUT_USEC; + _cleanup_(dns_stream_unrefp) DnsStream *s = NULL; + _cleanup_close_ int fd = -1; + union sockaddr_union sa; + DnsStreamType type; + int r; + + assert(t); + assert(t->sent); + + dns_transaction_close_connection(t, true); + + switch (t->scope->protocol) { + + case DNS_PROTOCOL_DNS: + r = dns_transaction_pick_server(t); + if (r < 0) + return r; + + if (manager_server_is_stub(t->scope->manager, t->server)) + return -ELOOP; + + if (!t->bypass) { + if (!dns_server_dnssec_supported(t->server) && dns_type_is_dnssec(dns_transaction_key(t)->type)) + return -EOPNOTSUPP; + + r = dns_server_adjust_opt(t->server, t->sent, t->current_feature_level); + if (r < 0) + return r; + } + + if (t->server->stream && (DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level) == t->server->stream->encrypted)) + s = dns_stream_ref(t->server->stream); + else + fd = dns_scope_socket_tcp(t->scope, AF_UNSPEC, NULL, t->server, dns_transaction_port(t), &sa); + + /* Lower timeout in DNS-over-TLS opportunistic mode. In environments where DoT is blocked + * without ICMP response overly long delays when contacting DoT servers are nasty, in + * particular if multiple DNS servers are defined which we try in turn and all are + * blocked. Hence, substantially lower the timeout in that case. */ + if (DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level) && + dns_server_get_dns_over_tls_mode(t->server) == DNS_OVER_TLS_OPPORTUNISTIC) + stream_timeout_usec = DNS_STREAM_OPPORTUNISTIC_TLS_TIMEOUT_USEC; + + type = DNS_STREAM_LOOKUP; + break; + + case DNS_PROTOCOL_LLMNR: + /* When we already received a reply to this (but it was truncated), send to its sender address */ + if (t->received) + fd = dns_scope_socket_tcp(t->scope, t->received->family, &t->received->sender, NULL, t->received->sender_port, &sa); + else { + union in_addr_union address; + int family = AF_UNSPEC; + + /* Otherwise, try to talk to the owner of a + * the IP address, in case this is a reverse + * PTR lookup */ + + r = dns_name_address(dns_resource_key_name(dns_transaction_key(t)), &family, &address); + if (r < 0) + return r; + if (r == 0) + return -EINVAL; + if (family != t->scope->family) + return -ESRCH; + + fd = dns_scope_socket_tcp(t->scope, family, &address, NULL, LLMNR_PORT, &sa); + } + + type = DNS_STREAM_LLMNR_SEND; + break; + + default: + return -EAFNOSUPPORT; + } + + if (!s) { + if (fd < 0) + return fd; + + r = dns_stream_new(t->scope->manager, &s, type, t->scope->protocol, fd, &sa, + on_stream_packet, on_stream_complete, stream_timeout_usec); + if (r < 0) + return r; + + fd = -1; + +#if ENABLE_DNS_OVER_TLS + if (t->scope->protocol == DNS_PROTOCOL_DNS && + DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level)) { + + assert(t->server); + r = dnstls_stream_connect_tls(s, t->server); + if (r < 0) + return r; + } +#endif + + if (t->server) { + dns_server_unref_stream(t->server); + s->server = dns_server_ref(t->server); + t->server->stream = dns_stream_ref(s); + } + + /* The interface index is difficult to determine if we are + * connecting to the local host, hence fill this in right away + * instead of determining it from the socket */ + s->ifindex = dns_scope_ifindex(t->scope); + } + + t->stream = TAKE_PTR(s); + LIST_PREPEND(transactions_by_stream, t->stream->transactions, t); + + r = dns_stream_write_packet(t->stream, t->sent); + if (r < 0) { + dns_transaction_close_connection(t, /* use_graveyard= */ false); + return r; + } + + dns_transaction_reset_answer(t); + + t->tried_stream = true; + + return 0; +} + +static void dns_transaction_cache_answer(DnsTransaction *t) { + assert(t); + + /* For mDNS we cache whenever we get the packet, rather than + * in each transaction. */ + if (!IN_SET(t->scope->protocol, DNS_PROTOCOL_DNS, DNS_PROTOCOL_LLMNR)) + return; + + /* Caching disabled? */ + if (t->scope->manager->enable_cache == DNS_CACHE_MODE_NO) + return; + + /* If validation is turned off for this transaction, but DNSSEC is on, then let's not cache this */ + if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) && t->scope->dnssec_mode != DNSSEC_NO) + return; + + /* Packet from localhost? */ + if (!t->scope->manager->cache_from_localhost && + in_addr_is_localhost(t->received->family, &t->received->sender) != 0) + return; + + dns_cache_put(&t->scope->cache, + t->scope->manager->enable_cache, + t->scope->protocol, + dns_transaction_key(t), + t->answer_rcode, + t->answer, + DNS_PACKET_CD(t->received) ? t->received : NULL, /* only cache full packets with CD on, + * since our usecase for caching them + * is "bypass" mode which is only + * enabled for CD packets. */ + t->answer_query_flags, + t->answer_dnssec_result, + t->answer_nsec_ttl, + t->received->family, + &t->received->sender); +} + +static bool dns_transaction_dnssec_is_live(DnsTransaction *t) { + DnsTransaction *dt; + + assert(t); + + SET_FOREACH(dt, t->dnssec_transactions) + if (DNS_TRANSACTION_IS_LIVE(dt->state)) + return true; + + return false; +} + +static int dns_transaction_dnssec_ready(DnsTransaction *t) { + DnsTransaction *dt; + int r; + + assert(t); + + /* Checks whether the auxiliary DNSSEC transactions of our transaction have completed, or are still + * ongoing. Returns 0, if we aren't ready for the DNSSEC validation, positive if we are. */ + + SET_FOREACH(dt, t->dnssec_transactions) { + + switch (dt->state) { + + case DNS_TRANSACTION_NULL: + case DNS_TRANSACTION_PENDING: + case DNS_TRANSACTION_VALIDATING: + /* Still ongoing */ + return 0; + + case DNS_TRANSACTION_RCODE_FAILURE: + if (!IN_SET(dt->answer_rcode, DNS_RCODE_NXDOMAIN, DNS_RCODE_SERVFAIL)) { + log_debug("Auxiliary DNSSEC RR query failed with rcode=%s.", FORMAT_DNS_RCODE(dt->answer_rcode)); + goto fail; + } + + /* Fall-through: NXDOMAIN/SERVFAIL is good enough for us. This is because some DNS servers + * erroneously return NXDOMAIN/SERVFAIL for empty non-terminals (Akamai...) or missing DS + * records (Facebook), and we need to handle that nicely, when asking for parent SOA or similar + * RRs to make unsigned proofs. */ + + case DNS_TRANSACTION_SUCCESS: + /* All good. */ + break; + + case DNS_TRANSACTION_DNSSEC_FAILED: + /* We handle DNSSEC failures different from other errors, as we care about the DNSSEC + * validation result */ + + log_debug("Auxiliary DNSSEC RR query failed validation: %s", dnssec_result_to_string(dt->answer_dnssec_result)); + t->answer_dnssec_result = dt->answer_dnssec_result; /* Copy error code over */ + dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED); + return 0; + + default: + log_debug("Auxiliary DNSSEC RR query failed with %s", dns_transaction_state_to_string(dt->state)); + goto fail; + } + } + + /* All is ready, we can go and validate */ + return 1; + +fail: + /* Some auxiliary DNSSEC transaction failed for some reason. Maybe we learned something about the + * server due to this failure, and the feature level is now different? Let's see and restart the + * transaction if so. If not, let's propagate the auxiliary failure. + * + * This is particularly relevant if an auxiliary request figured out that DNSSEC doesn't work, and we + * are in permissive DNSSEC mode, and thus should restart things without DNSSEC magic. */ + r = dns_transaction_maybe_restart(t); + if (r < 0) + return r; + if (r > 0) + return 0; /* don't validate just yet, we restarted things */ + + t->answer_dnssec_result = DNSSEC_FAILED_AUXILIARY; + dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED); + return 0; +} + +static void dns_transaction_process_dnssec(DnsTransaction *t) { + int r; + + assert(t); + + /* Are there ongoing DNSSEC transactions? If so, let's wait for them. */ + r = dns_transaction_dnssec_ready(t); + if (r < 0) + goto fail; + if (r == 0) /* We aren't ready yet (or one of our auxiliary transactions failed, and we shouldn't validate now */ + return; + + /* See if we learnt things from the additional DNSSEC transactions, that we didn't know before, and better + * restart the lookup immediately. */ + r = dns_transaction_maybe_restart(t); + if (r < 0) + goto fail; + if (r > 0) /* Transaction got restarted... */ + return; + + /* All our auxiliary DNSSEC transactions are complete now. Try + * to validate our RRset now. */ + r = dns_transaction_validate_dnssec(t); + if (r == -EBADMSG) { + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + return; + } + if (r < 0) + goto fail; + + if (t->answer_dnssec_result == DNSSEC_INCOMPATIBLE_SERVER && + t->scope->dnssec_mode == DNSSEC_YES) { + + /* We are not in automatic downgrade mode, and the server is bad. Let's try a different server, maybe + * that works. */ + + if (dns_transaction_limited_retry(t)) + return; + + /* OK, let's give up, apparently all servers we tried didn't work. */ + dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED); + return; + } + + if (!IN_SET(t->answer_dnssec_result, + _DNSSEC_RESULT_INVALID, /* No DNSSEC validation enabled */ + DNSSEC_VALIDATED, /* Answer is signed and validated successfully */ + DNSSEC_UNSIGNED, /* Answer is right-fully unsigned */ + DNSSEC_INCOMPATIBLE_SERVER)) { /* Server does not do DNSSEC (Yay, we are downgrade attack vulnerable!) */ + dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED); + return; + } + + if (t->answer_dnssec_result == DNSSEC_INCOMPATIBLE_SERVER) + dns_server_warn_downgrade(t->server); + + dns_transaction_cache_answer(t); + + if (t->answer_rcode == DNS_RCODE_SUCCESS) + dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS); + else + dns_transaction_complete(t, DNS_TRANSACTION_RCODE_FAILURE); + + return; + +fail: + dns_transaction_complete_errno(t, r); +} + +static int dns_transaction_has_positive_answer(DnsTransaction *t, DnsAnswerFlags *flags) { + int r; + + assert(t); + + /* Checks whether the answer is positive, i.e. either a direct + * answer to the question, or a CNAME/DNAME for it */ + + r = dns_answer_match_key(t->answer, dns_transaction_key(t), flags); + if (r != 0) + return r; + + r = dns_answer_find_cname_or_dname(t->answer, dns_transaction_key(t), NULL, flags); + if (r != 0) + return r; + + return false; +} + +static int dns_transaction_fix_rcode(DnsTransaction *t) { + int r; + + assert(t); + + /* Fix up the RCODE to SUCCESS if we get at least one matching RR in a response. Note that this contradicts the + * DNS RFCs a bit. Specifically, RFC 6604 Section 3 clarifies that the RCODE shall say something about a + * CNAME/DNAME chain element coming after the last chain element contained in the message, and not the first + * one included. However, it also indicates that not all DNS servers implement this correctly. Moreover, when + * using DNSSEC we usually only can prove the first element of a CNAME/DNAME chain anyway, hence let's settle + * on always processing the RCODE as referring to the immediate look-up we do, i.e. the first element of a + * CNAME/DNAME chain. This way, we uniformly handle CNAME/DNAME chains, regardless if the DNS server + * incorrectly implements RCODE, whether DNSSEC is in use, or whether the DNS server only supplied us with an + * incomplete CNAME/DNAME chain. + * + * Or in other words: if we get at least one positive reply in a message we patch NXDOMAIN to become SUCCESS, + * and then rely on the CNAME chasing logic to figure out that there's actually a CNAME error with a new + * lookup. */ + + if (t->answer_rcode != DNS_RCODE_NXDOMAIN) + return 0; + + r = dns_transaction_has_positive_answer(t, NULL); + if (r <= 0) + return r; + + t->answer_rcode = DNS_RCODE_SUCCESS; + return 0; +} + +void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypted) { + bool retry_with_tcp = false; + int r; + + assert(t); + assert(p); + assert(t->scope); + assert(t->scope->manager); + + if (t->state != DNS_TRANSACTION_PENDING) + return; + + /* Note that this call might invalidate the query. Callers + * should hence not attempt to access the query or transaction + * after calling this function. */ + + log_debug("Processing incoming packet of size %zu on transaction %" PRIu16" (rcode=%s).", + p->size, + t->id, FORMAT_DNS_RCODE(DNS_PACKET_RCODE(p))); + + switch (t->scope->protocol) { + + case DNS_PROTOCOL_LLMNR: + /* For LLMNR we will not accept any packets from other interfaces */ + + if (p->ifindex != dns_scope_ifindex(t->scope)) + return; + + if (p->family != t->scope->family) + return; + + /* Tentative packets are not full responses but still + * useful for identifying uniqueness conflicts during + * probing. */ + if (DNS_PACKET_LLMNR_T(p)) { + dns_transaction_tentative(t, p); + return; + } + + break; + + case DNS_PROTOCOL_MDNS: + /* For mDNS we will not accept any packets from other interfaces */ + + if (p->ifindex != dns_scope_ifindex(t->scope)) + return; + + if (p->family != t->scope->family) + return; + + break; + + case DNS_PROTOCOL_DNS: + /* Note that we do not need to verify the + * addresses/port numbers of incoming traffic, as we + * invoked connect() on our UDP socket in which case + * the kernel already does the needed verification for + * us. */ + break; + + default: + assert_not_reached(); + } + + if (t->received != p) + DNS_PACKET_REPLACE(t->received, dns_packet_ref(p)); + + t->answer_source = DNS_TRANSACTION_NETWORK; + + if (p->ipproto == IPPROTO_TCP) { + if (DNS_PACKET_TC(p)) { + /* Truncated via TCP? Somebody must be fucking with us */ + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + return; + } + + if (DNS_PACKET_ID(p) != t->id) { + /* Not the reply to our query? Somebody must be fucking with us */ + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + return; + } + } + + switch (t->scope->protocol) { + + case DNS_PROTOCOL_DNS: + assert(t->server); + + if (!t->bypass && + IN_SET(DNS_PACKET_RCODE(p), DNS_RCODE_FORMERR, DNS_RCODE_SERVFAIL, DNS_RCODE_NOTIMP)) { + + /* Request failed, immediately try again with reduced features */ + + if (t->current_feature_level <= DNS_SERVER_FEATURE_LEVEL_UDP) { + + /* This was already at UDP feature level? If so, it doesn't make sense to downgrade + * this transaction anymore, but let's see if it might make sense to send the request + * to a different DNS server instead. If not let's process the response, and accept the + * rcode. Note that we don't retry on TCP, since that's a suitable way to mitigate + * packet loss, but is not going to give us better rcodes should we actually have + * managed to get them already at UDP level. */ + + if (dns_transaction_limited_retry(t)) + return; + + /* Give up, accept the rcode */ + log_debug("Server returned error: %s", FORMAT_DNS_RCODE(DNS_PACKET_RCODE(p))); + break; + } + + /* SERVFAIL can happen for many reasons and may be transient. + * To avoid unnecessary downgrades retry once with the initial level. + * Check for clamp_feature_level_servfail having an invalid value as a sign that this is the + * first attempt to downgrade. If so, clamp to the current value so that the transaction + * is retried without actually downgrading. If the next try also fails we will downgrade by + * hitting the else branch below. */ + if (DNS_PACKET_RCODE(p) == DNS_RCODE_SERVFAIL && + t->clamp_feature_level_servfail < 0) { + t->clamp_feature_level_servfail = t->current_feature_level; + log_debug("Server returned error %s, retrying transaction.", + FORMAT_DNS_RCODE(DNS_PACKET_RCODE(p))); + } else { + /* Reduce this feature level by one and try again. */ + switch (t->current_feature_level) { + case DNS_SERVER_FEATURE_LEVEL_TLS_DO: + t->clamp_feature_level_servfail = DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN; + break; + case DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN + 1: + /* Skip plain TLS when TLS is not supported */ + t->clamp_feature_level_servfail = DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN - 1; + break; + default: + t->clamp_feature_level_servfail = t->current_feature_level - 1; + } + + log_debug("Server returned error %s, retrying transaction with reduced feature level %s.", + FORMAT_DNS_RCODE(DNS_PACKET_RCODE(p)), + dns_server_feature_level_to_string(t->clamp_feature_level_servfail)); + } + + dns_transaction_retry(t, false /* use the same server */); + return; + } + + if (DNS_PACKET_RCODE(p) == DNS_RCODE_REFUSED) { + /* This server refused our request? If so, try again, use a different server */ + log_debug("Server returned REFUSED, switching servers, and retrying."); + + if (dns_transaction_limited_retry(t)) + return; + + break; + } + + if (DNS_PACKET_TC(p)) + dns_server_packet_truncated(t->server, t->current_feature_level); + + break; + + case DNS_PROTOCOL_LLMNR: + case DNS_PROTOCOL_MDNS: + dns_scope_packet_received(t->scope, p->timestamp - t->start_usec); + break; + + default: + assert_not_reached(); + } + + if (DNS_PACKET_TC(p)) { + + /* Truncated packets for mDNS are not allowed. Give up immediately. */ + if (t->scope->protocol == DNS_PROTOCOL_MDNS) { + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + return; + } + + /* Response was truncated, let's try again with good old TCP */ + log_debug("Reply truncated, retrying via TCP."); + retry_with_tcp = true; + + } else if (t->scope->protocol == DNS_PROTOCOL_DNS && + DNS_PACKET_IS_FRAGMENTED(p)) { + + /* Report the fragment size, so that we downgrade from LARGE to regular EDNS0 if needed */ + if (t->server) + dns_server_packet_udp_fragmented(t->server, dns_packet_size_unfragmented(p)); + + if (t->current_feature_level > DNS_SERVER_FEATURE_LEVEL_UDP) { + /* Packet was fragmented. Let's retry with TCP to avoid fragmentation attack + * issues. (We don't do that on the lowest feature level however, since crappy DNS + * servers often do not implement TCP, hence falling back to TCP on fragmentation is + * counter-productive there.) */ + + log_debug("Reply fragmented, retrying via TCP. (Largest fragment size: %zu; Datagram size: %zu)", + p->fragsize, p->size); + retry_with_tcp = true; + } + } + + if (retry_with_tcp) { + r = dns_transaction_emit_tcp(t); + if (r == -ESRCH) { + /* No servers found? Damn! */ + dns_transaction_complete(t, DNS_TRANSACTION_NO_SERVERS); + return; + } + if (r == -EOPNOTSUPP) { + /* Tried to ask for DNSSEC RRs, on a server that doesn't do DNSSEC */ + dns_transaction_complete(t, DNS_TRANSACTION_RR_TYPE_UNSUPPORTED); + return; + } + if (r < 0) { + /* On LLMNR, if we cannot connect to the host, + * we immediately give up */ + if (t->scope->protocol != DNS_PROTOCOL_DNS) + goto fail; + + /* On DNS, couldn't send? Try immediately again, with a new server */ + if (dns_transaction_limited_retry(t)) + return; + + /* No new server to try, give up */ + dns_transaction_complete(t, DNS_TRANSACTION_ATTEMPTS_MAX_REACHED); + } + + return; + } + + /* After the superficial checks, actually parse the message. */ + r = dns_packet_extract(p); + if (r < 0) { + if (t->server) { + dns_server_packet_invalid(t->server, t->current_feature_level); + + r = dns_transaction_maybe_restart(t); + if (r < 0) + goto fail; + if (r > 0) /* Transaction got restarted... */ + return; + } + + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + return; + } + + if (t->scope->protocol == DNS_PROTOCOL_DNS && + !t->bypass && + DNS_PACKET_RCODE(p) == DNS_RCODE_NXDOMAIN && + p->opt && !DNS_PACKET_DO(p) && + DNS_SERVER_FEATURE_LEVEL_IS_EDNS0(t->current_feature_level) && + DNS_SERVER_FEATURE_LEVEL_IS_UDP(t->current_feature_level) && + t->scope->dnssec_mode != DNSSEC_YES) { + + /* Some captive portals are special in that the Aruba/Datavalet hardware will miss + * replacing the packets with the local server IP to point to the authenticated side + * of the network if EDNS0 is enabled. Instead they return NXDOMAIN, with DO bit set + * to zero... nothing to see here, yet respond with the captive portal IP, when using + * the more simple UDP level. + * + * Common portal names that fail like so are: + * secure.datavalet.io + * securelogin.arubanetworks.com + * securelogin.networks.mycompany.com + * + * Thus retry NXDOMAIN RCODES with a lower feature level. + * + * Do not lower the server's tracked feature level, as the captive portal should not + * be lying for the wider internet (e.g. _other_ queries were observed fine with + * EDNS0 on these networks, post auth), i.e. let's just lower the level transaction's + * feature level. + * + * This is reported as https://github.com/dns-violations/dns-violations/blob/master/2018/DVE-2018-0001.md + */ + + t->clamp_feature_level_nxdomain = DNS_SERVER_FEATURE_LEVEL_UDP; + + log_debug("Server returned error %s in EDNS0 mode, retrying transaction with reduced feature level %s (DVE-2018-0001 mitigation)", + FORMAT_DNS_RCODE(DNS_PACKET_RCODE(p)), + dns_server_feature_level_to_string(t->clamp_feature_level_nxdomain)); + + dns_transaction_retry(t, false /* use the same server */); + return; + } + + if (t->server) { + /* Report that we successfully received a valid packet with a good rcode after we initially got a bad + * rcode and subsequently downgraded the protocol */ + + if (IN_SET(DNS_PACKET_RCODE(p), DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN) && + t->clamp_feature_level_servfail != _DNS_SERVER_FEATURE_LEVEL_INVALID) + dns_server_packet_rcode_downgrade(t->server, t->clamp_feature_level_servfail); + + /* Report that the OPT RR was missing */ + if (!p->opt) + dns_server_packet_bad_opt(t->server, t->current_feature_level); + + /* Report that the server didn't copy our query DO bit from request to response */ + if (DNS_PACKET_DO(t->sent) && !DNS_PACKET_DO(t->received)) + dns_server_packet_do_off(t->server, t->current_feature_level); + + /* Report that we successfully received a packet. We keep track of the largest packet + * size/fragment size we got. Which is useful for announcing the EDNS(0) packet size we can + * receive to our server. */ + dns_server_packet_received(t->server, p->ipproto, t->current_feature_level, dns_packet_size_unfragmented(p)); + } + + /* See if we know things we didn't know before that indicate we better restart the lookup immediately. */ + r = dns_transaction_maybe_restart(t); + if (r < 0) + goto fail; + if (r > 0) /* Transaction got restarted... */ + return; + + /* When dealing with protocols other than mDNS only consider responses with equivalent query section + * to the request. For mDNS this check doesn't make sense, because the section 6 of RFC6762 states + * that "Multicast DNS responses MUST NOT contain any questions in the Question Section". */ + if (t->scope->protocol != DNS_PROTOCOL_MDNS) { + r = dns_packet_is_reply_for(p, dns_transaction_key(t)); + if (r < 0) + goto fail; + if (r == 0) { + dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY); + return; + } + } + + /* Install the answer as answer to the transaction. We ref the answer twice here: the main `answer` + * field is later replaced by the DNSSEC validated subset. The 'answer_auxiliary' field carries the + * original complete record set, including RRSIG and friends. We use this when passing data to + * clients that ask for DNSSEC metadata. */ + DNS_ANSWER_REPLACE(t->answer, dns_answer_ref(p->answer)); + t->answer_rcode = DNS_PACKET_RCODE(p); + t->answer_dnssec_result = _DNSSEC_RESULT_INVALID; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false); + SET_FLAG(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL, encrypted); + + r = dns_transaction_fix_rcode(t); + if (r < 0) + goto fail; + + /* Block GC while starting requests for additional DNSSEC RRs */ + t->block_gc++; + r = dns_transaction_request_dnssec_keys(t); + t->block_gc--; + + /* Maybe the transaction is ready for GC'ing now? If so, free it and return. */ + if (!dns_transaction_gc(t)) + return; + + /* Requesting additional keys might have resulted in this transaction to fail, since the auxiliary + * request failed for some reason. If so, we are not in pending state anymore, and we should exit + * quickly. */ + if (t->state != DNS_TRANSACTION_PENDING) + return; + if (r < 0) + goto fail; + if (r > 0) { + /* There are DNSSEC transactions pending now. Update the state accordingly. */ + t->state = DNS_TRANSACTION_VALIDATING; + dns_transaction_close_connection(t, true); + dns_transaction_stop_timeout(t); + return; + } + + dns_transaction_process_dnssec(t); + return; + +fail: + dns_transaction_complete_errno(t, r); +} + +static int on_dns_packet(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + DnsTransaction *t = ASSERT_PTR(userdata); + int r; + + assert(t->scope); + + r = manager_recv(t->scope->manager, fd, DNS_PROTOCOL_DNS, &p); + if (r < 0) { + if (ERRNO_IS_DISCONNECT(r)) { + usec_t usec; + + /* UDP connection failures get reported via ICMP and then are possibly delivered to us on the + * next recvmsg(). Treat this like a lost packet. */ + + log_debug_errno(r, "Connection failure for DNS UDP packet: %m"); + assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &usec) >= 0); + dns_server_packet_lost(t->server, IPPROTO_UDP, t->current_feature_level); + + dns_transaction_close_connection(t, /* use_graveyard = */ false); + + if (dns_transaction_limited_retry(t)) /* Try a different server */ + return 0; + } + dns_transaction_complete_errno(t, r); + return 0; + } + if (r == 0) + /* Spurious wakeup without any data */ + return 0; + + r = dns_packet_validate_reply(p); + if (r < 0) { + log_debug_errno(r, "Received invalid DNS packet as response, ignoring: %m"); + return 0; + } + if (r == 0) { + log_debug("Received inappropriate DNS packet as response, ignoring."); + return 0; + } + + if (DNS_PACKET_ID(p) != t->id) { + log_debug("Received packet with incorrect transaction ID, ignoring."); + return 0; + } + + dns_transaction_process_reply(t, p, false); + return 0; +} + +static int dns_transaction_emit_udp(DnsTransaction *t) { + int r; + + assert(t); + + if (t->scope->protocol == DNS_PROTOCOL_DNS) { + + r = dns_transaction_pick_server(t); + if (r < 0) + return r; + + if (manager_server_is_stub(t->scope->manager, t->server)) + return -ELOOP; + + if (t->current_feature_level < DNS_SERVER_FEATURE_LEVEL_UDP || DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level)) + return -EAGAIN; /* Sorry, can't do UDP, try TCP! */ + + if (!t->bypass && !dns_server_dnssec_supported(t->server) && dns_type_is_dnssec(dns_transaction_key(t)->type)) + return -EOPNOTSUPP; + + if (r > 0 || t->dns_udp_fd < 0) { /* Server changed, or no connection yet. */ + int fd; + + dns_transaction_close_connection(t, true); + + /* Before we allocate a new UDP socket, let's process the graveyard a bit to free some fds */ + manager_socket_graveyard_process(t->scope->manager); + + fd = dns_scope_socket_udp(t->scope, t->server); + if (fd < 0) + return fd; + + r = sd_event_add_io(t->scope->manager->event, &t->dns_udp_event_source, fd, EPOLLIN, on_dns_packet, t); + if (r < 0) { + safe_close(fd); + return r; + } + + (void) sd_event_source_set_description(t->dns_udp_event_source, "dns-transaction-udp"); + t->dns_udp_fd = fd; + } + + if (!t->bypass) { + r = dns_server_adjust_opt(t->server, t->sent, t->current_feature_level); + if (r < 0) + return r; + } + } else + dns_transaction_close_connection(t, true); + + r = dns_scope_emit_udp(t->scope, t->dns_udp_fd, t->server ? t->server->family : AF_UNSPEC, t->sent); + if (r < 0) + return r; + + dns_transaction_reset_answer(t); + + return 0; +} + +static int on_transaction_timeout(sd_event_source *s, usec_t usec, void *userdata) { + DnsTransaction *t = ASSERT_PTR(userdata); + + assert(s); + + if (t->initial_jitter_scheduled && !t->initial_jitter_elapsed) { + log_debug("Initial jitter phase for transaction %" PRIu16 " elapsed.", t->id); + t->initial_jitter_elapsed = true; + } else { + /* Timeout reached? Increase the timeout for the server used */ + switch (t->scope->protocol) { + + case DNS_PROTOCOL_DNS: + assert(t->server); + dns_server_packet_lost(t->server, t->stream ? IPPROTO_TCP : IPPROTO_UDP, t->current_feature_level); + break; + + case DNS_PROTOCOL_LLMNR: + case DNS_PROTOCOL_MDNS: + dns_scope_packet_lost(t->scope, usec - t->start_usec); + break; + + default: + assert_not_reached(); + } + + log_debug("Timeout reached on transaction %" PRIu16 ".", t->id); + } + + dns_transaction_retry(t, /* next_server= */ true); /* try a different server, but given this means + * packet loss, let's do so even if we already + * tried a bunch */ + return 0; +} + +static int dns_transaction_setup_timeout( + DnsTransaction *t, + usec_t timeout_usec /* relative */, + usec_t next_usec /* CLOCK_BOOTTIME */) { + + int r; + + assert(t); + + dns_transaction_stop_timeout(t); + + r = sd_event_add_time_relative( + t->scope->manager->event, + &t->timeout_event_source, + CLOCK_BOOTTIME, + timeout_usec, 0, + on_transaction_timeout, t); + if (r < 0) + return r; + + (void) sd_event_source_set_description(t->timeout_event_source, "dns-transaction-timeout"); + + t->next_attempt_after = next_usec; + t->state = DNS_TRANSACTION_PENDING; + return 0; +} + +static usec_t transaction_get_resend_timeout(DnsTransaction *t) { + assert(t); + assert(t->scope); + + switch (t->scope->protocol) { + + case DNS_PROTOCOL_DNS: + + /* When we do TCP, grant a much longer timeout, as in this case there's no need for us to quickly + * resend, as the kernel does that anyway for us, and we really don't want to interrupt it in that + * needlessly. */ + if (t->stream) + return TRANSACTION_TCP_TIMEOUT_USEC; + + return DNS_TIMEOUT_USEC; + + case DNS_PROTOCOL_MDNS: + if (t->probing) + return MDNS_PROBING_INTERVAL_USEC; + + /* See RFC 6762 Section 5.1 suggests that timeout should be a few seconds. */ + assert(t->n_attempts > 0); + return (1 << (t->n_attempts - 1)) * USEC_PER_SEC; + + case DNS_PROTOCOL_LLMNR: + return t->scope->resend_timeout; + + default: + assert_not_reached(); + } +} + +static void dns_transaction_randomize_answer(DnsTransaction *t) { + int r; + + assert(t); + + /* Randomizes the order of the answer array. This is done for all cached responses, so that we return + * a different order each time. We do this only for DNS traffic, in order to do some minimal, crappy + * load balancing. We don't do this for LLMNR or mDNS, since the order (preferring link-local + * addresses, and such like) might have meaning there, and load balancing is pointless. */ + + if (t->scope->protocol != DNS_PROTOCOL_DNS) + return; + + /* No point in randomizing, if there's just one RR */ + if (dns_answer_size(t->answer) <= 1) + return; + + r = dns_answer_reserve_or_clone(&t->answer, 0); + if (r < 0) /* If this fails, just don't randomize, this is non-essential stuff after all */ + return (void) log_debug_errno(r, "Failed to clone answer record, not randomizing RR order of answer: %m"); + + dns_answer_randomize(t->answer); +} + +static int dns_transaction_prepare(DnsTransaction *t, usec_t ts) { + int r; + + assert(t); + + /* Returns 0 if dns_transaction_complete() has been called. In that case the transaction and query + * candidate objects may have been invalidated and must not be accessed. Returns 1 if the transaction + * has been prepared. */ + + dns_transaction_stop_timeout(t); + + if (!dns_scope_network_good(t->scope)) { + dns_transaction_complete(t, DNS_TRANSACTION_NETWORK_DOWN); + return 0; + } + + if (t->n_attempts >= TRANSACTION_ATTEMPTS_MAX(t->scope->protocol)) { + DnsTransactionState result; + + if (t->scope->protocol == DNS_PROTOCOL_LLMNR) + /* If we didn't find anything on LLMNR, it's not an error, but a failure to resolve + * the name. */ + result = DNS_TRANSACTION_NOT_FOUND; + else + result = DNS_TRANSACTION_ATTEMPTS_MAX_REACHED; + + dns_transaction_complete(t, result); + return 0; + } + + if (t->scope->protocol == DNS_PROTOCOL_LLMNR && t->tried_stream) { + /* If we already tried via a stream, then we don't + * retry on LLMNR. See RFC 4795, Section 2.7. */ + dns_transaction_complete(t, DNS_TRANSACTION_ATTEMPTS_MAX_REACHED); + return 0; + } + + t->n_attempts++; + t->start_usec = ts; + + dns_transaction_reset_answer(t); + dns_transaction_flush_dnssec_transactions(t); + + /* Check the trust anchor. Do so only on classic DNS, since DNSSEC does not apply otherwise. */ + if (t->scope->protocol == DNS_PROTOCOL_DNS && + !FLAGS_SET(t->query_flags, SD_RESOLVED_NO_TRUST_ANCHOR)) { + r = dns_trust_anchor_lookup_positive(&t->scope->manager->trust_anchor, dns_transaction_key(t), &t->answer); + if (r < 0) + return r; + if (r > 0) { + t->answer_rcode = DNS_RCODE_SUCCESS; + t->answer_source = DNS_TRANSACTION_TRUST_ANCHOR; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL, true); + dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS); + return 0; + } + + if (dns_name_is_root(dns_resource_key_name(dns_transaction_key(t))) && + dns_transaction_key(t)->type == DNS_TYPE_DS) { + + /* Hmm, this is a request for the root DS? A DS RR doesn't exist in the root zone, + * and if our trust anchor didn't know it either, this means we cannot do any DNSSEC + * logic anymore. */ + + if (t->scope->dnssec_mode == DNSSEC_ALLOW_DOWNGRADE) { + /* We are in downgrade mode. In this case, synthesize an unsigned empty + * response, so that the any lookup depending on this one can continue + * assuming there was no DS, and hence the root zone was unsigned. */ + + t->answer_rcode = DNS_RCODE_SUCCESS; + t->answer_source = DNS_TRANSACTION_TRUST_ANCHOR; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false); + SET_FLAG(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL, true); + dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS); + } else + /* If we are not in downgrade mode, then fail the lookup, because we cannot + * reasonably answer it. There might be DS RRs, but we don't know them, and + * the DNS server won't tell them to us (and even if it would, we couldn't + * validate and trust them. */ + dns_transaction_complete(t, DNS_TRANSACTION_NO_TRUST_ANCHOR); + + return 0; + } + } + + /* Check the zone. */ + if (!FLAGS_SET(t->query_flags, SD_RESOLVED_NO_ZONE)) { + r = dns_zone_lookup(&t->scope->zone, dns_transaction_key(t), dns_scope_ifindex(t->scope), &t->answer, NULL, NULL); + if (r < 0) + return r; + if (r > 0) { + t->answer_rcode = DNS_RCODE_SUCCESS; + t->answer_source = DNS_TRANSACTION_ZONE; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL, true); + dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS); + return 0; + } + } + + /* Check the cache. */ + if (!FLAGS_SET(t->query_flags, SD_RESOLVED_NO_CACHE)) { + + /* Before trying the cache, let's make sure we figured out a server to use. Should this cause + * a change of server this might flush the cache. */ + (void) dns_scope_get_dns_server(t->scope); + + /* Let's then prune all outdated entries */ + dns_cache_prune(&t->scope->cache); + + r = dns_cache_lookup( + &t->scope->cache, + dns_transaction_key(t), + t->query_flags, + &t->answer_rcode, + &t->answer, + &t->received, + &t->answer_query_flags, + &t->answer_dnssec_result); + if (r < 0) + return r; + if (r > 0) { + dns_transaction_randomize_answer(t); + + if (t->bypass && t->scope->protocol == DNS_PROTOCOL_DNS && !t->received) + /* When bypass mode is on, do not use cached data unless it came with a full + * packet. */ + dns_transaction_reset_answer(t); + else { + t->answer_source = DNS_TRANSACTION_CACHE; + if (t->answer_rcode == DNS_RCODE_SUCCESS) + dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS); + else + dns_transaction_complete(t, DNS_TRANSACTION_RCODE_FAILURE); + return 0; + } + } + } + + if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_NETWORK)) { + dns_transaction_complete(t, DNS_TRANSACTION_NO_SOURCE); + return 0; + } + + return 1; +} + +static int dns_packet_append_zone(DnsPacket *p, DnsTransaction *t, DnsResourceKey *k, unsigned *nscount) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + bool tentative; + int r; + + assert(p); + assert(t); + assert(k); + + if (k->type != DNS_TYPE_ANY) + return 0; + + r = dns_zone_lookup(&t->scope->zone, k, t->scope->link->ifindex, &answer, NULL, &tentative); + if (r < 0) + return r; + + return dns_packet_append_answer(p, answer, nscount); +} + +static int mdns_make_dummy_packet(DnsTransaction *t, DnsPacket **ret_packet, Set **ret_keys) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + _cleanup_set_free_ Set *keys = NULL; + bool add_known_answers = false; + unsigned qdcount; + usec_t ts; + int r; + + assert(t); + assert(t->scope); + assert(t->scope->protocol == DNS_PROTOCOL_MDNS); + assert(ret_packet); + assert(ret_keys); + + r = dns_packet_new_query(&p, t->scope->protocol, 0, false); + if (r < 0) + return r; + + r = dns_packet_append_key(p, dns_transaction_key(t), 0, NULL); + if (r < 0) + return r; + + qdcount = 1; + + if (dns_key_is_shared(dns_transaction_key(t))) + add_known_answers = true; + + r = dns_packet_append_zone(p, t, dns_transaction_key(t), NULL); + if (r < 0) + return r; + + /* Save appended keys */ + r = set_ensure_put(&keys, &dns_resource_key_hash_ops, dns_transaction_key(t)); + if (r < 0) + return r; + + assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &ts) >= 0); + + LIST_FOREACH(transactions_by_scope, other, t->scope->transactions) { + + /* Skip ourselves */ + if (other == t) + continue; + + if (other->state != DNS_TRANSACTION_PENDING) + continue; + + if (other->next_attempt_after > ts) + continue; + + if (!set_contains(keys, dns_transaction_key(other))) { + size_t saved_packet_size; + + r = dns_packet_append_key(p, dns_transaction_key(other), 0, &saved_packet_size); + /* If we can't stuff more questions into the packet, just give up. + * One of the 'other' transactions will fire later and take care of the rest. */ + if (r == -EMSGSIZE) + break; + if (r < 0) + return r; + + r = dns_packet_append_zone(p, t, dns_transaction_key(other), NULL); + if (r == -EMSGSIZE) { + dns_packet_truncate(p, saved_packet_size); + break; + } + if (r < 0) + return r; + + r = set_ensure_put(&keys, &dns_resource_key_hash_ops, dns_transaction_key(other)); + if (r < 0) + return r; + } + + r = dns_transaction_prepare(other, ts); + if (r < 0) + return r; + if (r == 0) + /* In this case, not only this transaction, but multiple transactions may be + * freed. Hence, we need to restart the loop. */ + return -EAGAIN; + + usec_t timeout = transaction_get_resend_timeout(other); + r = dns_transaction_setup_timeout(other, timeout, usec_add(ts, timeout)); + if (r < 0) + return r; + + if (dns_key_is_shared(dns_transaction_key(other))) + add_known_answers = true; + + qdcount++; + if (qdcount >= UINT16_MAX) + break; + } + + DNS_PACKET_HEADER(p)->qdcount = htobe16(qdcount); + + /* Append known answers section if we're asking for any shared record */ + if (add_known_answers) { + r = dns_cache_export_shared_to_packet(&t->scope->cache, p, ts, 0); + if (r < 0) + return r; + } + + *ret_packet = TAKE_PTR(p); + *ret_keys = TAKE_PTR(keys); + return add_known_answers; +} + +static int dns_transaction_make_packet_mdns(DnsTransaction *t) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL, *dummy = NULL; + _cleanup_set_free_ Set *keys = NULL; + bool add_known_answers; + DnsResourceKey *k; + unsigned c; + int r; + + assert(t); + assert(t->scope->protocol == DNS_PROTOCOL_MDNS); + + /* Discard any previously prepared packet, so we can start over and coalesce again */ + t->sent = dns_packet_unref(t->sent); + + /* First, create a dummy packet to calculate the number of known answers to be appended in the first packet. */ + for (;;) { + r = mdns_make_dummy_packet(t, &dummy, &keys); + if (r == -EAGAIN) + continue; + if (r < 0) + return r; + + add_known_answers = r; + break; + } + + /* Then, create actual packet. */ + r = dns_packet_new_query(&p, t->scope->protocol, 0, false); + if (r < 0) + return r; + + /* Questions */ + c = 0; + SET_FOREACH(k, keys) { + r = dns_packet_append_key(p, k, 0, NULL); + if (r < 0) + return r; + c++; + } + DNS_PACKET_HEADER(p)->qdcount = htobe16(c); + + /* Known answers */ + if (add_known_answers) { + usec_t ts; + + assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &ts) >= 0); + + r = dns_cache_export_shared_to_packet(&t->scope->cache, p, ts, be16toh(DNS_PACKET_HEADER(dummy)->ancount)); + if (r < 0) + return r; + } + + /* Authorities */ + c = 0; + SET_FOREACH(k, keys) { + r = dns_packet_append_zone(p, t, k, &c); + if (r < 0) + return r; + } + DNS_PACKET_HEADER(p)->nscount = htobe16(c); + + t->sent = TAKE_PTR(p); + return 0; +} + +static int dns_transaction_make_packet(DnsTransaction *t) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + int r; + + assert(t); + + if (t->scope->protocol == DNS_PROTOCOL_MDNS) + return dns_transaction_make_packet_mdns(t); + + if (t->sent) + return 0; + + if (t->bypass && t->bypass->protocol == t->scope->protocol) { + /* If bypass logic is enabled and the protocol if the original packet and our scope match, + * take the original packet, copy it, and patch in our new ID */ + r = dns_packet_dup(&p, t->bypass); + if (r < 0) + return r; + } else { + r = dns_packet_new_query( + &p, t->scope->protocol, + /* min_alloc_dsize = */ 0, + /* dnssec_cd = */ !FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) && + t->scope->dnssec_mode != DNSSEC_NO); + if (r < 0) + return r; + + r = dns_packet_append_key(p, dns_transaction_key(t), 0, NULL); + if (r < 0) + return r; + + DNS_PACKET_HEADER(p)->qdcount = htobe16(1); + } + + DNS_PACKET_HEADER(p)->id = t->id; + + t->sent = TAKE_PTR(p); + return 0; +} + +int dns_transaction_go(DnsTransaction *t) { + usec_t ts; + int r; + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + assert(t); + + /* Returns > 0 if the transaction is now pending, returns 0 if could be processed immediately and has + * finished now. In the latter case, the transaction and query candidate objects must not be accessed. + */ + + assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &ts) >= 0); + + r = dns_transaction_prepare(t, ts); + if (r <= 0) + return r; + + log_debug("Firing %s transaction %" PRIu16 " for <%s> scope %s on %s/%s (validate=%s).", + t->bypass ? "bypass" : "regular", + t->id, + dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str), + dns_protocol_to_string(t->scope->protocol), + t->scope->link ? t->scope->link->ifname : "*", + af_to_name_short(t->scope->family), + yes_no(!FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE))); + + if (!t->initial_jitter_scheduled && + IN_SET(t->scope->protocol, DNS_PROTOCOL_LLMNR, DNS_PROTOCOL_MDNS)) { + usec_t jitter; + + /* RFC 4795 Section 2.7 suggests all LLMNR queries should be delayed by a random time from 0 to + * JITTER_INTERVAL. + * RFC 6762 Section 8.1 suggests initial probe queries should be delayed by a random time from + * 0 to 250ms. */ + + t->initial_jitter_scheduled = true; + t->n_attempts = 0; + + switch (t->scope->protocol) { + + case DNS_PROTOCOL_LLMNR: + jitter = random_u64_range(LLMNR_JITTER_INTERVAL_USEC); + break; + + case DNS_PROTOCOL_MDNS: + if (t->probing) + jitter = random_u64_range(MDNS_PROBING_INTERVAL_USEC); + else + jitter = 0; + break; + default: + assert_not_reached(); + } + + r = dns_transaction_setup_timeout(t, jitter, ts); + if (r < 0) + return r; + + log_debug("Delaying %s transaction %" PRIu16 " for " USEC_FMT "us.", + dns_protocol_to_string(t->scope->protocol), + t->id, + jitter); + return 1; + } + + /* Otherwise, we need to ask the network */ + r = dns_transaction_make_packet(t); + if (r < 0) + return r; + + if (t->scope->protocol == DNS_PROTOCOL_LLMNR && + (dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), "in-addr.arpa") > 0 || + dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), "ip6.arpa") > 0)) { + + /* RFC 4795, Section 2.4. says reverse lookups shall + * always be made via TCP on LLMNR */ + r = dns_transaction_emit_tcp(t); + } else { + /* Try via UDP, and if that fails due to large size or lack of + * support try via TCP */ + r = dns_transaction_emit_udp(t); + if (r == -EMSGSIZE) + log_debug("Sending query via TCP since it is too large."); + else if (r == -EAGAIN) + log_debug("Sending query via TCP since UDP isn't supported or DNS-over-TLS is selected."); + else if (r == -EPERM) + log_debug("Sending query via TCP since UDP is blocked."); + if (IN_SET(r, -EMSGSIZE, -EAGAIN, -EPERM)) + r = dns_transaction_emit_tcp(t); + } + if (r == -ELOOP) { + if (t->scope->protocol != DNS_PROTOCOL_DNS) + return r; + + /* One of our own stub listeners */ + log_debug_errno(r, "Detected that specified DNS server is our own extra listener, switching DNS servers."); + + dns_scope_next_dns_server(t->scope, t->server); + + if (dns_scope_get_dns_server(t->scope) == t->server) { + log_debug_errno(r, "Still pointing to extra listener after switching DNS servers, refusing operation."); + dns_transaction_complete(t, DNS_TRANSACTION_STUB_LOOP); + return 0; + } + + return dns_transaction_go(t); + } + if (r == -ESRCH) { + /* No servers to send this to? */ + dns_transaction_complete(t, DNS_TRANSACTION_NO_SERVERS); + return 0; + } + if (r == -EOPNOTSUPP) { + /* Tried to ask for DNSSEC RRs, on a server that doesn't do DNSSEC */ + dns_transaction_complete(t, DNS_TRANSACTION_RR_TYPE_UNSUPPORTED); + return 0; + } + if (t->scope->protocol == DNS_PROTOCOL_LLMNR && ERRNO_IS_DISCONNECT(r)) { + /* On LLMNR, if we cannot connect to a host via TCP when doing reverse lookups. This means we cannot + * answer this request with this protocol. */ + dns_transaction_complete(t, DNS_TRANSACTION_NOT_FOUND); + return 0; + } + if (r < 0) { + if (t->scope->protocol != DNS_PROTOCOL_DNS) + return r; + + /* Couldn't send? Try immediately again, with a new server */ + dns_scope_next_dns_server(t->scope, t->server); + + return dns_transaction_go(t); + } + + usec_t timeout = transaction_get_resend_timeout(t); + r = dns_transaction_setup_timeout(t, timeout, usec_add(ts, timeout)); + if (r < 0) + return r; + + return 1; +} + +static int dns_transaction_find_cyclic(DnsTransaction *t, DnsTransaction *aux) { + DnsTransaction *n; + int r; + + assert(t); + assert(aux); + + /* Try to find cyclic dependencies between transaction objects */ + + if (t == aux) + return 1; + + SET_FOREACH(n, aux->dnssec_transactions) { + r = dns_transaction_find_cyclic(t, n); + if (r != 0) + return r; + } + + return 0; +} + +static int dns_transaction_add_dnssec_transaction(DnsTransaction *t, DnsResourceKey *key, DnsTransaction **ret) { + _cleanup_(dns_transaction_gcp) DnsTransaction *aux = NULL; + int r; + + assert(t); + assert(ret); + assert(key); + + aux = dns_scope_find_transaction(t->scope, key, t->query_flags); + if (!aux) { + r = dns_transaction_new(&aux, t->scope, key, NULL, t->query_flags); + if (r < 0) + return r; + } else { + if (set_contains(t->dnssec_transactions, aux)) { + *ret = aux; + return 0; + } + + r = dns_transaction_find_cyclic(t, aux); + if (r < 0) + return r; + if (r > 0) { + char s[DNS_RESOURCE_KEY_STRING_MAX], saux[DNS_RESOURCE_KEY_STRING_MAX]; + + return log_debug_errno(SYNTHETIC_ERRNO(ELOOP), + "Potential cyclic dependency, refusing to add transaction %" PRIu16 " (%s) as dependency for %" PRIu16 " (%s).", + aux->id, + dns_resource_key_to_string(dns_transaction_key(t), s, sizeof s), + t->id, + dns_resource_key_to_string(dns_transaction_key(aux), saux, sizeof saux)); + } + } + + r = set_ensure_allocated(&aux->notify_transactions_done, NULL); + if (r < 0) + return r; + + r = set_ensure_put(&t->dnssec_transactions, NULL, aux); + if (r < 0) + return r; + + r = set_ensure_put(&aux->notify_transactions, NULL, t); + if (r < 0) { + (void) set_remove(t->dnssec_transactions, aux); + return r; + } + + *ret = TAKE_PTR(aux); + return 1; +} + +static int dns_transaction_request_dnssec_rr(DnsTransaction *t, DnsResourceKey *key) { + _cleanup_(dns_answer_unrefp) DnsAnswer *a = NULL; + DnsTransaction *aux; + int r; + + assert(t); + assert(key); + + /* Try to get the data from the trust anchor */ + r = dns_trust_anchor_lookup_positive(&t->scope->manager->trust_anchor, key, &a); + if (r < 0) + return r; + if (r > 0) { + r = dns_answer_extend(&t->validated_keys, a); + if (r < 0) + return r; + + return 0; + } + + /* This didn't work, ask for it via the network/cache then. */ + r = dns_transaction_add_dnssec_transaction(t, key, &aux); + if (r == -ELOOP) /* This would result in a cyclic dependency */ + return 0; + if (r < 0) + return r; + + if (aux->state == DNS_TRANSACTION_NULL) { + r = dns_transaction_go(aux); + if (r < 0) + return r; + } + + return 1; +} + +static int dns_transaction_negative_trust_anchor_lookup(DnsTransaction *t, const char *name) { + int r; + + assert(t); + + /* Check whether the specified name is in the NTA + * database, either in the global one, or the link-local + * one. */ + + r = dns_trust_anchor_lookup_negative(&t->scope->manager->trust_anchor, name); + if (r != 0) + return r; + + if (!t->scope->link) + return 0; + + return link_negative_trust_anchor_lookup(t->scope->link, name); +} + +static int dns_transaction_has_negative_answer(DnsTransaction *t) { + int r; + + assert(t); + + /* Checks whether the answer is negative, and lacks NSEC/NSEC3 + * RRs to prove it */ + + r = dns_transaction_has_positive_answer(t, NULL); + if (r < 0) + return r; + if (r > 0) + return false; + + /* Is this key explicitly listed as a negative trust anchor? + * If so, it's nothing we need to care about */ + r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(dns_transaction_key(t))); + if (r < 0) + return r; + return !r; +} + +static int dns_transaction_is_primary_response(DnsTransaction *t, DnsResourceRecord *rr) { + int r; + + assert(t); + assert(rr); + + /* Check if the specified RR is the "primary" response, + * i.e. either matches the question precisely or is a + * CNAME/DNAME for it. */ + + r = dns_resource_key_match_rr(dns_transaction_key(t), rr, NULL); + if (r != 0) + return r; + + return dns_resource_key_match_cname_or_dname(dns_transaction_key(t), rr->key, NULL); +} + +static bool dns_transaction_dnssec_supported(DnsTransaction *t) { + assert(t); + + /* Checks whether our transaction's DNS server is assumed to be compatible with DNSSEC. Returns false as soon + * as we changed our mind about a server, and now believe it is incompatible with DNSSEC. */ + + if (t->scope->protocol != DNS_PROTOCOL_DNS) + return false; + + /* If we have picked no server, then we are working from the cache or some other source, and DNSSEC might well + * be supported, hence return true. */ + if (!t->server) + return true; + + /* Note that we do not check the feature level actually used for the transaction but instead the feature level + * the server is known to support currently, as the transaction feature level might be lower than what the + * server actually supports, since we might have downgraded this transaction's feature level because we got a + * SERVFAIL earlier and wanted to check whether downgrading fixes it. */ + + return dns_server_dnssec_supported(t->server); +} + +static bool dns_transaction_dnssec_supported_full(DnsTransaction *t) { + DnsTransaction *dt; + + assert(t); + + /* Checks whether our transaction our any of the auxiliary transactions couldn't do DNSSEC. */ + + if (!dns_transaction_dnssec_supported(t)) + return false; + + SET_FOREACH(dt, t->dnssec_transactions) + if (!dns_transaction_dnssec_supported(dt)) + return false; + + return true; +} + +int dns_transaction_request_dnssec_keys(DnsTransaction *t) { + DnsResourceRecord *rr; + + int r; + + assert(t); + + /* + * Retrieve all auxiliary RRs for the answer we got, so that + * we can verify signatures or prove that RRs are rightfully + * unsigned. Specifically: + * + * - For RRSIG we get the matching DNSKEY + * - For DNSKEY we get the matching DS + * - For unsigned SOA/NS we get the matching DS + * - For unsigned CNAME/DNAME/DS we get the parent SOA RR + * - For other unsigned RRs we get the matching SOA RR + * - For SOA/NS queries with no matching response RR, and no NSEC/NSEC3, the DS RR + * - For DS queries with no matching response RRs, and no NSEC/NSEC3, the parent's SOA RR + * - For other queries with no matching response RRs, and no NSEC/NSEC3, the SOA RR + */ + + if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) || t->scope->dnssec_mode == DNSSEC_NO) + return 0; + if (t->answer_source != DNS_TRANSACTION_NETWORK) + return 0; /* We only need to validate stuff from the network */ + if (!dns_transaction_dnssec_supported(t)) + return 0; /* If we can't do DNSSEC anyway there's no point in getting the auxiliary RRs */ + + DNS_ANSWER_FOREACH(rr, t->answer) { + + if (dns_type_is_pseudo(rr->key->type)) + continue; + + /* If this RR is in the negative trust anchor, we don't need to validate it. */ + r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r > 0) + continue; + + switch (rr->key->type) { + + case DNS_TYPE_RRSIG: { + /* For each RRSIG we request the matching DNSKEY */ + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *dnskey = NULL; + + /* If this RRSIG is about a DNSKEY RR and the + * signer is the same as the owner, then we + * already have the DNSKEY, and we don't have + * to look for more. */ + if (rr->rrsig.type_covered == DNS_TYPE_DNSKEY) { + r = dns_name_equal(rr->rrsig.signer, dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r > 0) + continue; + } + + /* If the signer is not a parent of our + * original query, then this is about an + * auxiliary RRset, but not anything we asked + * for. In this case we aren't interested, + * because we don't want to request additional + * RRs for stuff we didn't really ask for, and + * also to avoid request loops, where + * additional RRs from one transaction result + * in another transaction whose additional RRs + * point back to the original transaction, and + * we deadlock. */ + r = dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), rr->rrsig.signer); + if (r < 0) + return r; + if (r == 0) + continue; + + dnskey = dns_resource_key_new(rr->key->class, DNS_TYPE_DNSKEY, rr->rrsig.signer); + if (!dnskey) + return -ENOMEM; + + log_debug("Requesting DNSKEY to validate transaction %" PRIu16" (%s, RRSIG with key tag: %" PRIu16 ").", + t->id, dns_resource_key_name(rr->key), rr->rrsig.key_tag); + r = dns_transaction_request_dnssec_rr(t, dnskey); + if (r < 0) + return r; + break; + } + + case DNS_TYPE_DNSKEY: { + /* For each DNSKEY we request the matching DS */ + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *ds = NULL; + + /* If the DNSKEY we are looking at is not for + * zone we are interested in, nor any of its + * parents, we aren't interested, and don't + * request it. After all, we don't want to end + * up in request loops, and want to keep + * additional traffic down. */ + + r = dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r == 0) + continue; + + ds = dns_resource_key_new(rr->key->class, DNS_TYPE_DS, dns_resource_key_name(rr->key)); + if (!ds) + return -ENOMEM; + + log_debug("Requesting DS to validate transaction %" PRIu16" (%s, DNSKEY with key tag: %" PRIu16 ").", + t->id, dns_resource_key_name(rr->key), dnssec_keytag(rr, false)); + r = dns_transaction_request_dnssec_rr(t, ds); + if (r < 0) + return r; + + break; + } + + case DNS_TYPE_SOA: + case DNS_TYPE_NS: { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *ds = NULL; + + /* For an unsigned SOA or NS, try to acquire + * the matching DS RR, as we are at a zone cut + * then, and whether a DS exists tells us + * whether the zone is signed. Do so only if + * this RR matches our original question, + * however. */ + + r = dns_resource_key_match_rr(dns_transaction_key(t), rr, NULL); + if (r < 0) + return r; + if (r == 0) { + /* Hmm, so this SOA RR doesn't match our original question. In this case, maybe this is + * a negative reply, and we need the SOA RR's TTL in order to cache a negative entry? + * If so, we need to validate it, too. */ + + r = dns_answer_match_key(t->answer, dns_transaction_key(t), NULL); + if (r < 0) + return r; + if (r > 0) /* positive reply, we won't need the SOA and hence don't need to validate + * it. */ + continue; + + /* Only bother with this if the SOA/NS RR we are looking at is actually a parent of + * what we are looking for, otherwise there's no value in it for us. */ + r = dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r == 0) + continue; + } + + r = dnssec_has_rrsig(t->answer, rr->key); + if (r < 0) + return r; + if (r > 0) + continue; + + ds = dns_resource_key_new(rr->key->class, DNS_TYPE_DS, dns_resource_key_name(rr->key)); + if (!ds) + return -ENOMEM; + + log_debug("Requesting DS to validate transaction %" PRIu16 " (%s, unsigned SOA/NS RRset).", + t->id, dns_resource_key_name(rr->key)); + r = dns_transaction_request_dnssec_rr(t, ds); + if (r < 0) + return r; + + break; + } + + case DNS_TYPE_DS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *soa = NULL; + const char *name; + + /* CNAMEs and DNAMEs cannot be located at a + * zone apex, hence ask for the parent SOA for + * unsigned CNAME/DNAME RRs, maybe that's the + * apex. But do all that only if this is + * actually a response to our original + * question. + * + * Similar for DS RRs, which are signed when + * the parent SOA is signed. */ + + r = dns_transaction_is_primary_response(t, rr); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dnssec_has_rrsig(t->answer, rr->key); + if (r < 0) + return r; + if (r > 0) + continue; + + r = dns_answer_has_dname_for_cname(t->answer, rr); + if (r < 0) + return r; + if (r > 0) + continue; + + name = dns_resource_key_name(rr->key); + r = dns_name_parent(&name); + if (r < 0) + return r; + if (r == 0) + continue; + + soa = dns_resource_key_new(rr->key->class, DNS_TYPE_SOA, name); + if (!soa) + return -ENOMEM; + + log_debug("Requesting parent SOA to validate transaction %" PRIu16 " (%s, unsigned CNAME/DNAME/DS RRset).", + t->id, dns_resource_key_name(rr->key)); + r = dns_transaction_request_dnssec_rr(t, soa); + if (r < 0) + return r; + + break; + } + + default: { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *soa = NULL; + + /* For other unsigned RRsets (including + * NSEC/NSEC3!), look for proof the zone is + * unsigned, by requesting the SOA RR of the + * zone. However, do so only if they are + * directly relevant to our original + * question. */ + + r = dns_transaction_is_primary_response(t, rr); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dnssec_has_rrsig(t->answer, rr->key); + if (r < 0) + return r; + if (r > 0) + continue; + + soa = dns_resource_key_new(rr->key->class, DNS_TYPE_SOA, dns_resource_key_name(rr->key)); + if (!soa) + return -ENOMEM; + + log_debug("Requesting SOA to validate transaction %" PRIu16 " (%s, unsigned non-SOA/NS RRset <%s>).", + t->id, dns_resource_key_name(rr->key), dns_resource_record_to_string(rr)); + r = dns_transaction_request_dnssec_rr(t, soa); + if (r < 0) + return r; + break; + }} + } + + /* Above, we requested everything necessary to validate what + * we got. Now, let's request what we need to validate what we + * didn't get... */ + + r = dns_transaction_has_negative_answer(t); + if (r < 0) + return r; + if (r > 0) { + const char *name, *signed_status; + uint16_t type = 0; + + name = dns_resource_key_name(dns_transaction_key(t)); + signed_status = dns_answer_contains_nsec_or_nsec3(t->answer) ? "signed" : "unsigned"; + + /* If this was a SOA or NS request, then check if there's a DS RR for the same domain. Note that this + * could also be used as indication that we are not at a zone apex, but in real world setups there are + * too many broken DNS servers (Hello, incapdns.net!) where non-terminal zones return NXDOMAIN even + * though they have further children. If this was a DS request, then it's signed when the parent zone + * is signed, hence ask the parent SOA in that case. If this was any other RR then ask for the SOA RR, + * to see if that is signed. */ + + if (dns_transaction_key(t)->type == DNS_TYPE_DS) { + r = dns_name_parent(&name); + if (r > 0) { + type = DNS_TYPE_SOA; + log_debug("Requesting parent SOA (%s %s) to validate transaction %" PRIu16 " (%s, %s empty DS response).", + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), name, t->id, + dns_resource_key_name(dns_transaction_key(t)), signed_status); + } else + name = NULL; + + } else if (IN_SET(dns_transaction_key(t)->type, DNS_TYPE_SOA, DNS_TYPE_NS)) { + + type = DNS_TYPE_DS; + log_debug("Requesting DS (%s %s) to validate transaction %" PRIu16 " (%s, %s empty SOA/NS response).", + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), name, t->id, name, signed_status); + + } else { + type = DNS_TYPE_SOA; + log_debug("Requesting SOA (%s %s) to validate transaction %" PRIu16 " (%s, %s empty non-SOA/NS/DS response).", + special_glyph(SPECIAL_GLYPH_ARROW_RIGHT), name, t->id, name, signed_status); + } + + if (name) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *soa = NULL; + + soa = dns_resource_key_new(dns_transaction_key(t)->class, type, name); + if (!soa) + return -ENOMEM; + + r = dns_transaction_request_dnssec_rr(t, soa); + if (r < 0) + return r; + } + } + + return dns_transaction_dnssec_is_live(t); +} + +void dns_transaction_notify(DnsTransaction *t, DnsTransaction *source) { + assert(t); + assert(source); + + /* Invoked whenever any of our auxiliary DNSSEC transactions completed its work. If the state is still PENDING, + we are still in the loop that adds further DNSSEC transactions, hence don't check if we are ready yet. If + the state is VALIDATING however, we should check if we are complete now. */ + + if (t->state == DNS_TRANSACTION_VALIDATING) + dns_transaction_process_dnssec(t); +} + +static int dns_transaction_validate_dnskey_by_ds(DnsTransaction *t) { + DnsAnswerItem *item; + int r; + + assert(t); + + /* Add all DNSKEY RRs from the answer that are validated by DS + * RRs from the list of validated keys to the list of + * validated keys. */ + + DNS_ANSWER_FOREACH_ITEM(item, t->answer) { + + r = dnssec_verify_dnskey_by_ds_search(item->rr, t->validated_keys); + if (r < 0) + return r; + if (r == 0) + continue; + + /* If so, the DNSKEY is validated too. */ + r = dns_answer_add_extend(&t->validated_keys, item->rr, item->ifindex, item->flags|DNS_ANSWER_AUTHENTICATED, item->rrsig); + if (r < 0) + return r; + } + + return 0; +} + +static int dns_transaction_requires_rrsig(DnsTransaction *t, DnsResourceRecord *rr) { + int r; + + assert(t); + assert(rr); + + /* Checks if the RR we are looking for must be signed with an + * RRSIG. This is used for positive responses. */ + + if (t->scope->dnssec_mode == DNSSEC_NO) + return false; + + if (dns_type_is_pseudo(rr->key->type)) + return -EINVAL; + + r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r > 0) + return false; + + switch (rr->key->type) { + + case DNS_TYPE_RRSIG: + /* RRSIGs are the signatures themselves, they need no signing. */ + return false; + + case DNS_TYPE_SOA: + case DNS_TYPE_NS: { + DnsTransaction *dt; + + /* For SOA or NS RRs we look for a matching DS transaction */ + + SET_FOREACH(dt, t->dnssec_transactions) { + + if (dns_transaction_key(dt)->class != rr->key->class) + continue; + if (dns_transaction_key(dt)->type != DNS_TYPE_DS) + continue; + + r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r == 0) + continue; + + /* We found a DS transactions for the SOA/NS + * RRs we are looking at. If it discovered signed DS + * RRs, then we need to be signed, too. */ + + if (!FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) + return false; + + return dns_answer_match_key(dt->answer, dns_transaction_key(dt), NULL); + } + + /* We found nothing that proves this is safe to leave + * this unauthenticated, hence ask inist on + * authentication. */ + return true; + } + + case DNS_TYPE_DS: + case DNS_TYPE_CNAME: + case DNS_TYPE_DNAME: { + const char *parent = NULL; + DnsTransaction *dt; + + /* + * CNAME/DNAME RRs cannot be located at a zone apex, hence look directly for the parent SOA. + * + * DS RRs are signed if the parent is signed, hence also look at the parent SOA + */ + + SET_FOREACH(dt, t->dnssec_transactions) { + + if (dns_transaction_key(dt)->class != rr->key->class) + continue; + if (dns_transaction_key(dt)->type != DNS_TYPE_SOA) + continue; + + if (!parent) { + parent = dns_resource_key_name(rr->key); + r = dns_name_parent(&parent); + if (r < 0) + return r; + if (r == 0) { + if (rr->key->type == DNS_TYPE_DS) + return true; + + /* A CNAME/DNAME without a parent? That's sooo weird. */ + return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), + "Transaction %" PRIu16 " claims CNAME/DNAME at root. Refusing.", t->id); + } + } + + r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), parent); + if (r < 0) + return r; + if (r == 0) + continue; + + return FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED); + } + + return true; + } + + default: { + DnsTransaction *dt; + + /* Any other kind of RR (including DNSKEY/NSEC/NSEC3). Let's see if our SOA lookup was authenticated */ + + SET_FOREACH(dt, t->dnssec_transactions) { + + if (dns_transaction_key(dt)->class != rr->key->class) + continue; + if (dns_transaction_key(dt)->type != DNS_TYPE_SOA) + continue; + + r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r == 0) + continue; + + /* We found the transaction that was supposed to find the SOA RR for us. It was + * successful, but found no RR for us. This means we are not at a zone cut. In this + * case, we require authentication if the SOA lookup was authenticated too. */ + return FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED); + } + + return true; + }} +} + +static int dns_transaction_in_private_tld(DnsTransaction *t, const DnsResourceKey *key) { + DnsTransaction *dt; + const char *tld; + int r; + + /* If DNSSEC downgrade mode is on, checks whether the + * specified RR is one level below a TLD we have proven not to + * exist. In such a case we assume that this is a private + * domain, and permit it. + * + * This detects cases like the Fritz!Box router networks. Each + * Fritz!Box router serves a private "fritz.box" zone, in the + * non-existing TLD "box". Requests for the "fritz.box" domain + * are served by the router itself, while requests for the + * "box" domain will result in NXDOMAIN. + * + * Note that this logic is unable to detect cases where a + * router serves a private DNS zone directly under + * non-existing TLD. In such a case we cannot detect whether + * the TLD is supposed to exist or not, as all requests we + * make for it will be answered by the router's zone, and not + * by the root zone. */ + + assert(t); + + if (t->scope->dnssec_mode != DNSSEC_ALLOW_DOWNGRADE) + return false; /* In strict DNSSEC mode what doesn't exist, doesn't exist */ + + tld = dns_resource_key_name(key); + r = dns_name_parent(&tld); + if (r < 0) + return r; + if (r == 0) + return false; /* Already the root domain */ + + if (!dns_name_is_single_label(tld)) + return false; + + SET_FOREACH(dt, t->dnssec_transactions) { + + if (dns_transaction_key(dt)->class != key->class) + continue; + + r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), tld); + if (r < 0) + return r; + if (r == 0) + continue; + + /* We found an auxiliary lookup we did for the TLD. If + * that returned with NXDOMAIN, we know the TLD didn't + * exist, and hence this might be a private zone. */ + + return dt->answer_rcode == DNS_RCODE_NXDOMAIN; + } + + return false; +} + +static int dns_transaction_requires_nsec(DnsTransaction *t) { + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + DnsTransaction *dt; + const char *name; + uint16_t type = 0; + int r; + + assert(t); + + /* Checks if we need to insist on NSEC/NSEC3 RRs for proving + * this negative reply */ + + if (t->scope->dnssec_mode == DNSSEC_NO) + return false; + + if (dns_type_is_pseudo(dns_transaction_key(t)->type)) + return -EINVAL; + + r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(dns_transaction_key(t))); + if (r < 0) + return r; + if (r > 0) + return false; + + r = dns_transaction_in_private_tld(t, dns_transaction_key(t)); + if (r < 0) + return r; + if (r > 0) { + /* The lookup is from a TLD that is proven not to + * exist, and we are in downgrade mode, hence ignore + * that fact that we didn't get any NSEC RRs. */ + + log_info("Detected a negative query %s in a private DNS zone, permitting unsigned response.", + dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str)); + return false; + } + + name = dns_resource_key_name(dns_transaction_key(t)); + + if (dns_transaction_key(t)->type == DNS_TYPE_DS) { + + /* We got a negative reply for this DS lookup? DS RRs are signed when their parent zone is signed, + * hence check the parent SOA in this case. */ + + r = dns_name_parent(&name); + if (r < 0) + return r; + if (r == 0) + return true; + + type = DNS_TYPE_SOA; + + } else if (IN_SET(dns_transaction_key(t)->type, DNS_TYPE_SOA, DNS_TYPE_NS)) + /* We got a negative reply for this SOA/NS lookup? If so, check if there's a DS RR for this */ + type = DNS_TYPE_DS; + else + /* For all other negative replies, check for the SOA lookup */ + type = DNS_TYPE_SOA; + + /* For all other RRs we check the SOA on the same level to see + * if it's signed. */ + + SET_FOREACH(dt, t->dnssec_transactions) { + + if (dns_transaction_key(dt)->class != dns_transaction_key(t)->class) + continue; + if (dns_transaction_key(dt)->type != type) + continue; + + r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), name); + if (r < 0) + return r; + if (r == 0) + continue; + + return FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED); + } + + /* If in doubt, require NSEC/NSEC3 */ + return true; +} + +static int dns_transaction_dnskey_authenticated(DnsTransaction *t, DnsResourceRecord *rr) { + DnsResourceRecord *rrsig; + bool found = false; + int r; + + /* Checks whether any of the DNSKEYs used for the RRSIGs for + * the specified RRset is authenticated (i.e. has a matching + * DS RR). */ + + r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(rr->key)); + if (r < 0) + return r; + if (r > 0) + return false; + + DNS_ANSWER_FOREACH(rrsig, t->answer) { + DnsTransaction *dt; + + r = dnssec_key_match_rrsig(rr->key, rrsig); + if (r < 0) + return r; + if (r == 0) + continue; + + SET_FOREACH(dt, t->dnssec_transactions) { + + if (dns_transaction_key(dt)->class != rr->key->class) + continue; + + if (dns_transaction_key(dt)->type == DNS_TYPE_DNSKEY) { + + r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), rrsig->rrsig.signer); + if (r < 0) + return r; + if (r == 0) + continue; + + /* OK, we found an auxiliary DNSKEY lookup. If that lookup is authenticated, + * report this. */ + + if (FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) + return true; + + found = true; + + } else if (dns_transaction_key(dt)->type == DNS_TYPE_DS) { + + r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), rrsig->rrsig.signer); + if (r < 0) + return r; + if (r == 0) + continue; + + /* OK, we found an auxiliary DS lookup. If that lookup is authenticated and + * non-zero, we won! */ + + if (!FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) + return false; + + return dns_answer_match_key(dt->answer, dns_transaction_key(dt), NULL); + } + } + } + + return found ? false : -ENXIO; +} + +static int dns_transaction_known_signed(DnsTransaction *t, DnsResourceRecord *rr) { + assert(t); + assert(rr); + + /* We know that the root domain is signed, hence if it appears + * not to be signed, there's a problem with the DNS server */ + + return rr->key->class == DNS_CLASS_IN && + dns_name_is_root(dns_resource_key_name(rr->key)); +} + +static int dns_transaction_check_revoked_trust_anchors(DnsTransaction *t) { + DnsResourceRecord *rr; + int r; + + assert(t); + + /* Maybe warn the user that we encountered a revoked DNSKEY + * for a key from our trust anchor. Note that we don't care + * whether the DNSKEY can be authenticated or not. It's + * sufficient if it is self-signed. */ + + DNS_ANSWER_FOREACH(rr, t->answer) { + r = dns_trust_anchor_check_revoked(&t->scope->manager->trust_anchor, rr, t->answer); + if (r < 0) + return r; + } + + return 0; +} + +static int dns_transaction_invalidate_revoked_keys(DnsTransaction *t) { + bool changed; + int r; + + assert(t); + + /* Removes all DNSKEY/DS objects from t->validated_keys that + * our trust anchors database considers revoked. */ + + do { + DnsResourceRecord *rr; + + changed = false; + + DNS_ANSWER_FOREACH(rr, t->validated_keys) { + r = dns_trust_anchor_is_revoked(&t->scope->manager->trust_anchor, rr); + if (r < 0) + return r; + if (r > 0) { + r = dns_answer_remove_by_rr(&t->validated_keys, rr); + if (r < 0) + return r; + + assert(r > 0); + changed = true; + break; + } + } + } while (changed); + + return 0; +} + +static int dns_transaction_copy_validated(DnsTransaction *t) { + DnsTransaction *dt; + int r; + + assert(t); + + /* Copy all validated RRs from the auxiliary DNSSEC transactions into our set of validated RRs */ + + SET_FOREACH(dt, t->dnssec_transactions) { + + if (DNS_TRANSACTION_IS_LIVE(dt->state)) + continue; + + if (!FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) + continue; + + r = dns_answer_extend(&t->validated_keys, dt->answer); + if (r < 0) + return r; + } + + return 0; +} + +typedef enum { + DNSSEC_PHASE_DNSKEY, /* Phase #1, only validate DNSKEYs */ + DNSSEC_PHASE_NSEC, /* Phase #2, only validate NSEC+NSEC3 */ + DNSSEC_PHASE_ALL, /* Phase #3, validate everything else */ +} Phase; + +static int dnssec_validate_records( + DnsTransaction *t, + Phase phase, + bool *have_nsec, + DnsAnswer **validated) { + + DnsResourceRecord *rr; + int r; + + /* Returns negative on error, 0 if validation failed, 1 to restart validation, 2 when finished. */ + + DNS_ANSWER_FOREACH(rr, t->answer) { + _unused_ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr_ref = dns_resource_record_ref(rr); + DnsResourceRecord *rrsig = NULL; + DnssecResult result; + + switch (rr->key->type) { + case DNS_TYPE_RRSIG: + continue; + + case DNS_TYPE_DNSKEY: + /* We validate DNSKEYs only in the DNSKEY and ALL phases */ + if (phase == DNSSEC_PHASE_NSEC) + continue; + break; + + case DNS_TYPE_NSEC: + case DNS_TYPE_NSEC3: + *have_nsec = true; + + /* We validate NSEC/NSEC3 only in the NSEC and ALL phases */ + if (phase == DNSSEC_PHASE_DNSKEY) + continue; + break; + + default: + /* We validate all other RRs only in the ALL phases */ + if (phase != DNSSEC_PHASE_ALL) + continue; + } + + r = dnssec_verify_rrset_search( + t->answer, + rr->key, + t->validated_keys, + USEC_INFINITY, + &result, + &rrsig); + if (r < 0) + return r; + + log_debug("Looking at %s: %s", strna(dns_resource_record_to_string(rr)), dnssec_result_to_string(result)); + + if (result == DNSSEC_VALIDATED) { + assert(rrsig); + + if (rr->key->type == DNS_TYPE_DNSKEY) { + /* If we just validated a DNSKEY RRset, then let's add these keys to + * the set of validated keys for this transaction. */ + + r = dns_answer_copy_by_key(&t->validated_keys, t->answer, rr->key, DNS_ANSWER_AUTHENTICATED, rrsig); + if (r < 0) + return r; + + /* Some of the DNSKEYs we just added might already have been revoked, + * remove them again in that case. */ + r = dns_transaction_invalidate_revoked_keys(t); + if (r < 0) + return r; + } + + /* Add the validated RRset to the new list of validated RRsets, and remove it from + * the unvalidated RRsets. We mark the RRset as authenticated and cacheable. */ + r = dns_answer_move_by_key(validated, &t->answer, rr->key, DNS_ANSWER_AUTHENTICATED|DNS_ANSWER_CACHEABLE, rrsig); + if (r < 0) + return r; + + manager_dnssec_verdict(t->scope->manager, DNSSEC_SECURE, rr->key); + + /* Exit the loop, we dropped something from the answer, start from the beginning */ + return 1; + } + + /* If we haven't read all DNSKEYs yet a negative result of the validation is irrelevant, as + * there might be more DNSKEYs coming. Similar, if we haven't read all NSEC/NSEC3 RRs yet, + * we cannot do positive wildcard proofs yet, as those require the NSEC/NSEC3 RRs. */ + if (phase != DNSSEC_PHASE_ALL) + continue; + + if (result == DNSSEC_VALIDATED_WILDCARD) { + bool authenticated = false; + const char *source; + + assert(rrsig); + + /* This RRset validated, but as a wildcard. This means we need + * to prove via NSEC/NSEC3 that no matching non-wildcard RR exists. */ + + /* First step, determine the source of synthesis */ + r = dns_resource_record_source(rrsig, &source); + if (r < 0) + return r; + + r = dnssec_test_positive_wildcard(*validated, + dns_resource_key_name(rr->key), + source, + rrsig->rrsig.signer, + &authenticated); + + /* Unless the NSEC proof showed that the key really doesn't exist something is off. */ + if (r == 0) + result = DNSSEC_INVALID; + else { + r = dns_answer_move_by_key( + validated, + &t->answer, + rr->key, + authenticated ? (DNS_ANSWER_AUTHENTICATED|DNS_ANSWER_CACHEABLE) : 0, + rrsig); + if (r < 0) + return r; + + manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, rr->key); + + /* Exit the loop, we dropped something from the answer, start from the beginning */ + return 1; + } + } + + if (result == DNSSEC_NO_SIGNATURE) { + r = dns_transaction_requires_rrsig(t, rr); + if (r < 0) + return r; + if (r == 0) { + /* Data does not require signing. In that case, just copy it over, + * but remember that this is by no means authenticated. */ + r = dns_answer_move_by_key( + validated, + &t->answer, + rr->key, + 0, + NULL); + if (r < 0) + return r; + + manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key); + return 1; + } + + r = dns_transaction_known_signed(t, rr); + if (r < 0) + return r; + if (r > 0) { + /* This is an RR we know has to be signed. If it isn't this means + * the server is not attaching RRSIGs, hence complain. */ + + dns_server_packet_rrsig_missing(t->server, t->current_feature_level); + + if (t->scope->dnssec_mode == DNSSEC_ALLOW_DOWNGRADE) { + + /* Downgrading is OK? If so, just consider the information unsigned */ + + r = dns_answer_move_by_key(validated, &t->answer, rr->key, 0, NULL); + if (r < 0) + return r; + + manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key); + return 1; + } + + /* Otherwise, fail */ + t->answer_dnssec_result = DNSSEC_INCOMPATIBLE_SERVER; + return 0; + } + + r = dns_transaction_in_private_tld(t, rr->key); + if (r < 0) + return r; + if (r > 0) { + char s[DNS_RESOURCE_KEY_STRING_MAX]; + + /* The data is from a TLD that is proven not to exist, and we are in downgrade + * mode, hence ignore the fact that this was not signed. */ + + log_info("Detected RRset %s is in a private DNS zone, permitting unsigned RRs.", + dns_resource_key_to_string(rr->key, s, sizeof s)); + + r = dns_answer_move_by_key(validated, &t->answer, rr->key, 0, NULL); + if (r < 0) + return r; + + manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key); + return 1; + } + } + + /* https://datatracker.ietf.org/doc/html/rfc6840#section-5.2 */ + if (result == DNSSEC_UNSUPPORTED_ALGORITHM) { + r = dns_answer_move_by_key(validated, &t->answer, rr->key, 0, NULL); + if (r < 0) + return r; + + manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key); + return 1; + } + + if (IN_SET(result, + DNSSEC_MISSING_KEY, + DNSSEC_SIGNATURE_EXPIRED)) { + + r = dns_transaction_dnskey_authenticated(t, rr); + if (r < 0 && r != -ENXIO) + return r; + if (r == 0) { + /* The DNSKEY transaction was not authenticated, this means there's + * no DS for this, which means it's OK if no keys are found for this signature. */ + + r = dns_answer_move_by_key(validated, &t->answer, rr->key, 0, NULL); + if (r < 0) + return r; + + manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key); + return 1; + } + } + + r = dns_transaction_is_primary_response(t, rr); + if (r < 0) + return r; + if (r > 0) { + /* Look for a matching DNAME for this CNAME */ + r = dns_answer_has_dname_for_cname(t->answer, rr); + if (r < 0) + return r; + if (r == 0) { + /* Also look among the stuff we already validated */ + r = dns_answer_has_dname_for_cname(*validated, rr); + if (r < 0) + return r; + } + + if (r == 0) { + if (IN_SET(result, + DNSSEC_INVALID, + DNSSEC_SIGNATURE_EXPIRED, + DNSSEC_NO_SIGNATURE)) + manager_dnssec_verdict(t->scope->manager, DNSSEC_BOGUS, rr->key); + else /* DNSSEC_MISSING_KEY or DNSSEC_UNSUPPORTED_ALGORITHM */ + manager_dnssec_verdict(t->scope->manager, DNSSEC_INDETERMINATE, rr->key); + + /* This is a primary response to our question, and it failed validation. + * That's fatal. */ + t->answer_dnssec_result = result; + return 0; + } + + /* This is a primary response, but we do have a DNAME RR + * in the RR that can replay this CNAME, hence rely on + * that, and we can remove the CNAME in favour of it. */ + } + + /* This is just some auxiliary data. Just remove the RRset and continue. */ + r = dns_answer_remove_by_key(&t->answer, rr->key); + if (r < 0) + return r; + + /* We dropped something from the answer, start from the beginning. */ + return 1; + } + + return 2; /* Finito. */ +} + +int dns_transaction_validate_dnssec(DnsTransaction *t) { + _cleanup_(dns_answer_unrefp) DnsAnswer *validated = NULL; + Phase phase; + DnsAnswerFlags flags; + int r; + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + assert(t); + + /* We have now collected all DS and DNSKEY RRs in t->validated_keys, let's see which RRs we can now + * authenticate with that. */ + + if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) || t->scope->dnssec_mode == DNSSEC_NO) + return 0; + + /* Already validated */ + if (t->answer_dnssec_result != _DNSSEC_RESULT_INVALID) + return 0; + + /* Our own stuff needs no validation */ + if (IN_SET(t->answer_source, DNS_TRANSACTION_ZONE, DNS_TRANSACTION_TRUST_ANCHOR)) { + t->answer_dnssec_result = DNSSEC_VALIDATED; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, true); + return 0; + } + + /* Cached stuff is not affected by validation. */ + if (t->answer_source != DNS_TRANSACTION_NETWORK) + return 0; + + if (!dns_transaction_dnssec_supported_full(t)) { + /* The server does not support DNSSEC, or doesn't augment responses with RRSIGs. */ + t->answer_dnssec_result = DNSSEC_INCOMPATIBLE_SERVER; + log_debug("Not validating response for %" PRIu16 ", used server feature level does not support DNSSEC.", t->id); + return 0; + } + + log_debug("Validating response from transaction %" PRIu16 " (%s).", + t->id, + dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str)); + + /* First, see if this response contains any revoked trust + * anchors we care about */ + r = dns_transaction_check_revoked_trust_anchors(t); + if (r < 0) + return r; + + /* Third, copy all RRs we acquired successfully from auxiliary RRs over. */ + r = dns_transaction_copy_validated(t); + if (r < 0) + return r; + + /* Second, see if there are DNSKEYs we already know a + * validated DS for. */ + r = dns_transaction_validate_dnskey_by_ds(t); + if (r < 0) + return r; + + /* Fourth, remove all DNSKEY and DS RRs again that our trust + * anchor says are revoked. After all we might have marked + * some keys revoked above, but they might still be lingering + * in our validated_keys list. */ + r = dns_transaction_invalidate_revoked_keys(t); + if (r < 0) + return r; + + phase = DNSSEC_PHASE_DNSKEY; + for (;;) { + bool have_nsec = false; + + r = dnssec_validate_records(t, phase, &have_nsec, &validated); + if (r <= 0) + return r; + + /* Try again as long as we managed to achieve something */ + if (r == 1) + continue; + + if (phase == DNSSEC_PHASE_DNSKEY && have_nsec) { + /* OK, we processed all DNSKEYs, and there are NSEC/NSEC3 RRs, look at those now. */ + phase = DNSSEC_PHASE_NSEC; + continue; + } + + if (phase != DNSSEC_PHASE_ALL) { + /* OK, we processed all DNSKEYs and NSEC/NSEC3 RRs, look at all the rest now. + * Note that in this third phase we start to remove RRs we couldn't validate. */ + phase = DNSSEC_PHASE_ALL; + continue; + } + + /* We're done */ + break; + } + + DNS_ANSWER_REPLACE(t->answer, TAKE_PTR(validated)); + + /* At this point the answer only contains validated + * RRsets. Now, let's see if it actually answers the question + * we asked. If so, great! If it doesn't, then see if + * NSEC/NSEC3 can prove this. */ + r = dns_transaction_has_positive_answer(t, &flags); + if (r > 0) { + /* Yes, it answers the question! */ + + if (flags & DNS_ANSWER_AUTHENTICATED) { + /* The answer is fully authenticated, yay. */ + t->answer_dnssec_result = DNSSEC_VALIDATED; + t->answer_rcode = DNS_RCODE_SUCCESS; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, true); + } else { + /* The answer is not fully authenticated. */ + t->answer_dnssec_result = DNSSEC_UNSIGNED; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false); + } + + } else if (r == 0) { + DnssecNsecResult nr; + bool authenticated = false; + + /* Bummer! Let's check NSEC/NSEC3 */ + r = dnssec_nsec_test(t->answer, dns_transaction_key(t), &nr, &authenticated, &t->answer_nsec_ttl); + if (r < 0) + return r; + + switch (nr) { + + case DNSSEC_NSEC_NXDOMAIN: + /* NSEC proves the domain doesn't exist. Very good. */ + log_debug("Proved NXDOMAIN via NSEC/NSEC3 for transaction %u (%s)", t->id, key_str); + t->answer_dnssec_result = DNSSEC_VALIDATED; + t->answer_rcode = DNS_RCODE_NXDOMAIN; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, authenticated); + + manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, dns_transaction_key(t)); + break; + + case DNSSEC_NSEC_NODATA: + /* NSEC proves that there's no data here, very good. */ + log_debug("Proved NODATA via NSEC/NSEC3 for transaction %u (%s)", t->id, key_str); + t->answer_dnssec_result = DNSSEC_VALIDATED; + t->answer_rcode = DNS_RCODE_SUCCESS; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, authenticated); + + manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, dns_transaction_key(t)); + break; + + case DNSSEC_NSEC_OPTOUT: + /* NSEC3 says the data might not be signed */ + log_debug("Data is NSEC3 opt-out via NSEC/NSEC3 for transaction %u (%s)", t->id, key_str); + t->answer_dnssec_result = DNSSEC_UNSIGNED; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false); + + manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, dns_transaction_key(t)); + break; + + case DNSSEC_NSEC_NO_RR: + /* No NSEC data? Bummer! */ + + r = dns_transaction_requires_nsec(t); + if (r < 0) + return r; + if (r > 0) { + t->answer_dnssec_result = DNSSEC_NO_SIGNATURE; + manager_dnssec_verdict(t->scope->manager, DNSSEC_BOGUS, dns_transaction_key(t)); + } else { + t->answer_dnssec_result = DNSSEC_UNSIGNED; + SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false); + manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, dns_transaction_key(t)); + } + + break; + + case DNSSEC_NSEC_UNSUPPORTED_ALGORITHM: + /* We don't know the NSEC3 algorithm used? */ + t->answer_dnssec_result = DNSSEC_UNSUPPORTED_ALGORITHM; + manager_dnssec_verdict(t->scope->manager, DNSSEC_INDETERMINATE, dns_transaction_key(t)); + break; + + case DNSSEC_NSEC_FOUND: + case DNSSEC_NSEC_CNAME: + /* NSEC says it needs to be there, but we couldn't find it? Bummer! */ + t->answer_dnssec_result = DNSSEC_NSEC_MISMATCH; + manager_dnssec_verdict(t->scope->manager, DNSSEC_BOGUS, dns_transaction_key(t)); + break; + + default: + assert_not_reached(); + } + } + + return 1; +} + +static const char* const dns_transaction_state_table[_DNS_TRANSACTION_STATE_MAX] = { + [DNS_TRANSACTION_NULL] = "null", + [DNS_TRANSACTION_PENDING] = "pending", + [DNS_TRANSACTION_VALIDATING] = "validating", + [DNS_TRANSACTION_RCODE_FAILURE] = "rcode-failure", + [DNS_TRANSACTION_SUCCESS] = "success", + [DNS_TRANSACTION_NO_SERVERS] = "no-servers", + [DNS_TRANSACTION_TIMEOUT] = "timeout", + [DNS_TRANSACTION_ATTEMPTS_MAX_REACHED] = "attempts-max-reached", + [DNS_TRANSACTION_INVALID_REPLY] = "invalid-reply", + [DNS_TRANSACTION_ERRNO] = "errno", + [DNS_TRANSACTION_ABORTED] = "aborted", + [DNS_TRANSACTION_DNSSEC_FAILED] = "dnssec-failed", + [DNS_TRANSACTION_NO_TRUST_ANCHOR] = "no-trust-anchor", + [DNS_TRANSACTION_RR_TYPE_UNSUPPORTED] = "rr-type-unsupported", + [DNS_TRANSACTION_NETWORK_DOWN] = "network-down", + [DNS_TRANSACTION_NOT_FOUND] = "not-found", + [DNS_TRANSACTION_NO_SOURCE] = "no-source", + [DNS_TRANSACTION_STUB_LOOP] = "stub-loop", +}; +DEFINE_STRING_TABLE_LOOKUP(dns_transaction_state, DnsTransactionState); + +static const char* const dns_transaction_source_table[_DNS_TRANSACTION_SOURCE_MAX] = { + [DNS_TRANSACTION_NETWORK] = "network", + [DNS_TRANSACTION_CACHE] = "cache", + [DNS_TRANSACTION_ZONE] = "zone", + [DNS_TRANSACTION_TRUST_ANCHOR] = "trust-anchor", +}; +DEFINE_STRING_TABLE_LOOKUP(dns_transaction_source, DnsTransactionSource); diff --git a/src/resolve/resolved-dns-transaction.h b/src/resolve/resolved-dns-transaction.h new file mode 100644 index 0000000..5a0e357 --- /dev/null +++ b/src/resolve/resolved-dns-transaction.h @@ -0,0 +1,220 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-event.h" +#include "in-addr-util.h" + +typedef struct DnsTransaction DnsTransaction; +typedef struct DnsTransactionFinder DnsTransactionFinder; +typedef enum DnsTransactionState DnsTransactionState; +typedef enum DnsTransactionSource DnsTransactionSource; + +#include "resolved-dns-answer.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-question.h" +#include "resolved-dns-server.h" + +enum DnsTransactionState { + DNS_TRANSACTION_NULL, + DNS_TRANSACTION_PENDING, + DNS_TRANSACTION_VALIDATING, + DNS_TRANSACTION_RCODE_FAILURE, + DNS_TRANSACTION_SUCCESS, + DNS_TRANSACTION_NO_SERVERS, + DNS_TRANSACTION_TIMEOUT, + DNS_TRANSACTION_ATTEMPTS_MAX_REACHED, + DNS_TRANSACTION_INVALID_REPLY, + DNS_TRANSACTION_ERRNO, + DNS_TRANSACTION_ABORTED, + DNS_TRANSACTION_DNSSEC_FAILED, + DNS_TRANSACTION_NO_TRUST_ANCHOR, + DNS_TRANSACTION_RR_TYPE_UNSUPPORTED, + DNS_TRANSACTION_NETWORK_DOWN, + DNS_TRANSACTION_NOT_FOUND, /* like NXDOMAIN, but when LLMNR/TCP connections fail */ + DNS_TRANSACTION_NO_SOURCE, /* All suitable DnsTransactionSource turned off */ + DNS_TRANSACTION_STUB_LOOP, + _DNS_TRANSACTION_STATE_MAX, + _DNS_TRANSACTION_STATE_INVALID = -EINVAL, +}; + +#define DNS_TRANSACTION_IS_LIVE(state) IN_SET((state), DNS_TRANSACTION_NULL, DNS_TRANSACTION_PENDING, DNS_TRANSACTION_VALIDATING) + +enum DnsTransactionSource { + DNS_TRANSACTION_NETWORK, + DNS_TRANSACTION_CACHE, + DNS_TRANSACTION_ZONE, + DNS_TRANSACTION_TRUST_ANCHOR, + _DNS_TRANSACTION_SOURCE_MAX, + _DNS_TRANSACTION_SOURCE_INVALID = -EINVAL, +}; + +struct DnsTransaction { + DnsScope *scope; + + DnsResourceKey *key; /* For regular lookups the RR key to look for */ + DnsPacket *bypass; /* For bypass lookups the full original request packet */ + + uint64_t query_flags; + + DnsPacket *sent, *received; + + DnsAnswer *answer; + int answer_rcode; + DnssecResult answer_dnssec_result; + DnsTransactionSource answer_source; + uint32_t answer_nsec_ttl; + int answer_errno; /* if state is DNS_TRANSACTION_ERRNO */ + + DnsTransactionState state; + + /* SD_RESOLVED_AUTHENTICATED here indicates whether the primary answer is authenticated, i.e. whether + * the RRs from answer which directly match the question are authenticated, or, if there are none, + * whether the NODATA or NXDOMAIN case is. It says nothing about additional RRs listed in the answer, + * however they have their own DNS_ANSWER_AUTHORIZED FLAGS. Note that this bit is defined different + * than the AD bit in DNS packets, as that covers more than just the actual primary answer. */ + uint64_t answer_query_flags; + + /* Contains DNSKEY, DS, SOA RRs we already verified and need + * to authenticate this reply */ + DnsAnswer *validated_keys; + + usec_t start_usec; + usec_t next_attempt_after; + sd_event_source *timeout_event_source; + unsigned n_attempts; + + /* UDP connection logic, if we need it */ + int dns_udp_fd; + sd_event_source *dns_udp_event_source; + + /* TCP connection logic, if we need it */ + DnsStream *stream; + + /* The active server */ + DnsServer *server; + + /* The features of the DNS server at time of transaction start */ + DnsServerFeatureLevel current_feature_level; + + /* If we got SERVFAIL back, we retry the lookup, using a lower feature level than we used + * before. Similar, if we get NXDOMAIN in pure EDNS0 mode, we check in EDNS0-less mode before giving + * up (as mitigation for DVE-2018-0001). */ + DnsServerFeatureLevel clamp_feature_level_servfail; + DnsServerFeatureLevel clamp_feature_level_nxdomain; + + uint16_t id; + + bool tried_stream:1; + + bool initial_jitter_scheduled:1; + bool initial_jitter_elapsed:1; + + bool probing:1; + + /* Query candidates this transaction is referenced by and that + * shall be notified about this specific transaction + * completing. */ + Set *notify_query_candidates, *notify_query_candidates_done; + + /* Zone items this transaction is referenced by and that shall + * be notified about completion. */ + Set *notify_zone_items, *notify_zone_items_done; + + /* Other transactions that this transactions is referenced by + * and that shall be notified about completion. This is used + * when transactions want to validate their RRsets, but need + * another DNSKEY or DS RR to do so. */ + Set *notify_transactions, *notify_transactions_done; + + /* The opposite direction: the transactions this transaction + * created in order to request DNSKEY or DS RRs. */ + Set *dnssec_transactions; + + unsigned n_picked_servers; + + unsigned block_gc; + + LIST_FIELDS(DnsTransaction, transactions_by_scope); + LIST_FIELDS(DnsTransaction, transactions_by_stream); + LIST_FIELDS(DnsTransaction, transactions_by_key); + + /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */ +}; + +int dns_transaction_new(DnsTransaction **ret, DnsScope *s, DnsResourceKey *key, DnsPacket *bypass, uint64_t flags); +DnsTransaction* dns_transaction_free(DnsTransaction *t); + +DnsTransaction* dns_transaction_gc(DnsTransaction *t); +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsTransaction*, dns_transaction_gc); + +int dns_transaction_go(DnsTransaction *t); + +void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypted); +void dns_transaction_complete(DnsTransaction *t, DnsTransactionState state); + +void dns_transaction_notify(DnsTransaction *t, DnsTransaction *source); +int dns_transaction_validate_dnssec(DnsTransaction *t); +int dns_transaction_request_dnssec_keys(DnsTransaction *t); + +static inline DnsResourceKey *dns_transaction_key(DnsTransaction *t) { + assert(t); + + /* Return the lookup key of this transaction. Either takes the lookup key from the bypass packet if + * we are a bypass transaction. Or take the configured key for regular transactions. */ + + if (t->key) + return t->key; + + assert(t->bypass); + + return dns_question_first_key(t->bypass->question); +} + +static inline uint64_t dns_transaction_source_to_query_flags(DnsTransactionSource s) { + + switch (s) { + + case DNS_TRANSACTION_NETWORK: + return SD_RESOLVED_FROM_NETWORK; + + case DNS_TRANSACTION_CACHE: + return SD_RESOLVED_FROM_CACHE; + + case DNS_TRANSACTION_ZONE: + return SD_RESOLVED_FROM_ZONE; + + case DNS_TRANSACTION_TRUST_ANCHOR: + return SD_RESOLVED_FROM_TRUST_ANCHOR; + + default: + return 0; + } +} + +const char* dns_transaction_state_to_string(DnsTransactionState p) _const_; +DnsTransactionState dns_transaction_state_from_string(const char *s) _pure_; + +const char* dns_transaction_source_to_string(DnsTransactionSource p) _const_; +DnsTransactionSource dns_transaction_source_from_string(const char *s) _pure_; + +/* LLMNR Jitter interval, see RFC 4795 Section 7 */ +#define LLMNR_JITTER_INTERVAL_USEC (100 * USEC_PER_MSEC) + +/* mDNS probing interval, see RFC 6762 Section 8.1 */ +#define MDNS_PROBING_INTERVAL_USEC (250 * USEC_PER_MSEC) + +/* Maximum attempts to send DNS requests, across all DNS servers */ +#define DNS_TRANSACTION_ATTEMPTS_MAX 24 + +/* Maximum attempts to send LLMNR requests, see RFC 4795 Section 2.7 */ +#define LLMNR_TRANSACTION_ATTEMPTS_MAX 3 + +/* Maximum attempts to send MDNS requests, see RFC 6762 Section 8.1 */ +#define MDNS_TRANSACTION_ATTEMPTS_MAX 3 + +#define TRANSACTION_ATTEMPTS_MAX(p) ((p) == DNS_PROTOCOL_LLMNR ? \ + LLMNR_TRANSACTION_ATTEMPTS_MAX : \ + (p) == DNS_PROTOCOL_MDNS ? \ + MDNS_TRANSACTION_ATTEMPTS_MAX : \ + DNS_TRANSACTION_ATTEMPTS_MAX) diff --git a/src/resolve/resolved-dns-trust-anchor.c b/src/resolve/resolved-dns-trust-anchor.c new file mode 100644 index 0000000..69a484d --- /dev/null +++ b/src/resolve/resolved-dns-trust-anchor.c @@ -0,0 +1,774 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-messages.h" + +#include "alloc-util.h" +#include "conf-files.h" +#include "def.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "fileio.h" +#include "hexdecoct.h" +#include "nulstr-util.h" +#include "parse-util.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-trust-anchor.h" +#include "set.h" +#include "sort-util.h" +#include "string-util.h" +#include "strv.h" + +static const char trust_anchor_dirs[] = CONF_PATHS_NULSTR("dnssec-trust-anchors.d"); + +/* The second DS RR from https://data.iana.org/root-anchors/root-anchors.xml, retrieved February 2017 */ +static const uint8_t root_digest2[] = + { 0xE0, 0x6D, 0x44, 0xB8, 0x0B, 0x8F, 0x1D, 0x39, 0xA9, 0x5C, 0x0B, 0x0D, 0x7C, 0x65, 0xD0, 0x84, + 0x58, 0xE8, 0x80, 0x40, 0x9B, 0xBC, 0x68, 0x34, 0x57, 0x10, 0x42, 0x37, 0xC7, 0xF8, 0xEC, 0x8D }; + +static bool dns_trust_anchor_knows_domain_positive(DnsTrustAnchor *d, const char *name) { + assert(d); + + /* Returns true if there's an entry for the specified domain + * name in our trust anchor */ + + return + hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DNSKEY, name)) || + hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DS, name)); +} + +static int add_root_ksk( + DnsAnswer *answer, + DnsResourceKey *key, + uint16_t key_tag, + uint8_t algorithm, + uint8_t digest_type, + const void *digest, + size_t digest_size) { + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + int r; + + rr = dns_resource_record_new(key); + if (!rr) + return -ENOMEM; + + rr->ds.key_tag = key_tag; + rr->ds.algorithm = algorithm; + rr->ds.digest_type = digest_type; + rr->ds.digest_size = digest_size; + rr->ds.digest = memdup(digest, rr->ds.digest_size); + if (!rr->ds.digest) + return -ENOMEM; + + r = dns_answer_add(answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return r; + + return 0; +} + +static int dns_trust_anchor_add_builtin_positive(DnsTrustAnchor *d) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + int r; + + assert(d); + + r = hashmap_ensure_allocated(&d->positive_by_key, &dns_resource_key_hash_ops); + if (r < 0) + return r; + + /* Only add the built-in trust anchor if there's neither a DS nor a DNSKEY defined for the root domain. That + * way users have an easy way to override the root domain DS/DNSKEY data. */ + if (dns_trust_anchor_knows_domain_positive(d, ".")) + return 0; + + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_DS, ""); + if (!key) + return -ENOMEM; + + answer = dns_answer_new(2); + if (!answer) + return -ENOMEM; + + /* Add the currently valid RRs from https://data.iana.org/root-anchors/root-anchors.xml */ + r = add_root_ksk(answer, key, 20326, DNSSEC_ALGORITHM_RSASHA256, DNSSEC_DIGEST_SHA256, root_digest2, sizeof(root_digest2)); + if (r < 0) + return r; + + r = hashmap_put(d->positive_by_key, key, answer); + if (r < 0) + return r; + + answer = NULL; + return 0; +} + +static int dns_trust_anchor_add_builtin_negative(DnsTrustAnchor *d) { + + static const char private_domains[] = + /* RFC 6761 says that .test is a special domain for + * testing and not to be installed in the root zone */ + "test\0" + + /* RFC 6761 says that these reverse IP lookup ranges + * are for private addresses, and hence should not + * show up in the root zone */ + "10.in-addr.arpa\0" + "16.172.in-addr.arpa\0" + "17.172.in-addr.arpa\0" + "18.172.in-addr.arpa\0" + "19.172.in-addr.arpa\0" + "20.172.in-addr.arpa\0" + "21.172.in-addr.arpa\0" + "22.172.in-addr.arpa\0" + "23.172.in-addr.arpa\0" + "24.172.in-addr.arpa\0" + "25.172.in-addr.arpa\0" + "26.172.in-addr.arpa\0" + "27.172.in-addr.arpa\0" + "28.172.in-addr.arpa\0" + "29.172.in-addr.arpa\0" + "30.172.in-addr.arpa\0" + "31.172.in-addr.arpa\0" + "168.192.in-addr.arpa\0" + + /* The same, but for IPv6. */ + "d.f.ip6.arpa\0" + + /* RFC 6762 reserves the .local domain for Multicast + * DNS, it hence cannot appear in the root zone. (Note + * that we by default do not route .local traffic to + * DNS anyway, except when a configured search domain + * suggests so.) */ + "local\0" + + /* These two are well known, popular private zone + * TLDs, that are blocked from delegation, according + * to: + * http://icannwiki.com/Name_Collision#NGPC_Resolution + * + * There's also ongoing work on making this official + * in an RRC: + * https://www.ietf.org/archive/id/draft-chapin-additional-reserved-tlds-02.txt */ + "home\0" + "corp\0" + + /* The following four TLDs are suggested for private + * zones in RFC 6762, Appendix G, and are hence very + * unlikely to be made official TLDs any day soon */ + "lan\0" + "intranet\0" + "internal\0" + "private\0" + + /* Defined by RFC 8375. The most official choice. */ + "home.arpa\0"; + + const char *name; + int r; + + assert(d); + + /* Only add the built-in trust anchor if there's no negative + * trust anchor defined at all. This enables easy overriding + * of negative trust anchors. */ + + if (set_size(d->negative_by_name) > 0) + return 0; + + r = set_ensure_allocated(&d->negative_by_name, &dns_name_hash_ops); + if (r < 0) + return r; + + /* We add a couple of domains as default negative trust + * anchors, where it's very unlikely they will be installed in + * the root zone. If they exist they must be private, and thus + * unsigned. */ + + NULSTR_FOREACH(name, private_domains) { + if (dns_trust_anchor_knows_domain_positive(d, name)) + continue; + + r = set_put_strdup(&d->negative_by_name, name); + if (r < 0) + return r; + } + + return 0; +} + +static int dns_trust_anchor_load_positive(DnsTrustAnchor *d, const char *path, unsigned line, const char *s) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + _cleanup_free_ char *domain = NULL, *class = NULL, *type = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnsAnswer *old_answer = NULL; + const char *p = s; + int r; + + assert(d); + assert(line); + + r = extract_first_word(&p, &domain, NULL, EXTRACT_UNQUOTE); + if (r < 0) + return log_warning_errno(r, "Unable to parse domain in line %s:%u: %m", path, line); + + r = dns_name_is_valid(domain); + if (r < 0) + return log_warning_errno(r, "Failed to check validity of domain name '%s', at line %s:%u, ignoring line: %m", domain, path, line); + if (r == 0) { + log_warning("Domain name %s is invalid, at line %s:%u, ignoring line.", domain, path, line); + return -EINVAL; + } + + r = extract_many_words(&p, NULL, 0, &class, &type, NULL); + if (r < 0) + return log_warning_errno(r, "Unable to parse class and type in line %s:%u: %m", path, line); + if (r != 2) { + log_warning("Missing class or type in line %s:%u", path, line); + return -EINVAL; + } + + if (!strcaseeq(class, "IN")) { + log_warning("RR class %s is not supported, ignoring line %s:%u.", class, path, line); + return -EINVAL; + } + + if (strcaseeq(type, "DS")) { + _cleanup_free_ char *key_tag = NULL, *algorithm = NULL, *digest_type = NULL; + _cleanup_free_ void *dd = NULL; + uint16_t kt; + int a, dt; + size_t l; + + r = extract_many_words(&p, NULL, 0, &key_tag, &algorithm, &digest_type, NULL); + if (r < 0) { + log_warning_errno(r, "Failed to parse DS parameters on line %s:%u: %m", path, line); + return -EINVAL; + } + if (r != 3) { + log_warning("Missing DS parameters on line %s:%u", path, line); + return -EINVAL; + } + + r = safe_atou16(key_tag, &kt); + if (r < 0) + return log_warning_errno(r, "Failed to parse DS key tag %s on line %s:%u: %m", key_tag, path, line); + + a = dnssec_algorithm_from_string(algorithm); + if (a < 0) { + log_warning("Failed to parse DS algorithm %s on line %s:%u", algorithm, path, line); + return -EINVAL; + } + + dt = dnssec_digest_from_string(digest_type); + if (dt < 0) { + log_warning("Failed to parse DS digest type %s on line %s:%u", digest_type, path, line); + return -EINVAL; + } + + if (isempty(p)) { + log_warning("Missing DS digest on line %s:%u", path, line); + return -EINVAL; + } + + r = unhexmem(p, strlen(p), &dd, &l); + if (r < 0) { + log_warning("Failed to parse DS digest %s on line %s:%u", p, path, line); + return -EINVAL; + } + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, domain); + if (!rr) + return log_oom(); + + rr->ds.key_tag = kt; + rr->ds.algorithm = a; + rr->ds.digest_type = dt; + rr->ds.digest_size = l; + rr->ds.digest = TAKE_PTR(dd); + + } else if (strcaseeq(type, "DNSKEY")) { + _cleanup_free_ char *flags = NULL, *protocol = NULL, *algorithm = NULL; + _cleanup_free_ void *k = NULL; + uint16_t f; + size_t l; + int a; + + r = extract_many_words(&p, NULL, 0, &flags, &protocol, &algorithm, NULL); + if (r < 0) + return log_warning_errno(r, "Failed to parse DNSKEY parameters on line %s:%u: %m", path, line); + if (r != 3) { + log_warning("Missing DNSKEY parameters on line %s:%u", path, line); + return -EINVAL; + } + + if (!streq(protocol, "3")) { + log_warning("DNSKEY Protocol is not 3 on line %s:%u", path, line); + return -EINVAL; + } + + r = safe_atou16(flags, &f); + if (r < 0) + return log_warning_errno(r, "Failed to parse DNSKEY flags field %s on line %s:%u", flags, path, line); + if ((f & DNSKEY_FLAG_ZONE_KEY) == 0) { + log_warning("DNSKEY lacks zone key bit set on line %s:%u", path, line); + return -EINVAL; + } + if ((f & DNSKEY_FLAG_REVOKE)) { + log_warning("DNSKEY is already revoked on line %s:%u", path, line); + return -EINVAL; + } + + a = dnssec_algorithm_from_string(algorithm); + if (a < 0) { + log_warning("Failed to parse DNSKEY algorithm %s on line %s:%u", algorithm, path, line); + return -EINVAL; + } + + if (isempty(p)) { + log_warning("Missing DNSKEY key on line %s:%u", path, line); + return -EINVAL; + } + + r = unbase64mem(p, strlen(p), &k, &l); + if (r < 0) + return log_warning_errno(r, "Failed to parse DNSKEY key data %s on line %s:%u", p, path, line); + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, domain); + if (!rr) + return log_oom(); + + rr->dnskey.flags = f; + rr->dnskey.protocol = 3; + rr->dnskey.algorithm = a; + rr->dnskey.key_size = l; + rr->dnskey.key = TAKE_PTR(k); + + } else { + log_warning("RR type %s is not supported, ignoring line %s:%u.", type, path, line); + return -EINVAL; + } + + r = hashmap_ensure_allocated(&d->positive_by_key, &dns_resource_key_hash_ops); + if (r < 0) + return log_oom(); + + old_answer = hashmap_get(d->positive_by_key, rr->key); + answer = dns_answer_ref(old_answer); + + r = dns_answer_add_extend(&answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return log_error_errno(r, "Failed to add trust anchor RR: %m"); + + r = hashmap_replace(d->positive_by_key, rr->key, answer); + if (r < 0) + return log_error_errno(r, "Failed to add answer to trust anchor: %m"); + + old_answer = dns_answer_unref(old_answer); + answer = NULL; + + return 0; +} + +static int dns_trust_anchor_load_negative(DnsTrustAnchor *d, const char *path, unsigned line, const char *s) { + _cleanup_free_ char *domain = NULL; + const char *p = s; + int r; + + assert(d); + assert(line); + + r = extract_first_word(&p, &domain, NULL, EXTRACT_UNQUOTE); + if (r < 0) + return log_warning_errno(r, "Unable to parse line %s:%u: %m", path, line); + + r = dns_name_is_valid(domain); + if (r < 0) + return log_warning_errno(r, "Failed to check validity of domain name '%s', at line %s:%u, ignoring line: %m", domain, path, line); + if (r == 0) { + log_warning("Domain name %s is invalid, at line %s:%u, ignoring line.", domain, path, line); + return -EINVAL; + } + + if (!isempty(p)) { + log_warning("Trailing garbage at line %s:%u, ignoring line.", path, line); + return -EINVAL; + } + + r = set_ensure_consume(&d->negative_by_name, &dns_name_hash_ops, TAKE_PTR(domain)); + if (r < 0) + return log_oom(); + + return 0; +} + +static int dns_trust_anchor_load_files( + DnsTrustAnchor *d, + const char *suffix, + int (*loader)(DnsTrustAnchor *d, const char *path, unsigned n, const char *line)) { + + _cleanup_strv_free_ char **files = NULL; + int r; + + assert(d); + assert(suffix); + assert(loader); + + r = conf_files_list_nulstr(&files, suffix, NULL, 0, trust_anchor_dirs); + if (r < 0) + return log_error_errno(r, "Failed to enumerate %s trust anchor files: %m", suffix); + + STRV_FOREACH(f, files) { + _cleanup_fclose_ FILE *g = NULL; + unsigned n = 0; + + g = fopen(*f, "re"); + if (!g) { + if (errno == ENOENT) + continue; + + log_warning_errno(errno, "Failed to open '%s', ignoring: %m", *f); + continue; + } + + for (;;) { + _cleanup_free_ char *line = NULL; + char *l; + + r = read_line(g, LONG_LINE_MAX, &line); + if (r < 0) { + log_warning_errno(r, "Failed to read '%s', ignoring: %m", *f); + break; + } + if (r == 0) + break; + + n++; + + l = strstrip(line); + if (isempty(l)) + continue; + + if (*l == ';') + continue; + + (void) loader(d, *f, n, l); + } + } + + return 0; +} + +static int domain_name_cmp(char * const *a, char * const *b) { + return dns_name_compare_func(*a, *b); +} + +static int dns_trust_anchor_dump(DnsTrustAnchor *d) { + DnsAnswer *a; + + assert(d); + + if (hashmap_isempty(d->positive_by_key)) + log_info("No positive trust anchors defined."); + else { + log_info("Positive Trust Anchors:"); + HASHMAP_FOREACH(a, d->positive_by_key) { + DnsResourceRecord *rr; + + DNS_ANSWER_FOREACH(rr, a) + log_info("%s", dns_resource_record_to_string(rr)); + } + } + + if (set_isempty(d->negative_by_name)) + log_info("No negative trust anchors defined."); + else { + _cleanup_free_ char **l = NULL, *j = NULL; + + l = set_get_strv(d->negative_by_name); + if (!l) + return log_oom(); + + typesafe_qsort(l, set_size(d->negative_by_name), domain_name_cmp); + + j = strv_join(l, " "); + if (!j) + return log_oom(); + + log_info("Negative trust anchors: %s", j); + } + + return 0; +} + +int dns_trust_anchor_load(DnsTrustAnchor *d) { + int r; + + assert(d); + + /* If loading things from disk fails, we don't consider this fatal */ + (void) dns_trust_anchor_load_files(d, ".positive", dns_trust_anchor_load_positive); + (void) dns_trust_anchor_load_files(d, ".negative", dns_trust_anchor_load_negative); + + /* However, if the built-in DS fails, then we have a problem. */ + r = dns_trust_anchor_add_builtin_positive(d); + if (r < 0) + return log_error_errno(r, "Failed to add built-in positive trust anchor: %m"); + + r = dns_trust_anchor_add_builtin_negative(d); + if (r < 0) + return log_error_errno(r, "Failed to add built-in negative trust anchor: %m"); + + dns_trust_anchor_dump(d); + + return 0; +} + +void dns_trust_anchor_flush(DnsTrustAnchor *d) { + assert(d); + + d->positive_by_key = hashmap_free_with_destructor(d->positive_by_key, dns_answer_unref); + d->revoked_by_rr = set_free_with_destructor(d->revoked_by_rr, dns_resource_record_unref); + d->negative_by_name = set_free_free(d->negative_by_name); +} + +int dns_trust_anchor_lookup_positive(DnsTrustAnchor *d, const DnsResourceKey *key, DnsAnswer **ret) { + DnsAnswer *a; + + assert(d); + assert(key); + assert(ret); + + /* We only serve DS and DNSKEY RRs. */ + if (!IN_SET(key->type, DNS_TYPE_DS, DNS_TYPE_DNSKEY)) + return 0; + + a = hashmap_get(d->positive_by_key, key); + if (!a) + return 0; + + *ret = dns_answer_ref(a); + return 1; +} + +int dns_trust_anchor_lookup_negative(DnsTrustAnchor *d, const char *name) { + int r; + + assert(d); + assert(name); + + for (;;) { + /* If the domain is listed as-is in the NTA database, then that counts */ + if (set_contains(d->negative_by_name, name)) + return true; + + /* If the domain isn't listed as NTA, but is listed as positive trust anchor, then that counts. See RFC + * 7646, section 1.1 */ + if (hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DS, name))) + return false; + + if (hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_KEY, name))) + return false; + + /* And now, let's look at the parent, and check that too */ + r = dns_name_parent(&name); + if (r < 0) + return r; + if (r == 0) + break; + } + + return false; +} + +static int dns_trust_anchor_revoked_put(DnsTrustAnchor *d, DnsResourceRecord *rr) { + int r; + + assert(d); + + r = set_ensure_put(&d->revoked_by_rr, &dns_resource_record_hash_ops, rr); + if (r < 0) + return r; + if (r > 0) + dns_resource_record_ref(rr); + + return r; +} + +static int dns_trust_anchor_remove_revoked(DnsTrustAnchor *d, DnsResourceRecord *rr) { + _cleanup_(dns_answer_unrefp) DnsAnswer *new_answer = NULL; + DnsAnswer *old_answer; + DnsAnswerItem *item; + int r; + + /* Remember that this is a revoked trust anchor RR */ + r = dns_trust_anchor_revoked_put(d, rr); + if (r < 0) + return r; + + /* Remove this from the positive trust anchor */ + old_answer = hashmap_get(d->positive_by_key, rr->key); + if (!old_answer) + return 0; + + new_answer = dns_answer_ref(old_answer); + + r = dns_answer_remove_by_rr(&new_answer, rr); + if (r <= 0) + return r; + + /* We found the key! Warn the user */ + log_struct(LOG_WARNING, + "MESSAGE_ID=" SD_MESSAGE_DNSSEC_TRUST_ANCHOR_REVOKED_STR, + LOG_MESSAGE("DNSSEC trust anchor %s has been revoked.\n" + "Please update the trust anchor, or upgrade your operating system.", + strna(dns_resource_record_to_string(rr))), + "TRUST_ANCHOR=%s", dns_resource_record_to_string(rr)); + + if (dns_answer_size(new_answer) <= 0) { + assert_se(hashmap_remove(d->positive_by_key, rr->key) == old_answer); + dns_answer_unref(old_answer); + return 1; + } + + item = ordered_set_first(new_answer->items); + r = hashmap_replace(d->positive_by_key, item->rr->key, new_answer); + if (r < 0) + return r; + + TAKE_PTR(new_answer); + dns_answer_unref(old_answer); + return 1; +} + +static int dns_trust_anchor_check_revoked_one(DnsTrustAnchor *d, DnsResourceRecord *revoked_dnskey) { + DnsAnswer *a; + int r; + + assert(d); + assert(revoked_dnskey); + assert(revoked_dnskey->key->type == DNS_TYPE_DNSKEY); + assert(revoked_dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE); + + a = hashmap_get(d->positive_by_key, revoked_dnskey->key); + if (a) { + DnsResourceRecord *anchor; + + /* First, look for the precise DNSKEY in our trust anchor database */ + + DNS_ANSWER_FOREACH(anchor, a) { + + if (anchor->dnskey.protocol != revoked_dnskey->dnskey.protocol) + continue; + + if (anchor->dnskey.algorithm != revoked_dnskey->dnskey.algorithm) + continue; + + if (anchor->dnskey.key_size != revoked_dnskey->dnskey.key_size) + continue; + + /* Note that we allow the REVOKE bit to be + * different! It will be set in the revoked + * key, but unset in our version of it */ + if (((anchor->dnskey.flags ^ revoked_dnskey->dnskey.flags) | DNSKEY_FLAG_REVOKE) != DNSKEY_FLAG_REVOKE) + continue; + + if (memcmp(anchor->dnskey.key, revoked_dnskey->dnskey.key, anchor->dnskey.key_size) != 0) + continue; + + dns_trust_anchor_remove_revoked(d, anchor); + break; + } + } + + a = hashmap_get(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(revoked_dnskey->key->class, DNS_TYPE_DS, dns_resource_key_name(revoked_dnskey->key))); + if (a) { + DnsResourceRecord *anchor; + + /* Second, look for DS RRs matching this DNSKEY in our trust anchor database */ + + DNS_ANSWER_FOREACH(anchor, a) { + + /* We set mask_revoke to true here, since our + * DS fingerprint will be the one of the + * unrevoked DNSKEY, but the one we got passed + * here has the bit set. */ + r = dnssec_verify_dnskey_by_ds(revoked_dnskey, anchor, true); + if (r < 0) + return r; + if (r == 0) + continue; + + dns_trust_anchor_remove_revoked(d, anchor); + break; + } + } + + return 0; +} + +int dns_trust_anchor_check_revoked(DnsTrustAnchor *d, DnsResourceRecord *dnskey, DnsAnswer *rrs) { + DnsResourceRecord *rrsig; + int r; + + assert(d); + assert(dnskey); + + /* Looks if "dnskey" is a self-signed RR that has been revoked + * and matches one of our trust anchor entries. If so, removes + * it from the trust anchor and returns > 0. */ + + if (dnskey->key->type != DNS_TYPE_DNSKEY) + return 0; + + /* Is this DNSKEY revoked? */ + if ((dnskey->dnskey.flags & DNSKEY_FLAG_REVOKE) == 0) + return 0; + + /* Could this be interesting to us at all? If not, + * there's no point in looking for and verifying a + * self-signed RRSIG. */ + if (!dns_trust_anchor_knows_domain_positive(d, dns_resource_key_name(dnskey->key))) + return 0; + + /* Look for a self-signed RRSIG in the other rrs belonging to this DNSKEY */ + DNS_ANSWER_FOREACH(rrsig, rrs) { + DnssecResult result; + + if (rrsig->key->type != DNS_TYPE_RRSIG) + continue; + + r = dnssec_rrsig_match_dnskey(rrsig, dnskey, true); + if (r < 0) + return r; + if (r == 0) + continue; + + r = dnssec_verify_rrset(rrs, dnskey->key, rrsig, dnskey, USEC_INFINITY, &result); + if (r < 0) + return r; + if (result != DNSSEC_VALIDATED) + continue; + + /* Bingo! This is a revoked self-signed DNSKEY. Let's + * see if this precise one exists in our trust anchor + * database, too. */ + r = dns_trust_anchor_check_revoked_one(d, dnskey); + if (r < 0) + return r; + + return 1; + } + + return 0; +} + +int dns_trust_anchor_is_revoked(DnsTrustAnchor *d, DnsResourceRecord *rr) { + assert(d); + + if (!IN_SET(rr->key->type, DNS_TYPE_DS, DNS_TYPE_DNSKEY)) + return 0; + + return set_contains(d->revoked_by_rr, rr); +} diff --git a/src/resolve/resolved-dns-trust-anchor.h b/src/resolve/resolved-dns-trust-anchor.h new file mode 100644 index 0000000..14047ec --- /dev/null +++ b/src/resolve/resolved-dns-trust-anchor.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct DnsTrustAnchor DnsTrustAnchor; + +#include "hashmap.h" +#include "resolved-dns-answer.h" +#include "resolved-dns-rr.h" + +/* This contains a fixed database mapping domain names to DS or DNSKEY records. */ + +struct DnsTrustAnchor { + Hashmap *positive_by_key; + Set *negative_by_name; + Set *revoked_by_rr; +}; + +int dns_trust_anchor_load(DnsTrustAnchor *d); +void dns_trust_anchor_flush(DnsTrustAnchor *d); + +int dns_trust_anchor_lookup_positive(DnsTrustAnchor *d, const DnsResourceKey* key, DnsAnswer **answer); +int dns_trust_anchor_lookup_negative(DnsTrustAnchor *d, const char *name); + +int dns_trust_anchor_check_revoked(DnsTrustAnchor *d, DnsResourceRecord *dnskey, DnsAnswer *rrs); +int dns_trust_anchor_is_revoked(DnsTrustAnchor *d, DnsResourceRecord *rr); diff --git a/src/resolve/resolved-dns-zone.c b/src/resolve/resolved-dns-zone.c new file mode 100644 index 0000000..f533f97 --- /dev/null +++ b/src/resolve/resolved-dns-zone.c @@ -0,0 +1,686 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "dns-domain.h" +#include "list.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-zone.h" +#include "resolved-dnssd.h" +#include "resolved-manager.h" +#include "string-util.h" + +/* Never allow more than 1K entries */ +#define ZONE_MAX 1024 + +void dns_zone_item_probe_stop(DnsZoneItem *i) { + DnsTransaction *t; + assert(i); + + if (!i->probe_transaction) + return; + + t = TAKE_PTR(i->probe_transaction); + + set_remove(t->notify_zone_items, i); + set_remove(t->notify_zone_items_done, i); + dns_transaction_gc(t); +} + +static DnsZoneItem* dns_zone_item_free(DnsZoneItem *i) { + if (!i) + return NULL; + + dns_zone_item_probe_stop(i); + dns_resource_record_unref(i->rr); + + return mfree(i); +} +DEFINE_TRIVIAL_CLEANUP_FUNC(DnsZoneItem*, dns_zone_item_free); + +static void dns_zone_item_remove_and_free(DnsZone *z, DnsZoneItem *i) { + DnsZoneItem *first; + + assert(z); + + if (!i) + return; + + first = hashmap_get(z->by_key, i->rr->key); + LIST_REMOVE(by_key, first, i); + if (first) + assert_se(hashmap_replace(z->by_key, first->rr->key, first) >= 0); + else + hashmap_remove(z->by_key, i->rr->key); + + first = hashmap_get(z->by_name, dns_resource_key_name(i->rr->key)); + LIST_REMOVE(by_name, first, i); + if (first) + assert_se(hashmap_replace(z->by_name, dns_resource_key_name(first->rr->key), first) >= 0); + else + hashmap_remove(z->by_name, dns_resource_key_name(i->rr->key)); + + dns_zone_item_free(i); +} + +void dns_zone_flush(DnsZone *z) { + DnsZoneItem *i; + + assert(z); + + while ((i = hashmap_first(z->by_key))) + dns_zone_item_remove_and_free(z, i); + + assert(hashmap_size(z->by_key) == 0); + assert(hashmap_size(z->by_name) == 0); + + z->by_key = hashmap_free(z->by_key); + z->by_name = hashmap_free(z->by_name); +} + +DnsZoneItem* dns_zone_get(DnsZone *z, DnsResourceRecord *rr) { + assert(z); + assert(rr); + + LIST_FOREACH(by_key, i, (DnsZoneItem*) hashmap_get(z->by_key, rr->key)) + if (dns_resource_record_equal(i->rr, rr) > 0) + return i; + + return NULL; +} + +void dns_zone_remove_rr(DnsZone *z, DnsResourceRecord *rr) { + DnsZoneItem *i; + + assert(z); + + if (!rr) + return; + + i = dns_zone_get(z, rr); + if (i) + dns_zone_item_remove_and_free(z, i); +} + +int dns_zone_remove_rrs_by_key(DnsZone *z, DnsResourceKey *key) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; + DnsResourceRecord *rr; + bool tentative; + int r; + + r = dns_zone_lookup(z, key, 0, &answer, &soa, &tentative); + if (r < 0) + return r; + + DNS_ANSWER_FOREACH(rr, answer) + dns_zone_remove_rr(z, rr); + + return 0; +} + +static int dns_zone_init(DnsZone *z) { + int r; + + assert(z); + + r = hashmap_ensure_allocated(&z->by_key, &dns_resource_key_hash_ops); + if (r < 0) + return r; + + r = hashmap_ensure_allocated(&z->by_name, &dns_name_hash_ops); + if (r < 0) + return r; + + return 0; +} + +static int dns_zone_link_item(DnsZone *z, DnsZoneItem *i) { + DnsZoneItem *first; + int r; + + first = hashmap_get(z->by_key, i->rr->key); + if (first) { + LIST_PREPEND(by_key, first, i); + assert_se(hashmap_replace(z->by_key, first->rr->key, first) >= 0); + } else { + r = hashmap_put(z->by_key, i->rr->key, i); + if (r < 0) + return r; + } + + first = hashmap_get(z->by_name, dns_resource_key_name(i->rr->key)); + if (first) { + LIST_PREPEND(by_name, first, i); + assert_se(hashmap_replace(z->by_name, dns_resource_key_name(first->rr->key), first) >= 0); + } else { + r = hashmap_put(z->by_name, dns_resource_key_name(i->rr->key), i); + if (r < 0) + return r; + } + + return 0; +} + +static int dns_zone_item_probe_start(DnsZoneItem *i) { + _cleanup_(dns_transaction_gcp) DnsTransaction *t = NULL; + int r; + + assert(i); + + if (i->probe_transaction) + return 0; + + t = dns_scope_find_transaction( + i->scope, + &DNS_RESOURCE_KEY_CONST(i->rr->key->class, DNS_TYPE_ANY, dns_resource_key_name(i->rr->key)), + SD_RESOLVED_NO_CACHE|SD_RESOLVED_NO_ZONE); + if (!t) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + + key = dns_resource_key_new(i->rr->key->class, DNS_TYPE_ANY, dns_resource_key_name(i->rr->key)); + if (!key) + return -ENOMEM; + + r = dns_transaction_new(&t, i->scope, key, NULL, SD_RESOLVED_NO_CACHE|SD_RESOLVED_NO_ZONE); + if (r < 0) + return r; + } + + r = set_ensure_allocated(&t->notify_zone_items_done, NULL); + if (r < 0) + return r; + + r = set_ensure_put(&t->notify_zone_items, NULL, i); + if (r < 0) + return r; + + t->probing = true; + i->probe_transaction = TAKE_PTR(t); + + if (i->probe_transaction->state == DNS_TRANSACTION_NULL) { + i->block_ready++; + r = dns_transaction_go(i->probe_transaction); + i->block_ready--; + + if (r < 0) { + dns_zone_item_probe_stop(i); + return r; + } + } + + dns_zone_item_notify(i); + return 0; +} + +int dns_zone_put(DnsZone *z, DnsScope *s, DnsResourceRecord *rr, bool probe) { + _cleanup_(dns_zone_item_freep) DnsZoneItem *i = NULL; + DnsZoneItem *existing; + int r; + + assert(z); + assert(s); + assert(rr); + + if (dns_class_is_pseudo(rr->key->class)) + return -EINVAL; + if (dns_type_is_pseudo(rr->key->type)) + return -EINVAL; + + existing = dns_zone_get(z, rr); + if (existing) + return 0; + + r = dns_zone_init(z); + if (r < 0) + return r; + + i = new(DnsZoneItem, 1); + if (!i) + return -ENOMEM; + + *i = (DnsZoneItem) { + .scope = s, + .rr = dns_resource_record_ref(rr), + .probing_enabled = probe, + }; + + r = dns_zone_link_item(z, i); + if (r < 0) + return r; + + if (probe) { + bool established = false; + + /* Check if there's already an RR with the same name + * established. If so, it has been probed already, and + * we don't need to probe again. */ + + LIST_FOREACH_OTHERS(by_name, j, i) + if (j->state == DNS_ZONE_ITEM_ESTABLISHED) + established = true; + + if (established) + i->state = DNS_ZONE_ITEM_ESTABLISHED; + else { + i->state = DNS_ZONE_ITEM_PROBING; + + r = dns_zone_item_probe_start(i); + if (r < 0) { + dns_zone_item_remove_and_free(z, i); + i = NULL; + return r; + } + } + } else + i->state = DNS_ZONE_ITEM_ESTABLISHED; + + i = NULL; + return 0; +} + +static int dns_zone_add_authenticated_answer(DnsAnswer *a, DnsZoneItem *i, int ifindex) { + DnsAnswerFlags flags; + + /* From RFC 6762, Section 10.2 + * "They (the rules about when to set the cache-flush bit) apply to + * startup announcements as described in Section 8.3, "Announcing", + * and to responses generated as a result of receiving query messages." + * So, set the cache-flush bit for mDNS answers except for DNS-SD + * service enumeration PTRs described in RFC 6763, Section 4.1. */ + if (i->scope->protocol == DNS_PROTOCOL_MDNS && + !dns_resource_key_is_dnssd_ptr(i->rr->key)) + flags = DNS_ANSWER_AUTHENTICATED|DNS_ANSWER_CACHE_FLUSH; + else + flags = DNS_ANSWER_AUTHENTICATED; + + return dns_answer_add(a, i->rr, ifindex, flags, NULL); +} + +int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **ret_answer, DnsAnswer **ret_soa, bool *ret_tentative) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; + unsigned n_answer = 0; + DnsZoneItem *first; + bool tentative = true, need_soa = false; + int r; + + /* Note that we don't actually need the ifindex for anything. However when it is passed we'll initialize the + * ifindex field in the answer with it */ + + assert(z); + assert(key); + assert(ret_answer); + + /* First iteration, count what we have */ + + if (key->type == DNS_TYPE_ANY || key->class == DNS_CLASS_ANY) { + bool found = false, added = false; + int k; + + /* If this is a generic match, then we have to + * go through the list by the name and look + * for everything manually */ + + first = hashmap_get(z->by_name, dns_resource_key_name(key)); + LIST_FOREACH(by_name, j, first) { + if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + found = true; + + k = dns_resource_key_match_rr(key, j->rr, NULL); + if (k < 0) + return k; + if (k > 0) { + n_answer++; + added = true; + } + + } + + if (found && !added) + need_soa = true; + + } else { + bool found = false; + + /* If this is a specific match, then look for + * the right key immediately */ + + first = hashmap_get(z->by_key, key); + LIST_FOREACH(by_key, j, first) { + if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + found = true; + n_answer++; + } + + if (!found) { + first = hashmap_get(z->by_name, dns_resource_key_name(key)); + LIST_FOREACH(by_name, j, first) { + if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + need_soa = true; + break; + } + } + } + + if (n_answer <= 0 && !need_soa) + goto return_empty; + + if (n_answer > 0) { + answer = dns_answer_new(n_answer); + if (!answer) + return -ENOMEM; + } + + if (need_soa) { + soa = dns_answer_new(1); + if (!soa) + return -ENOMEM; + } + + /* Second iteration, actually add the RRs to the answers */ + if (key->type == DNS_TYPE_ANY || key->class == DNS_CLASS_ANY) { + bool found = false, added = false; + int k; + + first = hashmap_get(z->by_name, dns_resource_key_name(key)); + LIST_FOREACH(by_name, j, first) { + if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + found = true; + + if (j->state != DNS_ZONE_ITEM_PROBING) + tentative = false; + + k = dns_resource_key_match_rr(key, j->rr, NULL); + if (k < 0) + return k; + if (k > 0) { + r = dns_zone_add_authenticated_answer(answer, j, ifindex); + if (r < 0) + return r; + + added = true; + } + } + + if (found && !added) { + r = dns_answer_add_soa(soa, dns_resource_key_name(key), LLMNR_DEFAULT_TTL, ifindex); + if (r < 0) + return r; + } + } else { + bool found = false; + + first = hashmap_get(z->by_key, key); + LIST_FOREACH(by_key, j, first) { + if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + found = true; + + if (j->state != DNS_ZONE_ITEM_PROBING) + tentative = false; + + r = dns_zone_add_authenticated_answer(answer, j, ifindex); + if (r < 0) + return r; + } + + if (!found) { + bool add_soa = false; + + first = hashmap_get(z->by_name, dns_resource_key_name(key)); + LIST_FOREACH(by_name, j, first) { + if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + if (j->state != DNS_ZONE_ITEM_PROBING) + tentative = false; + + add_soa = true; + } + + if (add_soa) { + r = dns_answer_add_soa(soa, dns_resource_key_name(key), LLMNR_DEFAULT_TTL, ifindex); + if (r < 0) + return r; + } + } + } + + /* If the caller sets ret_tentative to NULL, then use this as + * indication to not return tentative entries */ + + if (!ret_tentative && tentative) + goto return_empty; + + *ret_answer = TAKE_PTR(answer); + + if (ret_soa) + *ret_soa = TAKE_PTR(soa); + + if (ret_tentative) + *ret_tentative = tentative; + + return 1; + +return_empty: + *ret_answer = NULL; + + if (ret_soa) + *ret_soa = NULL; + + if (ret_tentative) + *ret_tentative = false; + + return 0; +} + +void dns_zone_item_conflict(DnsZoneItem *i) { + assert(i); + + if (!IN_SET(i->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_VERIFYING, DNS_ZONE_ITEM_ESTABLISHED)) + return; + + log_info("Detected conflict on %s", strna(dns_resource_record_to_string(i->rr))); + + dns_zone_item_probe_stop(i); + + /* Withdraw the conflict item */ + i->state = DNS_ZONE_ITEM_WITHDRAWN; + + (void) dnssd_signal_conflict(i->scope->manager, dns_resource_key_name(i->rr->key)); + + /* Maybe change the hostname */ + if (manager_is_own_hostname(i->scope->manager, dns_resource_key_name(i->rr->key)) > 0) + manager_next_hostname(i->scope->manager); +} + +void dns_zone_item_notify(DnsZoneItem *i) { + assert(i); + assert(i->probe_transaction); + + if (i->block_ready > 0) + return; + + if (IN_SET(i->probe_transaction->state, DNS_TRANSACTION_NULL, DNS_TRANSACTION_PENDING, DNS_TRANSACTION_VALIDATING)) + return; + + if (i->probe_transaction->state == DNS_TRANSACTION_SUCCESS) { + bool we_lost = false; + + /* The probe got a successful reply. If we so far + * weren't established we just give up. + * + * In LLMNR case if we already + * were established, and the peer has the + * lexicographically larger IP address we continue + * and defend it. */ + + if (!IN_SET(i->state, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) { + log_debug("Got a successful probe for not yet established RR, we lost."); + we_lost = true; + } else if (i->probe_transaction->scope->protocol == DNS_PROTOCOL_LLMNR) { + assert(i->probe_transaction->received); + we_lost = memcmp(&i->probe_transaction->received->sender, &i->probe_transaction->received->destination, FAMILY_ADDRESS_SIZE(i->probe_transaction->received->family)) < 0; + if (we_lost) + log_debug("Got a successful probe reply for an established RR, and we have a lexicographically larger IP address and thus lost."); + } + + if (we_lost) { + dns_zone_item_conflict(i); + return; + } + + log_debug("Got a successful probe reply, but peer has lexicographically lower IP address and thus lost."); + } + + log_debug("Record %s successfully probed.", strna(dns_resource_record_to_string(i->rr))); + + dns_zone_item_probe_stop(i); + i->state = DNS_ZONE_ITEM_ESTABLISHED; +} + +static int dns_zone_item_verify(DnsZoneItem *i) { + int r; + + assert(i); + + if (i->state != DNS_ZONE_ITEM_ESTABLISHED) + return 0; + + log_debug("Verifying RR %s", strna(dns_resource_record_to_string(i->rr))); + + i->state = DNS_ZONE_ITEM_VERIFYING; + r = dns_zone_item_probe_start(i); + if (r < 0) { + log_error_errno(r, "Failed to start probing for verifying RR: %m"); + i->state = DNS_ZONE_ITEM_ESTABLISHED; + return r; + } + + return 0; +} + +int dns_zone_check_conflicts(DnsZone *zone, DnsResourceRecord *rr) { + DnsZoneItem *first; + int c = 0; + + assert(zone); + assert(rr); + + /* This checks whether a response RR we received from somebody + * else is one that we actually thought was uniquely ours. If + * so, we'll verify our RRs. */ + + /* No conflict if we don't have the name at all. */ + first = hashmap_get(zone->by_name, dns_resource_key_name(rr->key)); + if (!first) + return 0; + + /* No conflict if we have the exact same RR */ + if (dns_zone_get(zone, rr)) + return 0; + + /* No conflict if it is DNS-SD RR used for service enumeration. */ + if (dns_resource_key_is_dnssd_ptr(rr->key)) + return 0; + + /* OK, somebody else has RRs for the same name. Yuck! Let's + * start probing again */ + + LIST_FOREACH(by_name, i, first) { + if (dns_resource_record_equal(i->rr, rr)) + continue; + + dns_zone_item_verify(i); + c++; + } + + return c; +} + +int dns_zone_verify_conflicts(DnsZone *zone, DnsResourceKey *key) { + DnsZoneItem *first; + int c = 0; + + assert(zone); + + /* Somebody else notified us about a possible conflict. Let's + * verify if that's true. */ + + first = hashmap_get(zone->by_name, dns_resource_key_name(key)); + if (!first) + return 0; + + LIST_FOREACH(by_name, i, first) { + dns_zone_item_verify(i); + c++; + } + + return c; +} + +void dns_zone_verify_all(DnsZone *zone) { + DnsZoneItem *i; + + assert(zone); + + HASHMAP_FOREACH(i, zone->by_key) + LIST_FOREACH(by_key, j, i) + dns_zone_item_verify(j); +} + +void dns_zone_dump(DnsZone *zone, FILE *f) { + DnsZoneItem *i; + + if (!zone) + return; + + if (!f) + f = stdout; + + HASHMAP_FOREACH(i, zone->by_key) + LIST_FOREACH(by_key, j, i) { + const char *t; + + t = dns_resource_record_to_string(j->rr); + if (!t) { + log_oom(); + continue; + } + + fputc('\t', f); + fputs(t, f); + fputc('\n', f); + } +} + +bool dns_zone_is_empty(DnsZone *zone) { + if (!zone) + return true; + + return hashmap_isempty(zone->by_key); +} + +bool dns_zone_contains_name(DnsZone *z, const char *name) { + DnsZoneItem *first; + + first = hashmap_get(z->by_name, name); + if (!first) + return false; + + LIST_FOREACH(by_name, i, first) { + if (!IN_SET(i->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + return true; + } + + return false; +} diff --git a/src/resolve/resolved-dns-zone.h b/src/resolve/resolved-dns-zone.h new file mode 100644 index 0000000..1f5a6e0 --- /dev/null +++ b/src/resolve/resolved-dns-zone.h @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "hashmap.h" + +typedef struct DnsZone { + Hashmap *by_key; + Hashmap *by_name; +} DnsZone; + +typedef struct DnsZoneItem DnsZoneItem; +typedef enum DnsZoneItemState DnsZoneItemState; + +#include "resolved-dns-answer.h" +#include "resolved-dns-question.h" +#include "resolved-dns-rr.h" +#include "resolved-dns-transaction.h" + +/* RFC 4795 Section 2.8. suggests a TTL of 30s by default */ +#define LLMNR_DEFAULT_TTL (30) + +/* RFC 6762 Section 10. suggests a TTL of 120s by default */ +#define MDNS_DEFAULT_TTL (120) + +enum DnsZoneItemState { + DNS_ZONE_ITEM_PROBING, + DNS_ZONE_ITEM_ESTABLISHED, + DNS_ZONE_ITEM_VERIFYING, + DNS_ZONE_ITEM_WITHDRAWN, +}; + +struct DnsZoneItem { + DnsScope *scope; + DnsResourceRecord *rr; + + DnsZoneItemState state; + + unsigned block_ready; + + bool probing_enabled; + + LIST_FIELDS(DnsZoneItem, by_key); + LIST_FIELDS(DnsZoneItem, by_name); + + DnsTransaction *probe_transaction; +}; + +void dns_zone_flush(DnsZone *z); + +int dns_zone_put(DnsZone *z, DnsScope *s, DnsResourceRecord *rr, bool probe); +DnsZoneItem* dns_zone_get(DnsZone *z, DnsResourceRecord *rr); +void dns_zone_remove_rr(DnsZone *z, DnsResourceRecord *rr); +int dns_zone_remove_rrs_by_key(DnsZone *z, DnsResourceKey *key); + +int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **answer, DnsAnswer **soa, bool *tentative); + +void dns_zone_item_conflict(DnsZoneItem *i); +void dns_zone_item_notify(DnsZoneItem *i); + +int dns_zone_check_conflicts(DnsZone *zone, DnsResourceRecord *rr); +int dns_zone_verify_conflicts(DnsZone *zone, DnsResourceKey *key); + +void dns_zone_verify_all(DnsZone *zone); + +void dns_zone_item_probe_stop(DnsZoneItem *i); + +void dns_zone_dump(DnsZone *zone, FILE *f); +bool dns_zone_is_empty(DnsZone *zone); +bool dns_zone_contains_name(DnsZone *z, const char *name); diff --git a/src/resolve/resolved-dnssd-bus.c b/src/resolve/resolved-dnssd-bus.c new file mode 100644 index 0000000..0f0d478 --- /dev/null +++ b/src/resolve/resolved-dnssd-bus.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bus-polkit.h" +#include "missing_capability.h" +#include "resolved-dnssd-bus.h" +#include "resolved-dnssd.h" +#include "resolved-link.h" +#include "resolved-manager.h" +#include "strv.h" +#include "user-util.h" + +int bus_dnssd_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error) { + DnssdService *s = ASSERT_PTR(userdata); + Manager *m; + Link *l; + int r; + + assert(message); + + m = s->manager; + + r = bus_verify_polkit_async(message, CAP_SYS_ADMIN, + "org.freedesktop.resolve1.unregister-service", + NULL, false, s->originator, + &m->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + HASHMAP_FOREACH(l, m->links) { + if (l->mdns_ipv4_scope) { + r = dns_scope_announce(l->mdns_ipv4_scope, true); + if (r < 0) + log_warning_errno(r, "Failed to send goodbye messages in IPv4 scope: %m"); + + dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, s->ptr_rr); + dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, s->srv_rr); + LIST_FOREACH(items, txt_data, s->txt_data_items) + dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, txt_data->rr); + } + + if (l->mdns_ipv6_scope) { + r = dns_scope_announce(l->mdns_ipv6_scope, true); + if (r < 0) + log_warning_errno(r, "Failed to send goodbye messages in IPv6 scope: %m"); + + dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, s->ptr_rr); + dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, s->srv_rr); + LIST_FOREACH(items, txt_data, s->txt_data_items) + dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, txt_data->rr); + } + } + + dnssd_service_free(s); + + manager_refresh_rrs(m); + + return sd_bus_reply_method_return(message, NULL); +} + +static int dnssd_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { + _cleanup_free_ char *name = NULL; + Manager *m = ASSERT_PTR(userdata); + DnssdService *service; + int r; + + assert(bus); + assert(path); + assert(interface); + assert(found); + + r = sd_bus_path_decode(path, "/org/freedesktop/resolve1/dnssd", &name); + if (r <= 0) + return 0; + + service = hashmap_get(m->dnssd_services, name); + if (!service) + return 0; + + *found = service; + return 1; +} + +static int dnssd_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) { + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + DnssdService *service; + unsigned c = 0; + int r; + + assert(bus); + assert(path); + assert(nodes); + + l = new0(char*, hashmap_size(m->dnssd_services) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(service, m->dnssd_services) { + char *p; + + r = sd_bus_path_encode("/org/freedesktop/resolve1/dnssd", service->name, &p); + if (r < 0) + return r; + + l[c++] = p; + } + + l[c] = NULL; + *nodes = TAKE_PTR(l); + + return 1; +} + +static const sd_bus_vtable dnssd_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_METHOD("Unregister", NULL, NULL, bus_dnssd_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_SIGNAL("Conflicted", NULL, 0), + + SD_BUS_VTABLE_END +}; + +const BusObjectImplementation dnssd_object = { + "/org/freedesktop/resolve1/dnssd", + "org.freedesktop.resolve1.DnssdService", + .fallback_vtables = BUS_FALLBACK_VTABLES({dnssd_vtable, dnssd_object_find}), + .node_enumerator = dnssd_node_enumerator, +}; diff --git a/src/resolve/resolved-dnssd-bus.h b/src/resolve/resolved-dnssd-bus.h new file mode 100644 index 0000000..f396e23 --- /dev/null +++ b/src/resolve/resolved-dnssd-bus.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +#include "sd-bus.h" + +#include "bus-object.h" + +extern const BusObjectImplementation dnssd_object; + +int bus_dnssd_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error); diff --git a/src/resolve/resolved-dnssd-gperf.gperf b/src/resolve/resolved-dnssd-gperf.gperf new file mode 100644 index 0000000..f10eae3 --- /dev/null +++ b/src/resolve/resolved-dnssd-gperf.gperf @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +%{ +#include <stddef.h> +#include "conf-parser.h" +#include "resolved-conf.h" +#include "resolved-dnssd.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name resolved_dnssd_gperf_hash +%define lookup-function-name resolved_dnssd_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Service.Name, config_parse_dnssd_service_name, 0, 0 +Service.Type, config_parse_dnssd_service_type, 0, 0 +Service.Port, config_parse_ip_port, 0, offsetof(DnssdService, port) +Service.Priority, config_parse_uint16, 0, offsetof(DnssdService, priority) +Service.Weight, config_parse_uint16, 0, offsetof(DnssdService, weight) +Service.TxtText, config_parse_dnssd_txt, DNS_TXT_ITEM_TEXT, 0 +Service.TxtData, config_parse_dnssd_txt, DNS_TXT_ITEM_DATA, 0 diff --git a/src/resolve/resolved-dnssd.c b/src/resolve/resolved-dnssd.c new file mode 100644 index 0000000..464892a --- /dev/null +++ b/src/resolve/resolved-dnssd.c @@ -0,0 +1,362 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "conf-files.h" +#include "conf-parser.h" +#include "def.h" +#include "resolved-dnssd.h" +#include "resolved-dns-rr.h" +#include "resolved-manager.h" +#include "resolved-conf.h" +#include "specifier.h" +#include "strv.h" + +#define DNSSD_SERVICE_DIRS ((const char* const*) CONF_PATHS_STRV("systemd/dnssd")) + +DnssdTxtData *dnssd_txtdata_free(DnssdTxtData *txt_data) { + if (!txt_data) + return NULL; + + dns_resource_record_unref(txt_data->rr); + dns_txt_item_free_all(txt_data->txts); + + return mfree(txt_data); +} + +DnssdTxtData *dnssd_txtdata_free_all(DnssdTxtData *txt_data) { + DnssdTxtData *next; + + if (!txt_data) + return NULL; + + next = txt_data->items_next; + + dnssd_txtdata_free(txt_data); + + return dnssd_txtdata_free_all(next); +} + +DnssdService *dnssd_service_free(DnssdService *service) { + if (!service) + return NULL; + + if (service->manager) + hashmap_remove(service->manager->dnssd_services, service->name); + + dns_resource_record_unref(service->ptr_rr); + dns_resource_record_unref(service->srv_rr); + + dnssd_txtdata_free_all(service->txt_data_items); + + free(service->filename); + free(service->name); + free(service->type); + free(service->name_template); + + return mfree(service); +} + +static int dnssd_service_load(Manager *manager, const char *filename) { + _cleanup_(dnssd_service_freep) DnssdService *service = NULL; + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + char *d; + const char *dropin_dirname; + int r; + + assert(manager); + assert(filename); + + service = new0(DnssdService, 1); + if (!service) + return log_oom(); + + service->filename = strdup(filename); + if (!service->filename) + return log_oom(); + + service->name = strdup(basename(filename)); + if (!service->name) + return log_oom(); + + d = endswith(service->name, ".dnssd"); + if (!d) + return -EINVAL; + + assert(streq(d, ".dnssd")); + + *d = '\0'; + + dropin_dirname = strjoina(service->name, ".dnssd.d"); + + r = config_parse_many( + STRV_MAKE_CONST(filename), DNSSD_SERVICE_DIRS, dropin_dirname, + "Service\0", + config_item_perf_lookup, resolved_dnssd_gperf_lookup, + CONFIG_PARSE_WARN, + service, + NULL, + NULL); + if (r < 0) + return r; + + if (!service->name_template) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s doesn't define service instance name", + service->name); + + if (!service->type) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "%s doesn't define service type", + service->name); + + if (!service->txt_data_items) { + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + r = dns_txt_item_new_empty(&txt_data->txts); + if (r < 0) + return r; + + LIST_PREPEND(items, service->txt_data_items, txt_data); + TAKE_PTR(txt_data); + } + + r = hashmap_ensure_put(&manager->dnssd_services, &string_hash_ops, service->name, service); + if (r < 0) + return r; + + service->manager = manager; + + r = dnssd_update_rrs(service); + if (r < 0) + return r; + + TAKE_PTR(service); + + return 0; +} + +static int specifier_dnssd_hostname(char specifier, const void *data, const char *root, const void *userdata, char **ret) { + const Manager *m = ASSERT_PTR(userdata); + char *n; + + assert(m->llmnr_hostname); + + n = strdup(m->llmnr_hostname); + if (!n) + return -ENOMEM; + + *ret = n; + return 0; +} + +int dnssd_render_instance_name(Manager *m, DnssdService *s, char **ret) { + static const Specifier specifier_table[] = { + { 'a', specifier_architecture, NULL }, + { 'b', specifier_boot_id, NULL }, + { 'B', specifier_os_build_id, NULL }, + { 'H', specifier_dnssd_hostname, NULL }, + { 'm', specifier_machine_id, NULL }, + { 'o', specifier_os_id, NULL }, + { 'v', specifier_kernel_release, NULL }, + { 'w', specifier_os_version_id, NULL }, + { 'W', specifier_os_variant_id, NULL }, + {} + }; + _cleanup_free_ char *name = NULL; + int r; + + assert(m); + assert(s); + assert(s->name_template); + + r = specifier_printf(s->name_template, DNS_LABEL_MAX, specifier_table, NULL, m, &name); + if (r < 0) + return log_debug_errno(r, "Failed to replace specifiers: %m"); + + if (!dns_service_name_is_valid(name)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Service instance name '%s' is invalid.", + name); + + if (ret) + *ret = TAKE_PTR(name); + + return 0; +} + +int dnssd_load(Manager *manager) { + _cleanup_strv_free_ char **files = NULL; + int r; + + assert(manager); + + if (manager->mdns_support != RESOLVE_SUPPORT_YES) + return 0; + + r = conf_files_list_strv(&files, ".dnssd", NULL, 0, DNSSD_SERVICE_DIRS); + if (r < 0) + return log_error_errno(r, "Failed to enumerate .dnssd files: %m"); + + STRV_FOREACH_BACKWARDS(f, files) { + r = dnssd_service_load(manager, *f); + if (r < 0) + log_warning_errno(r, "Failed to load '%s': %m", *f); + } + + return 0; +} + +int dnssd_update_rrs(DnssdService *s) { + _cleanup_free_ char *n = NULL, *service_name = NULL, *full_name = NULL; + int r; + + assert(s); + assert(s->txt_data_items); + assert(s->manager); + + s->ptr_rr = dns_resource_record_unref(s->ptr_rr); + s->srv_rr = dns_resource_record_unref(s->srv_rr); + LIST_FOREACH(items, txt_data, s->txt_data_items) + txt_data->rr = dns_resource_record_unref(txt_data->rr); + + r = dnssd_render_instance_name(s->manager, s, &n); + if (r < 0) + return r; + + r = dns_name_concat(s->type, "local", 0, &service_name); + if (r < 0) + return r; + r = dns_name_concat(n, service_name, 0, &full_name); + if (r < 0) + return r; + + LIST_FOREACH(items, txt_data, s->txt_data_items) { + txt_data->rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_TXT, + full_name); + if (!txt_data->rr) + goto oom; + + txt_data->rr->ttl = MDNS_DEFAULT_TTL; + txt_data->rr->txt.items = dns_txt_item_copy(txt_data->txts); + if (!txt_data->rr->txt.items) + goto oom; + } + + s->ptr_rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR, + service_name); + if (!s->ptr_rr) + goto oom; + + s->ptr_rr->ttl = MDNS_DEFAULT_TTL; + s->ptr_rr->ptr.name = strdup(full_name); + if (!s->ptr_rr->ptr.name) + goto oom; + + s->srv_rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_SRV, + full_name); + if (!s->srv_rr) + goto oom; + + s->srv_rr->ttl = MDNS_DEFAULT_TTL; + s->srv_rr->srv.priority = s->priority; + s->srv_rr->srv.weight = s->weight; + s->srv_rr->srv.port = s->port; + s->srv_rr->srv.name = strdup(s->manager->mdns_hostname); + if (!s->srv_rr->srv.name) + goto oom; + + return 0; + +oom: + LIST_FOREACH(items, txt_data, s->txt_data_items) + txt_data->rr = dns_resource_record_unref(txt_data->rr); + s->ptr_rr = dns_resource_record_unref(s->ptr_rr); + s->srv_rr = dns_resource_record_unref(s->srv_rr); + return -ENOMEM; +} + +int dnssd_txt_item_new_from_string(const char *key, const char *value, DnsTxtItem **ret_item) { + size_t length; + DnsTxtItem *i; + + length = strlen(key); + + if (!isempty(value)) + length += strlen(value) + 1; /* length of value plus '=' */ + + i = malloc0(offsetof(DnsTxtItem, data) + length + 1); /* for safety reasons we add an extra NUL byte */ + if (!i) + return -ENOMEM; + + memcpy(i->data, key, strlen(key)); + if (!isempty(value)) { + memcpy(i->data + strlen(key), "=", 1); + memcpy(i->data + strlen(key) + 1, value, strlen(value)); + } + i->length = length; + + *ret_item = TAKE_PTR(i); + + return 0; +} + +int dnssd_txt_item_new_from_data(const char *key, const void *data, const size_t size, DnsTxtItem **ret_item) { + size_t length; + DnsTxtItem *i; + + length = strlen(key); + + if (size > 0) + length += size + 1; /* size of date plus '=' */ + + i = malloc0(offsetof(DnsTxtItem, data) + length + 1); /* for safety reasons we add an extra NUL byte */ + if (!i) + return -ENOMEM; + + memcpy(i->data, key, strlen(key)); + if (size > 0) { + memcpy(i->data + strlen(key), "=", 1); + memcpy(i->data + strlen(key) + 1, data, size); + } + i->length = length; + + *ret_item = TAKE_PTR(i); + + return 0; +} + +int dnssd_signal_conflict(Manager *manager, const char *name) { + DnssdService *s; + int r; + + if (sd_bus_is_ready(manager->bus) <= 0) + return 0; + + HASHMAP_FOREACH(s, manager->dnssd_services) { + if (s->withdrawn) + continue; + + if (dns_name_equal(dns_resource_key_name(s->srv_rr->key), name)) { + _cleanup_free_ char *path = NULL; + + s->withdrawn = true; + + r = sd_bus_path_encode("/org/freedesktop/resolve1/dnssd", s->name, &path); + if (r < 0) + return log_error_errno(r, "Can't get D-BUS object path: %m"); + + r = sd_bus_emit_signal(manager->bus, + path, + "org.freedesktop.resolve1.DnssdService", + "Conflicted", + NULL); + if (r < 0) + return log_error_errno(r, "Cannot emit signal: %m"); + + break; + } + } + + return 0; +} diff --git a/src/resolve/resolved-dnssd.h b/src/resolve/resolved-dnssd.h new file mode 100644 index 0000000..e978a0d --- /dev/null +++ b/src/resolve/resolved-dnssd.h @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#pragma once + +#include "list.h" + +typedef struct DnssdService DnssdService; +typedef struct DnssdTxtData DnssdTxtData; + +typedef struct Manager Manager; +typedef struct DnsResourceRecord DnsResourceRecord; +typedef struct DnsTxtItem DnsTxtItem; + +enum { + DNS_TXT_ITEM_TEXT, + DNS_TXT_ITEM_DATA, +}; + +struct DnssdTxtData { + DnsResourceRecord *rr; + + LIST_HEAD(DnsTxtItem, txts); + + LIST_FIELDS(DnssdTxtData, items); +}; + +struct DnssdService { + char *filename; + char *name; + char *name_template; + char *type; + uint16_t port; + uint16_t priority; + uint16_t weight; + + DnsResourceRecord *ptr_rr; + DnsResourceRecord *srv_rr; + + /* Section 6.8 of RFC 6763 allows having service + * instances with multiple TXT resource records. */ + LIST_HEAD(DnssdTxtData, txt_data_items); + + Manager *manager; + + bool withdrawn:1; + uid_t originator; +}; + +DnssdService *dnssd_service_free(DnssdService *service); +DnssdTxtData *dnssd_txtdata_free(DnssdTxtData *txt_data); +DnssdTxtData *dnssd_txtdata_free_all(DnssdTxtData *txt_data); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnssdService*, dnssd_service_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(DnssdTxtData*, dnssd_txtdata_free); + +int dnssd_render_instance_name(Manager *m, DnssdService *s, char **ret); +int dnssd_load(Manager *manager); +int dnssd_txt_item_new_from_string(const char *key, const char *value, DnsTxtItem **ret_item); +int dnssd_txt_item_new_from_data(const char *key, const void *value, const size_t size, DnsTxtItem **ret_item); +int dnssd_update_rrs(DnssdService *s); +int dnssd_signal_conflict(Manager *manager, const char *name); diff --git a/src/resolve/resolved-dnstls-gnutls.c b/src/resolve/resolved-dnstls-gnutls.c new file mode 100644 index 0000000..acdad6f --- /dev/null +++ b/src/resolve/resolved-dnstls-gnutls.c @@ -0,0 +1,253 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if !ENABLE_DNS_OVER_TLS || !DNS_OVER_TLS_USE_GNUTLS +#error This source file requires DNS-over-TLS to be enabled and GnuTLS to be available. +#endif + +#include <gnutls/socket.h> + +#include "io-util.h" +#include "resolved-dns-stream.h" +#include "resolved-dnstls.h" +#include "resolved-manager.h" + +#define TLS_PROTOCOL_PRIORITY "NORMAL:-VERS-ALL:+VERS-TLS1.3:+VERS-TLS1.2" +DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(gnutls_session_t, gnutls_deinit, NULL); + +static ssize_t dnstls_stream_vec_push(gnutls_transport_ptr_t p, const giovec_t *iov, int iovcnt) { + int r; + + assert(p); + + r = dns_stream_writev((DnsStream*) p, (const struct iovec*) iov, iovcnt, DNS_STREAM_WRITE_TLS_DATA); + if (r < 0) { + errno = -r; + return -1; + } + + return r; +} + +int dnstls_stream_connect_tls(DnsStream *stream, DnsServer *server) { + _cleanup_(gnutls_deinitp) gnutls_session_t gs = NULL; + int r; + + assert(stream); + assert(server); + + r = gnutls_init(&gs, GNUTLS_CLIENT | GNUTLS_ENABLE_FALSE_START | GNUTLS_NONBLOCK); + if (r < 0) + return r; + + /* As DNS-over-TLS is a recent protocol, older TLS versions can be disabled */ + r = gnutls_priority_set_direct(gs, TLS_PROTOCOL_PRIORITY, NULL); + if (r < 0) + return r; + + r = gnutls_credentials_set(gs, GNUTLS_CRD_CERTIFICATE, stream->manager->dnstls_data.cert_cred); + if (r < 0) + return r; + + if (server->dnstls_data.session_data.size > 0) { + gnutls_session_set_data(gs, server->dnstls_data.session_data.data, server->dnstls_data.session_data.size); + + // Clear old session ticket + gnutls_free(server->dnstls_data.session_data.data); + server->dnstls_data.session_data.data = NULL; + server->dnstls_data.session_data.size = 0; + } + + if (server->manager->dns_over_tls_mode == DNS_OVER_TLS_YES) { + if (server->server_name) + gnutls_session_set_verify_cert(gs, server->server_name, 0); + else { + stream->dnstls_data.validation.type = GNUTLS_DT_IP_ADDRESS; + if (server->family == AF_INET) { + stream->dnstls_data.validation.data = (unsigned char*) &server->address.in.s_addr; + stream->dnstls_data.validation.size = 4; + } else { + stream->dnstls_data.validation.data = server->address.in6.s6_addr; + stream->dnstls_data.validation.size = 16; + } + gnutls_session_set_verify_cert2(gs, &stream->dnstls_data.validation, 1, 0); + } + } + + if (server->server_name) { + r = gnutls_server_name_set(gs, GNUTLS_NAME_DNS, server->server_name, strlen(server->server_name)); + if (r < 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set server name: %s", gnutls_strerror(r)); + } + + gnutls_handshake_set_timeout(gs, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT); + + gnutls_transport_set_ptr2(gs, (gnutls_transport_ptr_t) (long) stream->fd, stream); + gnutls_transport_set_vec_push_function(gs, &dnstls_stream_vec_push); + + stream->encrypted = true; + stream->dnstls_data.handshake = gnutls_handshake(gs); + if (stream->dnstls_data.handshake < 0 && gnutls_error_is_fatal(stream->dnstls_data.handshake)) + return -ECONNREFUSED; + + stream->dnstls_data.session = TAKE_PTR(gs); + + return 0; +} + +void dnstls_stream_free(DnsStream *stream) { + assert(stream); + assert(stream->encrypted); + + if (stream->dnstls_data.session) + gnutls_deinit(stream->dnstls_data.session); +} + +int dnstls_stream_on_io(DnsStream *stream, uint32_t revents) { + int r; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.session); + + if (stream->dnstls_data.shutdown) { + r = gnutls_bye(stream->dnstls_data.session, GNUTLS_SHUT_RDWR); + if (r == GNUTLS_E_AGAIN) { + stream->dnstls_events = gnutls_record_get_direction(stream->dnstls_data.session) == 1 ? EPOLLOUT : EPOLLIN; + return -EAGAIN; + } else if (r < 0) + log_debug("Failed to invoke gnutls_bye: %s", gnutls_strerror(r)); + + stream->dnstls_events = 0; + stream->dnstls_data.shutdown = false; + dns_stream_unref(stream); + return DNSTLS_STREAM_CLOSED; + } else if (stream->dnstls_data.handshake < 0) { + stream->dnstls_data.handshake = gnutls_handshake(stream->dnstls_data.session); + if (stream->dnstls_data.handshake == GNUTLS_E_AGAIN) { + stream->dnstls_events = gnutls_record_get_direction(stream->dnstls_data.session) == 1 ? EPOLLOUT : EPOLLIN; + return -EAGAIN; + } else if (stream->dnstls_data.handshake < 0) { + log_debug("Failed to invoke gnutls_handshake: %s", gnutls_strerror(stream->dnstls_data.handshake)); + if (gnutls_error_is_fatal(stream->dnstls_data.handshake)) + return -ECONNREFUSED; + } + + stream->dnstls_events = 0; + } + + return 0; +} + +int dnstls_stream_shutdown(DnsStream *stream, int error) { + int r; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.session); + + /* Store TLS Ticket for faster successive TLS handshakes */ + if (stream->server && stream->server->dnstls_data.session_data.size == 0 && stream->dnstls_data.handshake == GNUTLS_E_SUCCESS) + gnutls_session_get_data2(stream->dnstls_data.session, &stream->server->dnstls_data.session_data); + + if (IN_SET(error, ETIMEDOUT, 0)) { + r = gnutls_bye(stream->dnstls_data.session, GNUTLS_SHUT_RDWR); + if (r == GNUTLS_E_AGAIN) { + if (!stream->dnstls_data.shutdown) { + stream->dnstls_data.shutdown = true; + dns_stream_ref(stream); + return -EAGAIN; + } + } else if (r < 0) + log_debug("Failed to invoke gnutls_bye: %s", gnutls_strerror(r)); + } + + return 0; +} + +ssize_t dnstls_stream_writev(DnsStream *stream, const struct iovec *iov, size_t iovcnt) { + ssize_t ss; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.session); + assert(iov); + assert(IOVEC_TOTAL_SIZE(iov, iovcnt) > 0); + + gnutls_record_cork(stream->dnstls_data.session); + + for (size_t i = 0; i < iovcnt; i++) { + ss = gnutls_record_send( + stream->dnstls_data.session, + iov[i].iov_base, iov[i].iov_len); + if (ss < 0) + break; + } + + ss = gnutls_record_uncork(stream->dnstls_data.session, 0); + if (ss < 0) + switch (ss) { + case GNUTLS_E_INTERRUPTED: + return -EINTR; + case GNUTLS_E_AGAIN: + return -EAGAIN; + default: + return log_debug_errno(SYNTHETIC_ERRNO(EPIPE), + "Failed to invoke gnutls_record_send: %s", + gnutls_strerror(ss)); + } + + return ss; +} + +ssize_t dnstls_stream_read(DnsStream *stream, void *buf, size_t count) { + ssize_t ss; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.session); + assert(buf); + + ss = gnutls_record_recv(stream->dnstls_data.session, buf, count); + if (ss < 0) + switch (ss) { + case GNUTLS_E_INTERRUPTED: + return -EINTR; + case GNUTLS_E_AGAIN: + return -EAGAIN; + default: + return log_debug_errno(SYNTHETIC_ERRNO(EPIPE), + "Failed to invoke gnutls_record_recv: %s", + gnutls_strerror(ss)); + } + + return ss; +} + +void dnstls_server_free(DnsServer *server) { + assert(server); + + if (server->dnstls_data.session_data.data) + gnutls_free(server->dnstls_data.session_data.data); +} + +int dnstls_manager_init(Manager *manager) { + int r; + assert(manager); + + r = gnutls_certificate_allocate_credentials(&manager->dnstls_data.cert_cred); + if (r < 0) + return -ENOMEM; + + r = gnutls_certificate_set_x509_system_trust(manager->dnstls_data.cert_cred); + if (r < 0) + log_warning("Failed to load system trust store: %s", gnutls_strerror(r)); + + return 0; +} + +void dnstls_manager_free(Manager *manager) { + assert(manager); + + if (manager->dnstls_data.cert_cred) + gnutls_certificate_free_credentials(manager->dnstls_data.cert_cred); +} diff --git a/src/resolve/resolved-dnstls-gnutls.h b/src/resolve/resolved-dnstls-gnutls.h new file mode 100644 index 0000000..dc1255f --- /dev/null +++ b/src/resolve/resolved-dnstls-gnutls.h @@ -0,0 +1,24 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#if !ENABLE_DNS_OVER_TLS || !DNS_OVER_TLS_USE_GNUTLS +#error This source file requires DNS-over-TLS to be enabled and GnuTLS to be available. +#endif + +#include <gnutls/gnutls.h> +#include <stdbool.h> + +struct DnsTlsManagerData { + gnutls_certificate_credentials_t cert_cred; +}; + +struct DnsTlsServerData { + gnutls_datum_t session_data; +}; + +struct DnsTlsStreamData { + gnutls_session_t session; + gnutls_typed_vdata_st validation; + int handshake; + bool shutdown; +}; diff --git a/src/resolve/resolved-dnstls-openssl.c b/src/resolve/resolved-dnstls-openssl.c new file mode 100644 index 0000000..4a0132a --- /dev/null +++ b/src/resolve/resolved-dnstls-openssl.c @@ -0,0 +1,422 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#if !ENABLE_DNS_OVER_TLS || !DNS_OVER_TLS_USE_OPENSSL +#error This source file requires DNS-over-TLS to be enabled and OpenSSL to be available. +#endif + +#include <openssl/bio.h> +#include <openssl/err.h> +#include <openssl/x509v3.h> + +#include "io-util.h" +#include "openssl-util.h" +#include "resolved-dns-stream.h" +#include "resolved-dnstls.h" +#include "resolved-manager.h" + +static char *dnstls_error_string(int ssl_error, char *buf, size_t count) { + assert(buf || count == 0); + if (ssl_error == SSL_ERROR_SSL) + ERR_error_string_n(ERR_get_error(), buf, count); + else + snprintf(buf, count, "SSL_get_error()=%d", ssl_error); + return buf; +} + +#define DNSTLS_ERROR_BUFSIZE 256 +#define DNSTLS_ERROR_STRING(error) \ + dnstls_error_string((error), (char[DNSTLS_ERROR_BUFSIZE]){}, DNSTLS_ERROR_BUFSIZE) + +static int dnstls_flush_write_buffer(DnsStream *stream) { + ssize_t ss; + + assert(stream); + assert(stream->encrypted); + + if (stream->dnstls_data.buffer_offset < stream->dnstls_data.write_buffer->length) { + assert(stream->dnstls_data.write_buffer->data); + + struct iovec iov[1]; + iov[0] = IOVEC_MAKE(stream->dnstls_data.write_buffer->data + stream->dnstls_data.buffer_offset, + stream->dnstls_data.write_buffer->length - stream->dnstls_data.buffer_offset); + ss = dns_stream_writev(stream, iov, 1, DNS_STREAM_WRITE_TLS_DATA); + if (ss < 0) { + if (ss == -EAGAIN) + stream->dnstls_events |= EPOLLOUT; + + return ss; + } else { + stream->dnstls_data.buffer_offset += ss; + + if (stream->dnstls_data.buffer_offset < stream->dnstls_data.write_buffer->length) { + stream->dnstls_events |= EPOLLOUT; + return -EAGAIN; + } else { + BIO_reset(SSL_get_wbio(stream->dnstls_data.ssl)); + stream->dnstls_data.buffer_offset = 0; + } + } + } + + return 0; +} + +int dnstls_stream_connect_tls(DnsStream *stream, DnsServer *server) { + _cleanup_(BIO_freep) BIO *rb = NULL, *wb = NULL; + _cleanup_(SSL_freep) SSL *s = NULL; + int error, r; + + assert(stream); + assert(stream->manager); + assert(server); + + rb = BIO_new_socket(stream->fd, 0); + if (!rb) + return -ENOMEM; + + wb = BIO_new(BIO_s_mem()); + if (!wb) + return -ENOMEM; + + BIO_get_mem_ptr(wb, &stream->dnstls_data.write_buffer); + stream->dnstls_data.buffer_offset = 0; + + s = SSL_new(stream->manager->dnstls_data.ctx); + if (!s) + return -ENOMEM; + + SSL_set_connect_state(s); + r = SSL_set_session(s, server->dnstls_data.session); + if (r == 0) + return -EIO; + SSL_set_bio(s, TAKE_PTR(rb), TAKE_PTR(wb)); + + if (server->manager->dns_over_tls_mode == DNS_OVER_TLS_YES) { + X509_VERIFY_PARAM *v; + + SSL_set_verify(s, SSL_VERIFY_PEER, NULL); + v = SSL_get0_param(s); + if (server->server_name) { + X509_VERIFY_PARAM_set_hostflags(v, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + if (X509_VERIFY_PARAM_set1_host(v, server->server_name, 0) == 0) + return -ECONNREFUSED; + } else { + const unsigned char *ip; + ip = server->family == AF_INET ? (const unsigned char*) &server->address.in.s_addr : server->address.in6.s6_addr; + if (X509_VERIFY_PARAM_set1_ip(v, ip, FAMILY_ADDRESS_SIZE(server->family)) == 0) + return -ECONNREFUSED; + } + } + + if (server->server_name) { + r = SSL_set_tlsext_host_name(s, server->server_name); + if (r <= 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Failed to set server name: %s", DNSTLS_ERROR_STRING(SSL_ERROR_SSL)); + } + + ERR_clear_error(); + stream->dnstls_data.handshake = SSL_do_handshake(s); + if (stream->dnstls_data.handshake <= 0) { + error = SSL_get_error(s, stream->dnstls_data.handshake); + if (!IN_SET(error, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE)) + return log_debug_errno(SYNTHETIC_ERRNO(ECONNREFUSED), + "Failed to invoke SSL_do_handshake: %s", DNSTLS_ERROR_STRING(error)); + } + + stream->encrypted = true; + stream->dnstls_data.ssl = TAKE_PTR(s); + + r = dnstls_flush_write_buffer(stream); + if (r < 0 && r != -EAGAIN) { + SSL_free(TAKE_PTR(stream->dnstls_data.ssl)); + return r; + } + + return 0; +} + +void dnstls_stream_free(DnsStream *stream) { + assert(stream); + assert(stream->encrypted); + + if (stream->dnstls_data.ssl) + SSL_free(stream->dnstls_data.ssl); +} + +int dnstls_stream_on_io(DnsStream *stream, uint32_t revents) { + int error, r; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.ssl); + + /* Flush write buffer when requested by OpenSSL */ + if ((revents & EPOLLOUT) && (stream->dnstls_events & EPOLLOUT)) { + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + } + + if (stream->dnstls_data.shutdown) { + ERR_clear_error(); + r = SSL_shutdown(stream->dnstls_data.ssl); + if (r == 0) { + stream->dnstls_events = 0; + + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + + return -EAGAIN; + } else if (r < 0) { + error = SSL_get_error(stream->dnstls_data.ssl, r); + if (IN_SET(error, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE)) { + stream->dnstls_events = error == SSL_ERROR_WANT_READ ? EPOLLIN : EPOLLOUT; + + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + + return -EAGAIN; + } else if (error == SSL_ERROR_SYSCALL) { + if (errno > 0) + log_debug_errno(errno, "Failed to invoke SSL_shutdown, ignoring: %m"); + } else + log_debug("Failed to invoke SSL_shutdown, ignoring: %s", DNSTLS_ERROR_STRING(error)); + } + + stream->dnstls_events = 0; + stream->dnstls_data.shutdown = false; + + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + + dns_stream_unref(stream); + return DNSTLS_STREAM_CLOSED; + } else if (stream->dnstls_data.handshake <= 0) { + ERR_clear_error(); + stream->dnstls_data.handshake = SSL_do_handshake(stream->dnstls_data.ssl); + if (stream->dnstls_data.handshake <= 0) { + error = SSL_get_error(stream->dnstls_data.ssl, stream->dnstls_data.handshake); + if (IN_SET(error, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE)) { + stream->dnstls_events = error == SSL_ERROR_WANT_READ ? EPOLLIN : EPOLLOUT; + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + + return -EAGAIN; + } else + return log_debug_errno(SYNTHETIC_ERRNO(ECONNREFUSED), + "Failed to invoke SSL_do_handshake: %s", + DNSTLS_ERROR_STRING(error)); + } + + stream->dnstls_events = 0; + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + } + + return 0; +} + +int dnstls_stream_shutdown(DnsStream *stream, int error) { + int ssl_error, r; + SSL_SESSION *s; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.ssl); + + if (stream->server) { + s = SSL_get1_session(stream->dnstls_data.ssl); + if (s) { + if (stream->server->dnstls_data.session) + SSL_SESSION_free(stream->server->dnstls_data.session); + + stream->server->dnstls_data.session = s; + } + } + + if (error == ETIMEDOUT) { + ERR_clear_error(); + r = SSL_shutdown(stream->dnstls_data.ssl); + if (r == 0) { + if (!stream->dnstls_data.shutdown) { + stream->dnstls_data.shutdown = true; + dns_stream_ref(stream); + } + + stream->dnstls_events = 0; + + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + + return -EAGAIN; + } else if (r < 0) { + ssl_error = SSL_get_error(stream->dnstls_data.ssl, r); + if (IN_SET(ssl_error, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE)) { + stream->dnstls_events = ssl_error == SSL_ERROR_WANT_READ ? EPOLLIN : EPOLLOUT; + r = dnstls_flush_write_buffer(stream); + if (r < 0 && r != -EAGAIN) + return r; + + if (!stream->dnstls_data.shutdown) { + stream->dnstls_data.shutdown = true; + dns_stream_ref(stream); + } + return -EAGAIN; + } else if (ssl_error == SSL_ERROR_SYSCALL) { + if (errno > 0) + log_debug_errno(errno, "Failed to invoke SSL_shutdown, ignoring: %m"); + } else + log_debug("Failed to invoke SSL_shutdown, ignoring: %s", DNSTLS_ERROR_STRING(ssl_error)); + } + + stream->dnstls_events = 0; + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + } + + return 0; +} + +static ssize_t dnstls_stream_write(DnsStream *stream, const char *buf, size_t count) { + int error, r; + ssize_t ss; + + ERR_clear_error(); + ss = r = SSL_write(stream->dnstls_data.ssl, buf, count); + if (r <= 0) { + error = SSL_get_error(stream->dnstls_data.ssl, r); + if (IN_SET(error, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE)) { + stream->dnstls_events = error == SSL_ERROR_WANT_READ ? EPOLLIN : EPOLLOUT; + ss = -EAGAIN; + } else if (error == SSL_ERROR_ZERO_RETURN) { + stream->dnstls_events = 0; + ss = 0; + } else { + log_debug("Failed to invoke SSL_write: %s", DNSTLS_ERROR_STRING(error)); + stream->dnstls_events = 0; + ss = -EPIPE; + } + } else + stream->dnstls_events = 0; + + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + + return ss; +} + +ssize_t dnstls_stream_writev(DnsStream *stream, const struct iovec *iov, size_t iovcnt) { + _cleanup_free_ char *buf = NULL; + size_t count; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.ssl); + assert(iov); + assert(IOVEC_TOTAL_SIZE(iov, iovcnt) > 0); + + if (iovcnt == 1) + return dnstls_stream_write(stream, iov[0].iov_base, iov[0].iov_len); + + /* As of now, OpenSSL can not accumulate multiple writes, so join into a + single buffer. Suboptimal, but better than multiple SSL_write calls. */ + count = IOVEC_TOTAL_SIZE(iov, iovcnt); + buf = new(char, count); + for (size_t i = 0, pos = 0; i < iovcnt; pos += iov[i].iov_len, i++) + memcpy(buf + pos, iov[i].iov_base, iov[i].iov_len); + + return dnstls_stream_write(stream, buf, count); +} + +ssize_t dnstls_stream_read(DnsStream *stream, void *buf, size_t count) { + int error, r; + ssize_t ss; + + assert(stream); + assert(stream->encrypted); + assert(stream->dnstls_data.ssl); + assert(buf); + + ERR_clear_error(); + ss = r = SSL_read(stream->dnstls_data.ssl, buf, count); + if (r <= 0) { + error = SSL_get_error(stream->dnstls_data.ssl, r); + if (IN_SET(error, SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE)) { + /* If we receive SSL_ERROR_WANT_READ here, there are two possible scenarios: + * OpenSSL needs to renegotiate (so we want to get an EPOLLIN event), or + * There is no more application data is available, so we can just return + And apparently there's no nice way to distinguish between the two. + To handle this, never set EPOLLIN and just continue as usual. + If OpenSSL really wants to read due to renegotiation, it will tell us + again on SSL_write (at which point we will request EPOLLIN force a read); + or we will just eventually read data anyway while we wait for a packet */ + stream->dnstls_events = error == SSL_ERROR_WANT_READ ? 0 : EPOLLOUT; + ss = -EAGAIN; + } else if (error == SSL_ERROR_ZERO_RETURN) { + stream->dnstls_events = 0; + ss = 0; + } else { + log_debug("Failed to invoke SSL_read: %s", DNSTLS_ERROR_STRING(error)); + stream->dnstls_events = 0; + ss = -EPIPE; + } + } else + stream->dnstls_events = 0; + + /* flush write buffer in cache of renegotiation */ + r = dnstls_flush_write_buffer(stream); + if (r < 0) + return r; + + return ss; +} + +void dnstls_server_free(DnsServer *server) { + assert(server); + + if (server->dnstls_data.session) + SSL_SESSION_free(server->dnstls_data.session); +} + +int dnstls_manager_init(Manager *manager) { + int r; + + assert(manager); + + ERR_load_crypto_strings(); + SSL_load_error_strings(); + + manager->dnstls_data.ctx = SSL_CTX_new(TLS_client_method()); + if (!manager->dnstls_data.ctx) + return -ENOMEM; + + r = SSL_CTX_set_min_proto_version(manager->dnstls_data.ctx, TLS1_2_VERSION); + if (r == 0) + return -EIO; + + (void) SSL_CTX_set_options(manager->dnstls_data.ctx, SSL_OP_NO_COMPRESSION); + + r = SSL_CTX_set_default_verify_paths(manager->dnstls_data.ctx); + if (r == 0) + return log_warning_errno(SYNTHETIC_ERRNO(EIO), + "Failed to load system trust store: %s", + ERR_error_string(ERR_get_error(), NULL)); + + return 0; +} + +void dnstls_manager_free(Manager *manager) { + assert(manager); + + if (manager->dnstls_data.ctx) + SSL_CTX_free(manager->dnstls_data.ctx); +} diff --git a/src/resolve/resolved-dnstls-openssl.h b/src/resolve/resolved-dnstls-openssl.h new file mode 100644 index 0000000..a73b77b --- /dev/null +++ b/src/resolve/resolved-dnstls-openssl.h @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#if !ENABLE_DNS_OVER_TLS || !DNS_OVER_TLS_USE_OPENSSL +#error This source file requires DNS-over-TLS to be enabled and OpenSSL to be available. +#endif + +#include <openssl/ssl.h> +#include <stdbool.h> + +struct DnsTlsManagerData { + SSL_CTX *ctx; +}; + +struct DnsTlsServerData { + SSL_SESSION *session; +}; + +struct DnsTlsStreamData { + int handshake; + bool shutdown; + SSL *ssl; + BUF_MEM *write_buffer; + size_t buffer_offset; +}; diff --git a/src/resolve/resolved-dnstls.h b/src/resolve/resolved-dnstls.h new file mode 100644 index 0000000..cda97e0 --- /dev/null +++ b/src/resolve/resolved-dnstls.h @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#if ENABLE_DNS_OVER_TLS + +#include <stdint.h> +#include <sys/uio.h> + +typedef struct DnsServer DnsServer; +typedef struct DnsStream DnsStream; +typedef struct DnsTlsManagerData DnsTlsManagerData; +typedef struct DnsTlsServerData DnsTlsServerData; +typedef struct DnsTlsStreamData DnsTlsStreamData; +typedef struct Manager Manager; + +#if DNS_OVER_TLS_USE_GNUTLS +#include "resolved-dnstls-gnutls.h" +#elif DNS_OVER_TLS_USE_OPENSSL +#include "resolved-dnstls-openssl.h" +#else +#error Unknown dependency for supporting DNS-over-TLS +#endif + +#define DNSTLS_STREAM_CLOSED 1 + +int dnstls_stream_connect_tls(DnsStream *stream, DnsServer *server); +void dnstls_stream_free(DnsStream *stream); +int dnstls_stream_on_io(DnsStream *stream, uint32_t revents); +int dnstls_stream_shutdown(DnsStream *stream, int error); +ssize_t dnstls_stream_writev(DnsStream *stream, const struct iovec *iov, size_t iovcnt); +ssize_t dnstls_stream_read(DnsStream *stream, void *buf, size_t count); + +void dnstls_server_free(DnsServer *server); + +int dnstls_manager_init(Manager *manager); +void dnstls_manager_free(Manager *manager); + +#endif /* ENABLE_DNS_OVER_TLS */ diff --git a/src/resolve/resolved-etc-hosts.c b/src/resolve/resolved-etc-hosts.c new file mode 100644 index 0000000..8a58db4 --- /dev/null +++ b/src/resolve/resolved-etc-hosts.c @@ -0,0 +1,467 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "fd-util.h" +#include "fileio.h" +#include "hostname-util.h" +#include "resolved-dns-synthesize.h" +#include "resolved-etc-hosts.h" +#include "socket-netlink.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "time-util.h" + +/* Recheck /etc/hosts at most once every 2s */ +#define ETC_HOSTS_RECHECK_USEC (2*USEC_PER_SEC) + +static void etc_hosts_item_free(EtcHostsItem *item) { + strv_free(item->names); + free(item); +} + +static void etc_hosts_item_by_name_free(EtcHostsItemByName *item) { + free(item->name); + free(item->addresses); + free(item); +} + +void etc_hosts_free(EtcHosts *hosts) { + hosts->by_address = hashmap_free_with_destructor(hosts->by_address, etc_hosts_item_free); + hosts->by_name = hashmap_free_with_destructor(hosts->by_name, etc_hosts_item_by_name_free); + hosts->no_address = set_free_free(hosts->no_address); +} + +void manager_etc_hosts_flush(Manager *m) { + etc_hosts_free(&m->etc_hosts); + m->etc_hosts_stat = (struct stat) {}; +} + +static int parse_line(EtcHosts *hosts, unsigned nr, const char *line) { + _cleanup_free_ char *address_str = NULL; + struct in_addr_data address = {}; + bool found = false; + EtcHostsItem *item; + int r; + + assert(hosts); + assert(line); + + r = extract_first_word(&line, &address_str, NULL, EXTRACT_RELAX); + if (r < 0) + return log_error_errno(r, "/etc/hosts:%u: failed to extract address: %m", nr); + assert(r > 0); /* We already checked that the line is not empty, so it should contain *something* */ + + r = in_addr_ifindex_from_string_auto(address_str, &address.family, &address.address, NULL); + if (r < 0) { + log_warning_errno(r, "/etc/hosts:%u: address '%s' is invalid, ignoring: %m", nr, address_str); + return 0; + } + + r = in_addr_data_is_null(&address); + if (r < 0) { + log_warning_errno(r, "/etc/hosts:%u: address '%s' is invalid, ignoring: %m", nr, address_str); + return 0; + } + if (r > 0) + /* This is an 0.0.0.0 or :: item, which we assume means that we shall map the specified hostname to + * nothing. */ + item = NULL; + else { + /* If this is a normal address, then simply add entry mapping it to the specified names */ + + item = hashmap_get(hosts->by_address, &address); + if (!item) { + r = hashmap_ensure_allocated(&hosts->by_address, &in_addr_data_hash_ops); + if (r < 0) + return log_oom(); + + item = new(EtcHostsItem, 1); + if (!item) + return log_oom(); + + *item = (EtcHostsItem) { + .address = address, + }; + + r = hashmap_put(hosts->by_address, &item->address, item); + if (r < 0) { + free(item); + return log_oom(); + } + } + } + + for (;;) { + _cleanup_free_ char *name = NULL; + EtcHostsItemByName *bn; + + r = extract_first_word(&line, &name, NULL, EXTRACT_RELAX); + if (r < 0) + return log_error_errno(r, "/etc/hosts:%u: couldn't extract hostname: %m", nr); + if (r == 0) + break; + + found = true; + + r = dns_name_is_valid_ldh(name); + if (r <= 0) { + if (r < 0) + log_warning_errno(r, "/etc/hosts:%u: Failed to check the validity of hostname \"%s\", ignoring: %m", nr, name); + else + log_warning("/etc/hosts:%u: hostname \"%s\" is not valid, ignoring.", nr, name); + continue; + } + + if (!item) { + /* Optimize the case where we don't need to store any addresses, by storing + * only the name in a dedicated Set instead of the hashmap */ + + r = set_ensure_consume(&hosts->no_address, &dns_name_hash_ops, TAKE_PTR(name)); + if (r < 0) + return r; + + continue; + } + + r = strv_extend_with_size(&item->names, &item->n_names, name); + if (r < 0) + return log_oom(); + + bn = hashmap_get(hosts->by_name, name); + if (!bn) { + r = hashmap_ensure_allocated(&hosts->by_name, &dns_name_hash_ops); + if (r < 0) + return log_oom(); + + bn = new0(EtcHostsItemByName, 1); + if (!bn) + return log_oom(); + + r = hashmap_put(hosts->by_name, name, bn); + if (r < 0) { + free(bn); + return log_oom(); + } + + bn->name = TAKE_PTR(name); + } + + if (!GREEDY_REALLOC(bn->addresses, bn->n_addresses + 1)) + return log_oom(); + + bn->addresses[bn->n_addresses++] = &item->address; + } + + if (!found) + log_warning("/etc/hosts:%u: line is missing any hostnames", nr); + + return 0; +} + +static void strip_localhost(EtcHosts *hosts) { + static const struct in_addr_data local_in_addrs[] = { + { + .family = AF_INET, +#if __BYTE_ORDER == __LITTLE_ENDIAN + /* We want constant expressions here, that's why we don't use htole32() here */ + .address.in.s_addr = UINT32_C(0x0100007F), +#else + .address.in.s_addr = UINT32_C(0x7F000001), +#endif + }, + { + .family = AF_INET6, + .address.in6 = IN6ADDR_LOOPBACK_INIT, + }, + }; + + EtcHostsItem *item; + + assert(hosts); + + /* Removes the 'localhost' entry from what we loaded. But only if the mapping is exclusively between + * 127.0.0.1 and localhost (or aliases to that we recognize). If there's any other name assigned to + * it, we leave the entry in. + * + * This way our regular synthesizing can take over, but only if it would result in the exact same + * mappings. */ + + for (size_t j = 0; j < ELEMENTSOF(local_in_addrs); j++) { + bool all_localhost, in_order; + + item = hashmap_get(hosts->by_address, local_in_addrs + j); + if (!item) + continue; + + /* Check whether all hostnames the loopback address points to are localhost ones */ + all_localhost = true; + STRV_FOREACH(i, item->names) + if (!is_localhost(*i)) { + all_localhost = false; + break; + } + + if (!all_localhost) /* Not all names are localhost, hence keep the entries for this address. */ + continue; + + /* Now check if the names listed for this address actually all point back just to this + * address (or the other loopback address). If not, let's stay away from this too. */ + in_order = true; + STRV_FOREACH(i, item->names) { + EtcHostsItemByName *n; + bool all_local_address; + + n = hashmap_get(hosts->by_name, *i); + if (!n) /* No reverse entry? Then almost certainly the entry already got deleted from + * the previous iteration of this loop, i.e. via the other protocol */ + break; + + /* Now check if the addresses of this item are all localhost addresses */ + all_local_address = true; + for (size_t m = 0; m < n->n_addresses; m++) + if (!in_addr_is_localhost(n->addresses[m]->family, &n->addresses[m]->address)) { + all_local_address = false; + break; + } + + if (!all_local_address) { + in_order = false; + break; + } + } + + if (!in_order) + continue; + + STRV_FOREACH(i, item->names) { + EtcHostsItemByName *n; + + n = hashmap_remove(hosts->by_name, *i); + if (n) + etc_hosts_item_by_name_free(n); + } + + assert_se(hashmap_remove(hosts->by_address, local_in_addrs + j) == item); + etc_hosts_item_free(item); + } +} + +int etc_hosts_parse(EtcHosts *hosts, FILE *f) { + _cleanup_(etc_hosts_free) EtcHosts t = {}; + unsigned nr = 0; + int r; + + for (;;) { + _cleanup_free_ char *line = NULL; + char *l; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) + return log_error_errno(r, "Failed to read /etc/hosts: %m"); + if (r == 0) + break; + + nr++; + + l = strchr(line, '#'); + if (l) + *l = '\0'; + + l = strstrip(line); + if (isempty(l)) + continue; + + r = parse_line(&t, nr, l); + if (r < 0) + return r; + } + + strip_localhost(&t); + + etc_hosts_free(hosts); + *hosts = t; + t = (EtcHosts) {}; /* prevent cleanup */ + return 0; +} + +static int manager_etc_hosts_read(Manager *m) { + _cleanup_fclose_ FILE *f = NULL; + struct stat st; + usec_t ts; + int r; + + assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &ts) >= 0); + + /* See if we checked /etc/hosts recently already */ + if (m->etc_hosts_last != USEC_INFINITY && m->etc_hosts_last + ETC_HOSTS_RECHECK_USEC > ts) + return 0; + + m->etc_hosts_last = ts; + + if (m->etc_hosts_stat.st_mode != 0) { + if (stat("/etc/hosts", &st) < 0) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to stat /etc/hosts: %m"); + + manager_etc_hosts_flush(m); + return 0; + } + + /* Did the mtime or ino/dev change? If not, there's no point in re-reading the file. */ + if (stat_inode_unmodified(&m->etc_hosts_stat, &st)) + return 0; + } + + f = fopen("/etc/hosts", "re"); + if (!f) { + if (errno != ENOENT) + return log_error_errno(errno, "Failed to open /etc/hosts: %m"); + + manager_etc_hosts_flush(m); + return 0; + } + + /* Take the timestamp at the beginning of processing, so that any changes made later are read on the next + * invocation */ + r = fstat(fileno(f), &st); + if (r < 0) + return log_error_errno(errno, "Failed to fstat() /etc/hosts: %m"); + + r = etc_hosts_parse(&m->etc_hosts, f); + if (r < 0) + return r; + + m->etc_hosts_stat = st; + m->etc_hosts_last = ts; + + return 1; +} + +int manager_etc_hosts_lookup(Manager *m, DnsQuestion* q, DnsAnswer **answer) { + bool found_a = false, found_aaaa = false; + struct in_addr_data k = {}; + EtcHostsItemByName *bn; + DnsResourceKey *t; + const char *name; + unsigned i; + int r; + + assert(m); + assert(q); + assert(answer); + + if (!m->read_etc_hosts) + return 0; + + (void) manager_etc_hosts_read(m); + + name = dns_question_first_name(q); + if (!name) + return 0; + + r = dns_name_address(name, &k.family, &k.address); + if (r > 0) { + EtcHostsItem *item; + DnsResourceKey *found_ptr = NULL; + + item = hashmap_get(m->etc_hosts.by_address, &k); + if (!item) + return 0; + + /* We have an address in /etc/hosts that matches the queried name. Let's return successful. Actual data + * we'll only return if the request was for PTR. */ + + DNS_QUESTION_FOREACH(t, q) { + if (!IN_SET(t->type, DNS_TYPE_PTR, DNS_TYPE_ANY)) + continue; + if (!IN_SET(t->class, DNS_CLASS_IN, DNS_CLASS_ANY)) + continue; + + r = dns_name_equal(dns_resource_key_name(t), name); + if (r < 0) + return r; + if (r > 0) { + found_ptr = t; + break; + } + } + + if (found_ptr) { + r = dns_answer_reserve(answer, item->n_names); + if (r < 0) + return r; + + STRV_FOREACH(n, item->names) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + rr = dns_resource_record_new(found_ptr); + if (!rr) + return -ENOMEM; + + rr->ptr.name = strdup(*n); + if (!rr->ptr.name) + return -ENOMEM; + + r = dns_answer_add(*answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return r; + } + } + + return 1; + } + + bn = hashmap_get(m->etc_hosts.by_name, name); + if (bn) { + r = dns_answer_reserve(answer, bn->n_addresses); + if (r < 0) + return r; + } else { + /* Check if name was listed with no address. If yes, continue to return an answer. */ + if (!set_contains(m->etc_hosts.no_address, name)) + return 0; + } + + DNS_QUESTION_FOREACH(t, q) { + if (!IN_SET(t->type, DNS_TYPE_A, DNS_TYPE_AAAA, DNS_TYPE_ANY)) + continue; + if (!IN_SET(t->class, DNS_CLASS_IN, DNS_CLASS_ANY)) + continue; + + r = dns_name_equal(dns_resource_key_name(t), name); + if (r < 0) + return r; + if (r == 0) + continue; + + if (IN_SET(t->type, DNS_TYPE_A, DNS_TYPE_ANY)) + found_a = true; + if (IN_SET(t->type, DNS_TYPE_AAAA, DNS_TYPE_ANY)) + found_aaaa = true; + + if (found_a && found_aaaa) + break; + } + + for (i = 0; bn && i < bn->n_addresses; i++) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + + if ((!found_a && bn->addresses[i]->family == AF_INET) || + (!found_aaaa && bn->addresses[i]->family == AF_INET6)) + continue; + + r = dns_resource_record_new_address(&rr, bn->addresses[i]->family, &bn->addresses[i]->address, bn->name); + if (r < 0) + return r; + + r = dns_answer_add(*answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL); + if (r < 0) + return r; + } + + return found_a || found_aaaa; +} diff --git a/src/resolve/resolved-etc-hosts.h b/src/resolve/resolved-etc-hosts.h new file mode 100644 index 0000000..df66e71 --- /dev/null +++ b/src/resolve/resolved-etc-hosts.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "resolved-manager.h" +#include "resolved-dns-question.h" +#include "resolved-dns-answer.h" + +typedef struct EtcHostsItem { + struct in_addr_data address; + + char **names; + size_t n_names; +} EtcHostsItem; + +typedef struct EtcHostsItemByName { + char *name; + + struct in_addr_data **addresses; + size_t n_addresses; +} EtcHostsItemByName; + +int etc_hosts_parse(EtcHosts *hosts, FILE *f); +void etc_hosts_free(EtcHosts *hosts); + +void manager_etc_hosts_flush(Manager *m); +int manager_etc_hosts_lookup(Manager *m, DnsQuestion* q, DnsAnswer **answer); diff --git a/src/resolve/resolved-gperf.gperf b/src/resolve/resolved-gperf.gperf new file mode 100644 index 0000000..eab4c7e --- /dev/null +++ b/src/resolve/resolved-gperf.gperf @@ -0,0 +1,34 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +%{ +#if __GNUC__ >= 7 +_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"") +#endif +#include <stddef.h> +#include "conf-parser.h" +#include "resolved-conf.h" +#include "resolved-manager.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name resolved_gperf_hash +%define lookup-function-name resolved_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Resolve.DNS, config_parse_dns_servers, DNS_SERVER_SYSTEM, 0 +Resolve.FallbackDNS, config_parse_dns_servers, DNS_SERVER_FALLBACK, 0 +Resolve.Domains, config_parse_search_domains, 0, 0 +Resolve.LLMNR, config_parse_resolve_support, 0, offsetof(Manager, llmnr_support) +Resolve.MulticastDNS, config_parse_resolve_support, 0, offsetof(Manager, mdns_support) +Resolve.DNSSEC, config_parse_dnssec_mode, 0, offsetof(Manager, dnssec_mode) +Resolve.DNSOverTLS, config_parse_dns_over_tls_mode, 0, offsetof(Manager, dns_over_tls_mode) +Resolve.Cache, config_parse_dns_cache_mode, DNS_CACHE_MODE_YES, offsetof(Manager, enable_cache) +Resolve.DNSStubListener, config_parse_dns_stub_listener_mode, 0, offsetof(Manager, dns_stub_listener_mode) +Resolve.ReadEtcHosts, config_parse_bool, 0, offsetof(Manager, read_etc_hosts) +Resolve.ResolveUnicastSingleLabel, config_parse_bool, 0, offsetof(Manager, resolve_unicast_single_label) +Resolve.DNSStubListenerExtra, config_parse_dns_stub_listener_extra, 0, offsetof(Manager, dns_extra_stub_listeners) +Resolve.CacheFromLocalhost, config_parse_bool, 0, offsetof(Manager, cache_from_localhost) diff --git a/src/resolve/resolved-link-bus.c b/src/resolve/resolved-link-bus.c new file mode 100644 index 0000000..a817b1b --- /dev/null +++ b/src/resolve/resolved-link-bus.c @@ -0,0 +1,933 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> +#include <netinet/in.h> +#include <sys/capability.h> + +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "bus-get-properties.h" +#include "bus-message-util.h" +#include "bus-polkit.h" +#include "log-link.h" +#include "parse-util.h" +#include "resolve-util.h" +#include "resolved-bus.h" +#include "resolved-link-bus.h" +#include "resolved-resolv-conf.h" +#include "socket-netlink.h" +#include "stdio-util.h" +#include "strv.h" +#include "user-util.h" + +static BUS_DEFINE_PROPERTY_GET(property_get_dnssec_supported, "b", Link, link_dnssec_supported); +static BUS_DEFINE_PROPERTY_GET2(property_get_dnssec_mode, "s", Link, link_get_dnssec_mode, dnssec_mode_to_string); + +static int property_get_dns_over_tls_mode( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *l = ASSERT_PTR(userdata); + + assert(reply); + + return sd_bus_message_append(reply, "s", dns_over_tls_mode_to_string(link_get_dns_over_tls_mode(l))); +} + +static int property_get_dns_internal( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error, + bool extended) { + + Link *l = ASSERT_PTR(userdata); + int r; + + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', extended ? "(iayqs)" : "(iay)"); + if (r < 0) + return r; + + LIST_FOREACH(servers, s, l->dns_servers) { + r = bus_dns_server_append(reply, s, false, extended); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int property_get_dns( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return property_get_dns_internal(bus, path, interface, property, reply, userdata, error, false); +} + +static int property_get_dns_ex( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return property_get_dns_internal(bus, path, interface, property, reply, userdata, error, true); +} + +static int property_get_current_dns_server_internal( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error, + bool extended) { + + DnsServer *s; + + assert(reply); + assert(userdata); + + s = *(DnsServer **) userdata; + + return bus_dns_server_append(reply, s, false, extended); +} + +static int property_get_current_dns_server( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return property_get_current_dns_server_internal(bus, path, interface, property, reply, userdata, error, false); +} + +static int property_get_current_dns_server_ex( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + return property_get_current_dns_server_internal(bus, path, interface, property, reply, userdata, error, true); +} + +static int property_get_domains( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *l = ASSERT_PTR(userdata); + int r; + + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "(sb)"); + if (r < 0) + return r; + + LIST_FOREACH(domains, d, l->search_domains) { + r = sd_bus_message_append(reply, "(sb)", d->name, d->route_only); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int property_get_default_route( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *l = ASSERT_PTR(userdata); + + assert(reply); + + /* Return what is configured, if there's something configured */ + if (l->default_route >= 0) + return sd_bus_message_append(reply, "b", l->default_route); + + /* Otherwise report what is in effect */ + if (l->unicast_scope) + return sd_bus_message_append(reply, "b", dns_scope_is_default_route(l->unicast_scope)); + + return sd_bus_message_append(reply, "b", false); +} + +static int property_get_scopes_mask( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *l = ASSERT_PTR(userdata); + uint64_t mask; + + assert(reply); + + mask = (l->unicast_scope ? SD_RESOLVED_DNS : 0) | + (l->llmnr_ipv4_scope ? SD_RESOLVED_LLMNR_IPV4 : 0) | + (l->llmnr_ipv6_scope ? SD_RESOLVED_LLMNR_IPV6 : 0) | + (l->mdns_ipv4_scope ? SD_RESOLVED_MDNS_IPV4 : 0) | + (l->mdns_ipv6_scope ? SD_RESOLVED_MDNS_IPV6 : 0); + + return sd_bus_message_append(reply, "t", mask); +} + +static int property_get_ntas( + sd_bus *bus, + const char *path, + const char *interface, + const char *property, + sd_bus_message *reply, + void *userdata, + sd_bus_error *error) { + + Link *l = ASSERT_PTR(userdata); + const char *name; + int r; + + assert(reply); + + r = sd_bus_message_open_container(reply, 'a', "s"); + if (r < 0) + return r; + + SET_FOREACH(name, l->dnssec_negative_trust_anchors) { + r = sd_bus_message_append(reply, "s", name); + if (r < 0) + return r; + } + + return sd_bus_message_close_container(reply); +} + +static int verify_unmanaged_link(Link *l, sd_bus_error *error) { + assert(l); + + if (l->flags & IFF_LOOPBACK) + return sd_bus_error_setf(error, BUS_ERROR_LINK_BUSY, "Link %s is loopback device.", l->ifname); + if (l->is_managed) + return sd_bus_error_setf(error, BUS_ERROR_LINK_BUSY, "Link %s is managed.", l->ifname); + + return 0; +} + +static int bus_link_method_set_dns_servers_internal(sd_bus_message *message, void *userdata, sd_bus_error *error, bool extended) { + _cleanup_free_ char *j = NULL; + struct in_addr_full **dns; + bool changed = false; + Link *l = ASSERT_PTR(userdata); + size_t n; + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = bus_message_read_dns_servers(message, error, extended, &dns, &n); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-dns-servers", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + goto finalize; + if (r == 0) { + r = 1; /* Polkit will call us back */ + goto finalize; + } + + for (size_t i = 0; i < n; i++) { + const char *s; + + s = in_addr_full_to_string(dns[i]); + if (!s) { + r = -ENOMEM; + goto finalize; + } + + if (!strextend_with_separator(&j, ", ", s)) { + r = -ENOMEM; + goto finalize; + } + } + + bus_client_log(message, "DNS server change"); + + dns_server_mark_all(l->dns_servers); + + for (size_t i = 0; i < n; i++) { + DnsServer *s; + + s = dns_server_find(l->dns_servers, dns[i]->family, &dns[i]->address, dns[i]->port, 0, dns[i]->server_name); + if (s) + dns_server_move_back_and_unmark(s); + else { + r = dns_server_new(l->manager, NULL, DNS_SERVER_LINK, l, dns[i]->family, &dns[i]->address, dns[i]->port, 0, dns[i]->server_name); + if (r < 0) { + dns_server_unlink_all(l->dns_servers); + goto finalize; + } + + changed = true; + } + + } + + changed = dns_server_unlink_marked(l->dns_servers) || changed; + + if (changed) { + link_allocate_scopes(l); + + (void) link_save_user(l); + (void) manager_write_resolv_conf(l->manager); + (void) manager_send_changed(l->manager, "DNS"); + + if (j) + log_link_info(l, "Bus client set DNS server list to: %s", j); + else + log_link_info(l, "Bus client reset DNS server list."); + } + + r = sd_bus_reply_method_return(message, NULL); + +finalize: + for (size_t i = 0; i < n; i++) + in_addr_full_free(dns[i]); + free(dns); + + return r; +} + +int bus_link_method_set_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return bus_link_method_set_dns_servers_internal(message, userdata, error, false); +} + +int bus_link_method_set_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) { + return bus_link_method_set_dns_servers_internal(message, userdata, error, true); +} + +int bus_link_method_set_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_free_ char *j = NULL; + Link *l = ASSERT_PTR(userdata); + bool changed = false; + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_enter_container(message, 'a', "(sb)"); + if (r < 0) + return r; + + for (;;) { + _cleanup_free_ char *prefixed = NULL; + const char *name; + int route_only; + + r = sd_bus_message_read(message, "(sb)", &name, &route_only); + if (r < 0) + return r; + if (r == 0) + break; + + r = dns_name_is_valid(name); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid search domain %s", name); + if (!route_only && dns_name_is_root(name)) + return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Root domain is not suitable as search domain"); + + if (route_only) { + prefixed = strjoin("~", name); + if (!prefixed) + return -ENOMEM; + + name = prefixed; + } + + if (!strextend_with_separator(&j, ", ", name)) + return -ENOMEM; + } + + r = sd_bus_message_rewind(message, false); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-domains", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "dns domains change"); + + dns_search_domain_mark_all(l->search_domains); + + for (;;) { + DnsSearchDomain *d; + const char *name; + int route_only; + + r = sd_bus_message_read(message, "(sb)", &name, &route_only); + if (r < 0) + goto clear; + if (r == 0) + break; + + r = dns_search_domain_find(l->search_domains, name, &d); + if (r < 0) + goto clear; + + if (r > 0) + dns_search_domain_move_back_and_unmark(d); + else { + r = dns_search_domain_new(l->manager, &d, DNS_SEARCH_DOMAIN_LINK, l, name); + if (r < 0) + goto clear; + + changed = true; + } + + d->route_only = route_only; + } + + r = sd_bus_message_exit_container(message); + if (r < 0) + goto clear; + + changed = dns_search_domain_unlink_marked(l->search_domains) || changed; + + if (changed) { + (void) link_save_user(l); + (void) manager_write_resolv_conf(l->manager); + + if (j) + log_link_info(l, "Bus client set search domain list to: %s", j); + else + log_link_info(l, "Bus client reset search domain list."); + } + + return sd_bus_reply_method_return(message, NULL); + +clear: + dns_search_domain_unlink_all(l->search_domains); + return r; +} + +int bus_link_method_set_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r, b; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "b", &b); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-default-route", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "dns default route change"); + + if (l->default_route != b) { + l->default_route = b; + + (void) link_save_user(l); + (void) manager_write_resolv_conf(l->manager); + + log_link_info(l, "Bus client set default route setting: %s", yes_no(b)); + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + ResolveSupport mode; + const char *llmnr; + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &llmnr); + if (r < 0) + return r; + + if (isempty(llmnr)) + mode = RESOLVE_SUPPORT_YES; + else { + mode = resolve_support_from_string(llmnr); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid LLMNR setting: %s", llmnr); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-llmnr", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "LLMNR change"); + + if (l->llmnr_support != mode) { + l->llmnr_support = mode; + link_allocate_scopes(l); + link_add_rrs(l, false); + + (void) link_save_user(l); + + log_link_info(l, "Bus client set LLMNR setting: %s", resolve_support_to_string(mode)); + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + ResolveSupport mode; + const char *mdns; + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &mdns); + if (r < 0) + return r; + + if (isempty(mdns)) + mode = RESOLVE_SUPPORT_NO; + else { + mode = resolve_support_from_string(mdns); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid MulticastDNS setting: %s", mdns); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-mdns", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "mDNS change"); + + if (l->mdns_support != mode) { + l->mdns_support = mode; + link_allocate_scopes(l); + link_add_rrs(l, false); + + (void) link_save_user(l); + + log_link_info(l, "Bus client set MulticastDNS setting: %s", resolve_support_to_string(mode)); + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + const char *dns_over_tls; + DnsOverTlsMode mode; + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &dns_over_tls); + if (r < 0) + return r; + + if (isempty(dns_over_tls)) + mode = _DNS_OVER_TLS_MODE_INVALID; + else { + mode = dns_over_tls_mode_from_string(dns_over_tls); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid DNSOverTLS setting: %s", dns_over_tls); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-dns-over-tls", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "D-o-T change"); + + if (l->dns_over_tls_mode != mode) { + link_set_dns_over_tls_mode(l, mode); + link_allocate_scopes(l); + + (void) link_save_user(l); + + log_link_info(l, "Bus client set DNSOverTLS setting: %s", + mode < 0 ? "default" : dns_over_tls_mode_to_string(mode)); + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + const char *dnssec; + DnssecMode mode; + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = sd_bus_message_read(message, "s", &dnssec); + if (r < 0) + return r; + + if (isempty(dnssec)) + mode = _DNSSEC_MODE_INVALID; + else { + mode = dnssec_mode_from_string(dnssec); + if (mode < 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid DNSSEC setting: %s", dnssec); + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-dnssec", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "DNSSEC change"); + + if (l->dnssec_mode != mode) { + link_set_dnssec_mode(l, mode); + link_allocate_scopes(l); + + (void) link_save_user(l); + + log_link_info(l, "Bus client set DNSSEC setting: %s", + mode < 0 ? "default" : dnssec_mode_to_string(mode)); + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_set_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_set_free_free_ Set *ns = NULL; + _cleanup_strv_free_ char **ntas = NULL; + _cleanup_free_ char *j = NULL; + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + ns = set_new(&dns_name_hash_ops); + if (!ns) + return -ENOMEM; + + r = sd_bus_message_read_strv(message, &ntas); + if (r < 0) + return r; + + STRV_FOREACH(i, ntas) { + r = dns_name_is_valid(*i); + if (r < 0) + return r; + if (r == 0) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, + "Invalid negative trust anchor domain: %s", *i); + + r = set_put_strdup(&ns, *i); + if (r < 0) + return r; + + if (!strextend_with_separator(&j, ", ", *i)) + return -ENOMEM; + } + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.set-dnssec-negative-trust-anchors", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "DNSSEC NTA change"); + + if (!set_equal(ns, l->dnssec_negative_trust_anchors)) { + set_free_free(l->dnssec_negative_trust_anchors); + l->dnssec_negative_trust_anchors = TAKE_PTR(ns); + + (void) link_save_user(l); + + if (j) + log_link_info(l, "Bus client set NTA list to: %s", j); + else + log_link_info(l, "Bus client reset NTA list."); + } + + return sd_bus_reply_method_return(message, NULL); +} + +int bus_link_method_revert(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Link *l = ASSERT_PTR(userdata); + int r; + + assert(message); + + r = verify_unmanaged_link(l, error); + if (r < 0) + return r; + + r = bus_verify_polkit_async(message, CAP_NET_ADMIN, + "org.freedesktop.resolve1.revert", + NULL, true, UID_INVALID, + &l->manager->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + bus_client_log(message, "revert"); + + link_flush_settings(l); + link_allocate_scopes(l); + link_add_rrs(l, false); + + (void) link_save_user(l); + (void) manager_write_resolv_conf(l->manager); + (void) manager_send_changed(l->manager, "DNS"); + + return sd_bus_reply_method_return(message, NULL); +} + +static int link_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { + _cleanup_free_ char *e = NULL; + Manager *m = ASSERT_PTR(userdata); + Link *link; + int ifindex, r; + + assert(bus); + assert(path); + assert(interface); + assert(found); + + r = sd_bus_path_decode(path, "/org/freedesktop/resolve1/link", &e); + if (r <= 0) + return 0; + + ifindex = parse_ifindex(e); + if (ifindex < 0) + return 0; + + link = hashmap_get(m->links, INT_TO_PTR(ifindex)); + if (!link) + return 0; + + *found = link; + return 1; +} + +char *link_bus_path(const Link *link) { + char *p, ifindex[DECIMAL_STR_MAX(link->ifindex)]; + int r; + + assert(link); + + xsprintf(ifindex, "%i", link->ifindex); + + r = sd_bus_path_encode("/org/freedesktop/resolve1/link", ifindex, &p); + if (r < 0) + return NULL; + + return p; +} + +static int link_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) { + _cleanup_strv_free_ char **l = NULL; + Manager *m = ASSERT_PTR(userdata); + Link *link; + unsigned c = 0; + + assert(bus); + assert(path); + assert(nodes); + + l = new0(char*, hashmap_size(m->links) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(link, m->links) { + char *p; + + p = link_bus_path(link); + if (!p) + return -ENOMEM; + + l[c++] = p; + } + + l[c] = NULL; + *nodes = TAKE_PTR(l); + + return 1; +} + +static const sd_bus_vtable link_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_PROPERTY("ScopesMask", "t", property_get_scopes_mask, 0, 0), + SD_BUS_PROPERTY("DNS", "a(iay)", property_get_dns, 0, 0), + SD_BUS_PROPERTY("DNSEx", "a(iayqs)", property_get_dns_ex, 0, 0), + SD_BUS_PROPERTY("CurrentDNSServer", "(iay)", property_get_current_dns_server, offsetof(Link, current_dns_server), 0), + SD_BUS_PROPERTY("CurrentDNSServerEx", "(iayqs)", property_get_current_dns_server_ex, offsetof(Link, current_dns_server), 0), + SD_BUS_PROPERTY("Domains", "a(sb)", property_get_domains, 0, 0), + SD_BUS_PROPERTY("DefaultRoute", "b", property_get_default_route, 0, 0), + SD_BUS_PROPERTY("LLMNR", "s", bus_property_get_resolve_support, offsetof(Link, llmnr_support), 0), + SD_BUS_PROPERTY("MulticastDNS", "s", bus_property_get_resolve_support, offsetof(Link, mdns_support), 0), + SD_BUS_PROPERTY("DNSOverTLS", "s", property_get_dns_over_tls_mode, 0, 0), + SD_BUS_PROPERTY("DNSSEC", "s", property_get_dnssec_mode, 0, 0), + SD_BUS_PROPERTY("DNSSECNegativeTrustAnchors", "as", property_get_ntas, 0, 0), + SD_BUS_PROPERTY("DNSSECSupported", "b", property_get_dnssec_supported, 0, 0), + + SD_BUS_METHOD_WITH_ARGS("SetDNS", + SD_BUS_ARGS("a(iay)", addresses), + SD_BUS_NO_RESULT, + bus_link_method_set_dns_servers, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSEx", + SD_BUS_ARGS("a(iayqs)", addresses), + SD_BUS_NO_RESULT, + bus_link_method_set_dns_servers_ex, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDomains", + SD_BUS_ARGS("a(sb)", domains), + SD_BUS_NO_RESULT, + bus_link_method_set_domains, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDefaultRoute", + SD_BUS_ARGS("b", enable), + SD_BUS_NO_RESULT, + bus_link_method_set_default_route, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetLLMNR", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_llmnr, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetMulticastDNS", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_mdns, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSOverTLS", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_dns_over_tls, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSSEC", + SD_BUS_ARGS("s", mode), + SD_BUS_NO_RESULT, + bus_link_method_set_dnssec, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("SetDNSSECNegativeTrustAnchors", + SD_BUS_ARGS("as", names), + SD_BUS_NO_RESULT, + bus_link_method_set_dnssec_negative_trust_anchors, + SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD_WITH_ARGS("Revert", + SD_BUS_NO_ARGS, + SD_BUS_NO_RESULT, + bus_link_method_revert, + SD_BUS_VTABLE_UNPRIVILEGED), + + SD_BUS_VTABLE_END +}; + +const BusObjectImplementation link_object = { + "/org/freedesktop/resolve1/link", + "org.freedesktop.resolve1.Link", + .fallback_vtables = BUS_FALLBACK_VTABLES({link_vtable, link_object_find}), + .node_enumerator = link_node_enumerator, +}; diff --git a/src/resolve/resolved-link-bus.h b/src/resolve/resolved-link-bus.h new file mode 100644 index 0000000..b882df5 --- /dev/null +++ b/src/resolve/resolved-link-bus.h @@ -0,0 +1,22 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "sd-bus.h" + +#include "bus-util.h" +#include "resolved-link.h" + +extern const BusObjectImplementation link_object; + +char *link_bus_path(const Link *link); + +int bus_link_method_set_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_domains(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_set_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error); +int bus_link_method_revert(sd_bus_message *message, void *userdata, sd_bus_error *error); diff --git a/src/resolve/resolved-link.c b/src/resolve/resolved-link.c new file mode 100644 index 0000000..9ab55eb --- /dev/null +++ b/src/resolve/resolved-link.c @@ -0,0 +1,1431 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <linux/if.h> +#include <unistd.h> + +#include "sd-network.h" + +#include "alloc-util.h" +#include "env-file.h" +#include "fd-util.h" +#include "fileio.h" +#include "log-link.h" +#include "mkdir.h" +#include "netif-util.h" +#include "parse-util.h" +#include "resolved-link.h" +#include "resolved-llmnr.h" +#include "resolved-mdns.h" +#include "socket-netlink.h" +#include "stat-util.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" + +int link_new(Manager *m, Link **ret, int ifindex) { + _cleanup_(link_freep) Link *l = NULL; + int r; + + assert(m); + assert(ifindex > 0); + + l = new(Link, 1); + if (!l) + return -ENOMEM; + + *l = (Link) { + .ifindex = ifindex, + .default_route = -1, + .llmnr_support = RESOLVE_SUPPORT_YES, + .mdns_support = RESOLVE_SUPPORT_NO, + .dnssec_mode = _DNSSEC_MODE_INVALID, + .dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID, + .operstate = IF_OPER_UNKNOWN, + }; + + if (asprintf(&l->state_file, "/run/systemd/resolve/netif/%i", ifindex) < 0) + return -ENOMEM; + + r = hashmap_ensure_put(&m->links, NULL, INT_TO_PTR(ifindex), l); + if (r < 0) + return r; + + l->manager = m; + + if (ret) + *ret = l; + TAKE_PTR(l); + + return 0; +} + +void link_flush_settings(Link *l) { + assert(l); + + l->default_route = -1; + l->llmnr_support = RESOLVE_SUPPORT_YES; + l->mdns_support = RESOLVE_SUPPORT_NO; + l->dnssec_mode = _DNSSEC_MODE_INVALID; + l->dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID; + + dns_server_unlink_all(l->dns_servers); + dns_search_domain_unlink_all(l->search_domains); + + l->dnssec_negative_trust_anchors = set_free_free(l->dnssec_negative_trust_anchors); +} + +Link *link_free(Link *l) { + if (!l) + return NULL; + + /* Send goodbye messages. */ + dns_scope_announce(l->mdns_ipv4_scope, true); + dns_scope_announce(l->mdns_ipv6_scope, true); + + link_flush_settings(l); + + while (l->addresses) + (void) link_address_free(l->addresses); + + if (l->manager) + hashmap_remove(l->manager->links, INT_TO_PTR(l->ifindex)); + + dns_scope_free(l->unicast_scope); + dns_scope_free(l->llmnr_ipv4_scope); + dns_scope_free(l->llmnr_ipv6_scope); + dns_scope_free(l->mdns_ipv4_scope); + dns_scope_free(l->mdns_ipv6_scope); + + free(l->state_file); + free(l->ifname); + + return mfree(l); +} + +void link_allocate_scopes(Link *l) { + bool unicast_relevant; + int r; + + assert(l); + + /* If a link that used to be relevant is no longer, or a link that did not use to be relevant now becomes + * relevant, let's reinit the learnt global DNS server information, since we might talk to different servers + * now, even if they have the same addresses as before. */ + + unicast_relevant = link_relevant(l, AF_UNSPEC, false); + if (unicast_relevant != l->unicast_relevant) { + l->unicast_relevant = unicast_relevant; + + dns_server_reset_features_all(l->manager->fallback_dns_servers); + dns_server_reset_features_all(l->manager->dns_servers); + + /* Also, flush the global unicast scope, to deal with split horizon setups, where talking through one + * interface reveals different DNS zones than through others. */ + if (l->manager->unicast_scope) + dns_cache_flush(&l->manager->unicast_scope->cache); + } + + /* And now, allocate all scopes that makes sense now if we didn't have them yet, and drop those which we don't + * need anymore */ + + if (unicast_relevant && l->dns_servers) { + if (!l->unicast_scope) { + dns_server_reset_features_all(l->dns_servers); + + r = dns_scope_new(l->manager, &l->unicast_scope, l, DNS_PROTOCOL_DNS, AF_UNSPEC); + if (r < 0) + log_link_warning_errno(l, r, "Failed to allocate DNS scope, ignoring: %m"); + } + } else + l->unicast_scope = dns_scope_free(l->unicast_scope); + + if (link_relevant(l, AF_INET, true) && + l->llmnr_support != RESOLVE_SUPPORT_NO && + l->manager->llmnr_support != RESOLVE_SUPPORT_NO) { + if (!l->llmnr_ipv4_scope) { + r = dns_scope_new(l->manager, &l->llmnr_ipv4_scope, l, DNS_PROTOCOL_LLMNR, AF_INET); + if (r < 0) + log_link_warning_errno(l, r, "Failed to allocate LLMNR IPv4 scope, ignoring: %m"); + } + } else + l->llmnr_ipv4_scope = dns_scope_free(l->llmnr_ipv4_scope); + + if (link_relevant(l, AF_INET6, true) && + l->llmnr_support != RESOLVE_SUPPORT_NO && + l->manager->llmnr_support != RESOLVE_SUPPORT_NO && + socket_ipv6_is_supported()) { + if (!l->llmnr_ipv6_scope) { + r = dns_scope_new(l->manager, &l->llmnr_ipv6_scope, l, DNS_PROTOCOL_LLMNR, AF_INET6); + if (r < 0) + log_link_warning_errno(l, r, "Failed to allocate LLMNR IPv6 scope, ignoring: %m"); + } + } else + l->llmnr_ipv6_scope = dns_scope_free(l->llmnr_ipv6_scope); + + if (link_relevant(l, AF_INET, true) && + l->mdns_support != RESOLVE_SUPPORT_NO && + l->manager->mdns_support != RESOLVE_SUPPORT_NO) { + if (!l->mdns_ipv4_scope) { + r = dns_scope_new(l->manager, &l->mdns_ipv4_scope, l, DNS_PROTOCOL_MDNS, AF_INET); + if (r < 0) + log_link_warning_errno(l, r, "Failed to allocate mDNS IPv4 scope, ignoring: %m"); + } + } else + l->mdns_ipv4_scope = dns_scope_free(l->mdns_ipv4_scope); + + if (link_relevant(l, AF_INET6, true) && + l->mdns_support != RESOLVE_SUPPORT_NO && + l->manager->mdns_support != RESOLVE_SUPPORT_NO) { + if (!l->mdns_ipv6_scope) { + r = dns_scope_new(l->manager, &l->mdns_ipv6_scope, l, DNS_PROTOCOL_MDNS, AF_INET6); + if (r < 0) + log_link_warning_errno(l, r, "Failed to allocate mDNS IPv6 scope, ignoring: %m"); + } + } else + l->mdns_ipv6_scope = dns_scope_free(l->mdns_ipv6_scope); +} + +void link_add_rrs(Link *l, bool force_remove) { + int r; + + LIST_FOREACH(addresses, a, l->addresses) + link_address_add_rrs(a, force_remove); + + if (!force_remove && + l->mdns_support == RESOLVE_SUPPORT_YES && + l->manager->mdns_support == RESOLVE_SUPPORT_YES) { + + if (l->mdns_ipv4_scope) { + r = dns_scope_add_dnssd_services(l->mdns_ipv4_scope); + if (r < 0) + log_link_warning_errno(l, r, "Failed to add IPv4 DNS-SD services, ignoring: %m"); + } + + if (l->mdns_ipv6_scope) { + r = dns_scope_add_dnssd_services(l->mdns_ipv6_scope); + if (r < 0) + log_link_warning_errno(l, r, "Failed to add IPv6 DNS-SD services, ignoring: %m"); + } + + } else { + + if (l->mdns_ipv4_scope) { + r = dns_scope_remove_dnssd_services(l->mdns_ipv4_scope); + if (r < 0) + log_link_warning_errno(l, r, "Failed to remove IPv4 DNS-SD services, ignoring: %m"); + } + + if (l->mdns_ipv6_scope) { + r = dns_scope_remove_dnssd_services(l->mdns_ipv6_scope); + if (r < 0) + log_link_warning_errno(l, r, "Failed to remove IPv6 DNS-SD services, ignoring: %m"); + } + } +} + +int link_process_rtnl(Link *l, sd_netlink_message *m) { + const char *n = NULL; + int r; + + assert(l); + assert(m); + + r = sd_rtnl_message_link_get_flags(m, &l->flags); + if (r < 0) + return r; + + (void) sd_netlink_message_read_u32(m, IFLA_MTU, &l->mtu); + (void) sd_netlink_message_read_u8(m, IFLA_OPERSTATE, &l->operstate); + + if (sd_netlink_message_read_string(m, IFLA_IFNAME, &n) >= 0 && + !streq_ptr(l->ifname, n)) { + if (l->ifname) + log_link_debug(l, "Interface name change detected: %s -> %s", l->ifname, n); + + r = free_and_strdup(&l->ifname, n); + if (r < 0) + return r; + } + + return 0; +} + +static int link_update_dns_server_one(Link *l, const char *str) { + _cleanup_free_ char *name = NULL; + int family, ifindex, r; + union in_addr_union a; + DnsServer *s; + uint16_t port; + + assert(l); + assert(str); + + r = in_addr_port_ifindex_name_from_string_auto(str, &family, &a, &port, &ifindex, &name); + if (r < 0) + return r; + + if (ifindex != 0 && ifindex != l->ifindex) + return -EINVAL; + + /* By default, the port number is determined with the transaction feature level. + * See dns_transaction_port() and dns_server_port(). */ + if (IN_SET(port, 53, 853)) + port = 0; + + s = dns_server_find(l->dns_servers, family, &a, port, 0, name); + if (s) { + dns_server_move_back_and_unmark(s); + return 0; + } + + return dns_server_new(l->manager, NULL, DNS_SERVER_LINK, l, family, &a, port, 0, name); +} + +static int link_update_dns_servers(Link *l) { + _cleanup_strv_free_ char **nameservers = NULL; + int r; + + assert(l); + + r = sd_network_link_get_dns(l->ifindex, &nameservers); + if (r == -ENODATA) { + r = 0; + goto clear; + } + if (r < 0) + goto clear; + + dns_server_mark_all(l->dns_servers); + + STRV_FOREACH(nameserver, nameservers) { + r = link_update_dns_server_one(l, *nameserver); + if (r < 0) + goto clear; + } + + dns_server_unlink_marked(l->dns_servers); + return 0; + +clear: + dns_server_unlink_all(l->dns_servers); + return r; +} + +static int link_update_default_route(Link *l) { + int r; + + assert(l); + + r = sd_network_link_get_dns_default_route(l->ifindex); + if (r == -ENODATA) { + r = 0; + goto clear; + } + if (r < 0) + goto clear; + + l->default_route = r > 0; + return 0; + +clear: + l->default_route = -1; + return r; +} + +static int link_update_llmnr_support(Link *l) { + _cleanup_free_ char *b = NULL; + int r; + + assert(l); + + l->llmnr_support = RESOLVE_SUPPORT_YES; /* yes, yes, we set it twice which is ugly */ + + r = sd_network_link_get_llmnr(l->ifindex, &b); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + r = resolve_support_from_string(b); + if (r < 0) + return r; + + l->llmnr_support = r; + return 0; +} + +static int link_update_mdns_support(Link *l) { + _cleanup_free_ char *b = NULL; + int r; + + assert(l); + + l->mdns_support = RESOLVE_SUPPORT_NO; + + r = sd_network_link_get_mdns(l->ifindex, &b); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + r = resolve_support_from_string(b); + if (r < 0) + return r; + + l->mdns_support = r; + return 0; +} + +void link_set_dns_over_tls_mode(Link *l, DnsOverTlsMode mode) { + + assert(l); + +#if ! ENABLE_DNS_OVER_TLS + if (mode != DNS_OVER_TLS_NO) + log_link_warning(l, + "DNS-over-TLS option for the link cannot be enabled or set to opportunistic " + "when systemd-resolved is built without DNS-over-TLS support. " + "Turning off DNS-over-TLS support."); + return; +#endif + + l->dns_over_tls_mode = mode; + l->unicast_scope = dns_scope_free(l->unicast_scope); +} + +static int link_update_dns_over_tls_mode(Link *l) { + _cleanup_free_ char *b = NULL; + int r; + + assert(l); + + l->dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID; + + r = sd_network_link_get_dns_over_tls(l->ifindex, &b); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + r = dns_over_tls_mode_from_string(b); + if (r < 0) + return r; + + l->dns_over_tls_mode = r; + return 0; +} + +void link_set_dnssec_mode(Link *l, DnssecMode mode) { + + assert(l); + +#if !HAVE_OPENSSL_OR_GCRYPT + if (IN_SET(mode, DNSSEC_YES, DNSSEC_ALLOW_DOWNGRADE)) + log_link_warning(l, + "DNSSEC option for the link cannot be enabled or set to allow-downgrade " + "when systemd-resolved is built without a cryptographic library. " + "Turning off DNSSEC support."); + return; +#endif + + if (l->dnssec_mode == mode) + return; + + l->dnssec_mode = mode; + l->unicast_scope = dns_scope_free(l->unicast_scope); +} + +static int link_update_dnssec_mode(Link *l) { + _cleanup_free_ char *m = NULL; + DnssecMode mode; + int r; + + assert(l); + + l->dnssec_mode = _DNSSEC_MODE_INVALID; + + r = sd_network_link_get_dnssec(l->ifindex, &m); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + mode = dnssec_mode_from_string(m); + if (mode < 0) + return mode; + + link_set_dnssec_mode(l, mode); + return 0; +} + +static int link_update_dnssec_negative_trust_anchors(Link *l) { + _cleanup_strv_free_ char **ntas = NULL; + _cleanup_set_free_free_ Set *ns = NULL; + int r; + + assert(l); + + l->dnssec_negative_trust_anchors = set_free_free(l->dnssec_negative_trust_anchors); + + r = sd_network_link_get_dnssec_negative_trust_anchors(l->ifindex, &ntas); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + ns = set_new(&dns_name_hash_ops); + if (!ns) + return -ENOMEM; + + r = set_put_strdupv(&ns, ntas); + if (r < 0) + return r; + + l->dnssec_negative_trust_anchors = TAKE_PTR(ns); + return 0; +} + +static int link_update_search_domain_one(Link *l, const char *name, bool route_only) { + DnsSearchDomain *d; + int r; + + assert(l); + assert(name); + + r = dns_search_domain_find(l->search_domains, name, &d); + if (r < 0) + return r; + if (r > 0) + dns_search_domain_move_back_and_unmark(d); + else { + r = dns_search_domain_new(l->manager, &d, DNS_SEARCH_DOMAIN_LINK, l, name); + if (r < 0) + return r; + } + + d->route_only = route_only; + return 0; +} + +static int link_update_search_domains(Link *l) { + _cleanup_strv_free_ char **sdomains = NULL, **rdomains = NULL; + int r, q; + + assert(l); + + r = sd_network_link_get_search_domains(l->ifindex, &sdomains); + if (r < 0 && r != -ENODATA) + goto clear; + + q = sd_network_link_get_route_domains(l->ifindex, &rdomains); + if (q < 0 && q != -ENODATA) { + r = q; + goto clear; + } + + if (r == -ENODATA && q == -ENODATA) { + /* networkd knows nothing about this interface, and that's fine. */ + r = 0; + goto clear; + } + + dns_search_domain_mark_all(l->search_domains); + + STRV_FOREACH(i, sdomains) { + r = link_update_search_domain_one(l, *i, false); + if (r < 0) + goto clear; + } + + STRV_FOREACH(i, rdomains) { + r = link_update_search_domain_one(l, *i, true); + if (r < 0) + goto clear; + } + + dns_search_domain_unlink_marked(l->search_domains); + return 0; + +clear: + dns_search_domain_unlink_all(l->search_domains); + return r; +} + +static int link_is_managed(Link *l) { + _cleanup_free_ char *state = NULL; + int r; + + assert(l); + + r = sd_network_link_get_setup_state(l->ifindex, &state); + if (r == -ENODATA) + return 0; + if (r < 0) + return r; + + return !STR_IN_SET(state, "pending", "initialized", "unmanaged"); +} + +static void link_enter_unmanaged(Link *l) { + assert(l); + + /* If this link used to be managed, but is now unmanaged, flush all our settings — but only once. */ + if (l->is_managed) + link_flush_settings(l); + + l->is_managed = false; +} + +static void link_read_settings(Link *l) { + struct stat st; + int r; + + assert(l); + + /* Read settings from networkd, except when networkd is not managing this interface. */ + + r = sd_network_link_get_stat(l->ifindex, &st); + if (r == -ENOENT) + return link_enter_unmanaged(l); + if (r < 0) + return (void) log_link_warning_errno(l, r, "Failed to stat() networkd's link state file, ignoring: %m"); + + if (stat_inode_unmodified(&l->networkd_state_file_stat, &st)) + /* The state file is unmodified. Not necessary to re-read settings. */ + return; + + /* Save the new stat for the next event. */ + l->networkd_state_file_stat = st; + + r = link_is_managed(l); + if (r < 0) + return (void) log_link_warning_errno(l, r, "Failed to determine whether the interface is managed, ignoring: %m"); + if (r == 0) + return link_enter_unmanaged(l); + + l->is_managed = true; + + r = network_link_get_operational_state(l->ifindex, &l->networkd_operstate); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read networkd's link operational state, ignoring: %m"); + + r = link_update_dns_servers(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read DNS servers for the interface, ignoring: %m"); + + r = link_update_llmnr_support(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read LLMNR support for the interface, ignoring: %m"); + + r = link_update_mdns_support(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read mDNS support for the interface, ignoring: %m"); + + r = link_update_dns_over_tls_mode(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read DNS-over-TLS mode for the interface, ignoring: %m"); + + r = link_update_dnssec_mode(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read DNSSEC mode for the interface, ignoring: %m"); + + r = link_update_dnssec_negative_trust_anchors(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read DNSSEC negative trust anchors for the interface, ignoring: %m"); + + r = link_update_search_domains(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read search domains for the interface, ignoring: %m"); + + r = link_update_default_route(l); + if (r < 0) + log_link_warning_errno(l, r, "Failed to read default route setting for the interface, proceeding anyway: %m"); +} + +int link_update(Link *l) { + int r; + + assert(l); + + link_read_settings(l); + r = link_load_user(l); + if (r < 0) + return r; + + if (l->llmnr_support != RESOLVE_SUPPORT_NO) { + r = manager_llmnr_start(l->manager); + if (r < 0) + return r; + } + + if (l->mdns_support != RESOLVE_SUPPORT_NO) { + r = manager_mdns_start(l->manager); + if (r < 0) + return r; + } + + link_allocate_scopes(l); + link_add_rrs(l, false); + + return 0; +} + +bool link_relevant(Link *l, int family, bool local_multicast) { + assert(l); + + /* A link is relevant for local multicast traffic if it isn't a loopback device, has a link + * beat, can do multicast and has at least one link-local (or better) IP address. + * + * A link is relevant for non-multicast traffic if it isn't a loopback device, has a link beat, and has at + * least one routable address. */ + + if ((l->flags & (IFF_LOOPBACK | IFF_DORMANT)) != 0) + return false; + + if (!FLAGS_SET(l->flags, IFF_UP | IFF_LOWER_UP)) + return false; + + if (local_multicast && + !FLAGS_SET(l->flags, IFF_MULTICAST)) + return false; + + if (!netif_has_carrier(l->operstate, l->flags)) + return false; + + if (l->is_managed && + !IN_SET(l->networkd_operstate, LINK_OPERSTATE_DEGRADED_CARRIER, LINK_OPERSTATE_DEGRADED, LINK_OPERSTATE_ROUTABLE)) + return false; + + LIST_FOREACH(addresses, a, l->addresses) + if ((family == AF_UNSPEC || a->family == family) && link_address_relevant(a, local_multicast)) + return true; + + return false; +} + +LinkAddress *link_find_address(Link *l, int family, const union in_addr_union *in_addr) { + assert(l); + + if (!IN_SET(family, AF_INET, AF_INET6)) + return NULL; + + if (!in_addr) + return NULL; + + LIST_FOREACH(addresses, a, l->addresses) + if (a->family == family && in_addr_equal(family, &a->in_addr, in_addr)) + return a; + + return NULL; +} + +DnsServer* link_set_dns_server(Link *l, DnsServer *s) { + assert(l); + + if (l->current_dns_server == s) + return s; + + if (s) + log_link_debug(l, "Switching to DNS server %s.", strna(dns_server_string_full(s))); + + dns_server_unref(l->current_dns_server); + l->current_dns_server = dns_server_ref(s); + + if (l->unicast_scope) + dns_cache_flush(&l->unicast_scope->cache); + + return s; +} + +DnsServer *link_get_dns_server(Link *l) { + assert(l); + + if (!l->current_dns_server) + link_set_dns_server(l, l->dns_servers); + + return l->current_dns_server; +} + +void link_next_dns_server(Link *l, DnsServer *if_current) { + assert(l); + + /* If the current server of the transaction is specified, and we already are at a different one, + * don't do anything */ + if (if_current && l->current_dns_server != if_current) + return; + + /* If currently have no DNS server, then don't do anything, we'll pick it lazily the next time a DNS + * server is needed. */ + if (!l->current_dns_server) + return; + + /* Change to the next one, but make sure to follow the linked list only if this server is actually + * still linked. */ + if (l->current_dns_server->linked && l->current_dns_server->servers_next) { + link_set_dns_server(l, l->current_dns_server->servers_next); + return; + } + + /* Pick the first one again, after we reached the end */ + link_set_dns_server(l, l->dns_servers); +} + +DnsOverTlsMode link_get_dns_over_tls_mode(Link *l) { + assert(l); + + if (l->dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID) + return l->dns_over_tls_mode; + + return manager_get_dns_over_tls_mode(l->manager); +} + +DnssecMode link_get_dnssec_mode(Link *l) { + assert(l); + + if (l->dnssec_mode != _DNSSEC_MODE_INVALID) + return l->dnssec_mode; + + return manager_get_dnssec_mode(l->manager); +} + +bool link_dnssec_supported(Link *l) { + DnsServer *server; + + assert(l); + + if (link_get_dnssec_mode(l) == DNSSEC_NO) + return false; + + server = link_get_dns_server(l); + if (server) + return dns_server_dnssec_supported(server); + + return true; +} + +int link_address_new(Link *l, LinkAddress **ret, int family, const union in_addr_union *in_addr) { + LinkAddress *a; + + assert(l); + assert(in_addr); + + a = new(LinkAddress, 1); + if (!a) + return -ENOMEM; + + *a = (LinkAddress) { + .family = family, + .in_addr = *in_addr, + .link = l, + .prefixlen = UCHAR_MAX, + }; + + LIST_PREPEND(addresses, l->addresses, a); + l->n_addresses++; + + if (ret) + *ret = a; + + return 0; +} + +LinkAddress *link_address_free(LinkAddress *a) { + if (!a) + return NULL; + + if (a->link) { + LIST_REMOVE(addresses, a->link->addresses, a); + + assert(a->link->n_addresses > 0); + a->link->n_addresses--; + + if (a->llmnr_address_rr) { + if (a->family == AF_INET && a->link->llmnr_ipv4_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv4_scope->zone, a->llmnr_address_rr); + else if (a->family == AF_INET6 && a->link->llmnr_ipv6_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv6_scope->zone, a->llmnr_address_rr); + } + + if (a->llmnr_ptr_rr) { + if (a->family == AF_INET && a->link->llmnr_ipv4_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv4_scope->zone, a->llmnr_ptr_rr); + else if (a->family == AF_INET6 && a->link->llmnr_ipv6_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv6_scope->zone, a->llmnr_ptr_rr); + } + + if (a->mdns_address_rr) { + if (a->family == AF_INET && a->link->mdns_ipv4_scope) + dns_zone_remove_rr(&a->link->mdns_ipv4_scope->zone, a->mdns_address_rr); + else if (a->family == AF_INET6 && a->link->mdns_ipv6_scope) + dns_zone_remove_rr(&a->link->mdns_ipv6_scope->zone, a->mdns_address_rr); + } + + if (a->mdns_ptr_rr) { + if (a->family == AF_INET && a->link->mdns_ipv4_scope) + dns_zone_remove_rr(&a->link->mdns_ipv4_scope->zone, a->mdns_ptr_rr); + else if (a->family == AF_INET6 && a->link->mdns_ipv6_scope) + dns_zone_remove_rr(&a->link->mdns_ipv6_scope->zone, a->mdns_ptr_rr); + } + } + + dns_resource_record_unref(a->llmnr_address_rr); + dns_resource_record_unref(a->llmnr_ptr_rr); + dns_resource_record_unref(a->mdns_address_rr); + dns_resource_record_unref(a->mdns_ptr_rr); + + return mfree(a); +} + +void link_address_add_rrs(LinkAddress *a, bool force_remove) { + int r; + + assert(a); + + if (a->family == AF_INET) { + + if (!force_remove && + link_address_relevant(a, true) && + a->link->llmnr_ipv4_scope && + a->link->llmnr_support == RESOLVE_SUPPORT_YES && + a->link->manager->llmnr_support == RESOLVE_SUPPORT_YES) { + + if (!a->link->manager->llmnr_host_ipv4_key) { + a->link->manager->llmnr_host_ipv4_key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, a->link->manager->llmnr_hostname); + if (!a->link->manager->llmnr_host_ipv4_key) { + r = -ENOMEM; + goto fail; + } + } + + if (!a->llmnr_address_rr) { + a->llmnr_address_rr = dns_resource_record_new(a->link->manager->llmnr_host_ipv4_key); + if (!a->llmnr_address_rr) { + r = -ENOMEM; + goto fail; + } + + a->llmnr_address_rr->a.in_addr = a->in_addr.in; + a->llmnr_address_rr->ttl = LLMNR_DEFAULT_TTL; + } + + if (!a->llmnr_ptr_rr) { + r = dns_resource_record_new_reverse(&a->llmnr_ptr_rr, a->family, &a->in_addr, a->link->manager->llmnr_hostname); + if (r < 0) + goto fail; + + a->llmnr_ptr_rr->ttl = LLMNR_DEFAULT_TTL; + } + + r = dns_zone_put(&a->link->llmnr_ipv4_scope->zone, a->link->llmnr_ipv4_scope, a->llmnr_address_rr, true); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add A record to LLMNR zone, ignoring: %m"); + + r = dns_zone_put(&a->link->llmnr_ipv4_scope->zone, a->link->llmnr_ipv4_scope, a->llmnr_ptr_rr, false); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add IPv4 PTR record to LLMNR zone, ignoring: %m"); + } else { + if (a->llmnr_address_rr) { + if (a->link->llmnr_ipv4_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv4_scope->zone, a->llmnr_address_rr); + a->llmnr_address_rr = dns_resource_record_unref(a->llmnr_address_rr); + } + + if (a->llmnr_ptr_rr) { + if (a->link->llmnr_ipv4_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv4_scope->zone, a->llmnr_ptr_rr); + a->llmnr_ptr_rr = dns_resource_record_unref(a->llmnr_ptr_rr); + } + } + + if (!force_remove && + link_address_relevant(a, true) && + a->link->mdns_ipv4_scope && + a->link->mdns_support == RESOLVE_SUPPORT_YES && + a->link->manager->mdns_support == RESOLVE_SUPPORT_YES) { + if (!a->link->manager->mdns_host_ipv4_key) { + a->link->manager->mdns_host_ipv4_key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, a->link->manager->mdns_hostname); + if (!a->link->manager->mdns_host_ipv4_key) { + r = -ENOMEM; + goto fail; + } + } + + if (!a->mdns_address_rr) { + a->mdns_address_rr = dns_resource_record_new(a->link->manager->mdns_host_ipv4_key); + if (!a->mdns_address_rr) { + r = -ENOMEM; + goto fail; + } + + a->mdns_address_rr->a.in_addr = a->in_addr.in; + a->mdns_address_rr->ttl = MDNS_DEFAULT_TTL; + } + + if (!a->mdns_ptr_rr) { + r = dns_resource_record_new_reverse(&a->mdns_ptr_rr, a->family, &a->in_addr, a->link->manager->mdns_hostname); + if (r < 0) + goto fail; + + a->mdns_ptr_rr->ttl = MDNS_DEFAULT_TTL; + } + + r = dns_zone_put(&a->link->mdns_ipv4_scope->zone, a->link->mdns_ipv4_scope, a->mdns_address_rr, true); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add A record to MDNS zone, ignoring: %m"); + + r = dns_zone_put(&a->link->mdns_ipv4_scope->zone, a->link->mdns_ipv4_scope, a->mdns_ptr_rr, false); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add IPv4 PTR record to MDNS zone, ignoring: %m"); + } else { + if (a->mdns_address_rr) { + if (a->link->mdns_ipv4_scope) + dns_zone_remove_rr(&a->link->mdns_ipv4_scope->zone, a->mdns_address_rr); + a->mdns_address_rr = dns_resource_record_unref(a->mdns_address_rr); + } + + if (a->mdns_ptr_rr) { + if (a->link->mdns_ipv4_scope) + dns_zone_remove_rr(&a->link->mdns_ipv4_scope->zone, a->mdns_ptr_rr); + a->mdns_ptr_rr = dns_resource_record_unref(a->mdns_ptr_rr); + } + } + } + + if (a->family == AF_INET6) { + + if (!force_remove && + link_address_relevant(a, true) && + a->link->llmnr_ipv6_scope && + a->link->llmnr_support == RESOLVE_SUPPORT_YES && + a->link->manager->llmnr_support == RESOLVE_SUPPORT_YES) { + + if (!a->link->manager->llmnr_host_ipv6_key) { + a->link->manager->llmnr_host_ipv6_key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_AAAA, a->link->manager->llmnr_hostname); + if (!a->link->manager->llmnr_host_ipv6_key) { + r = -ENOMEM; + goto fail; + } + } + + if (!a->llmnr_address_rr) { + a->llmnr_address_rr = dns_resource_record_new(a->link->manager->llmnr_host_ipv6_key); + if (!a->llmnr_address_rr) { + r = -ENOMEM; + goto fail; + } + + a->llmnr_address_rr->aaaa.in6_addr = a->in_addr.in6; + a->llmnr_address_rr->ttl = LLMNR_DEFAULT_TTL; + } + + if (!a->llmnr_ptr_rr) { + r = dns_resource_record_new_reverse(&a->llmnr_ptr_rr, a->family, &a->in_addr, a->link->manager->llmnr_hostname); + if (r < 0) + goto fail; + + a->llmnr_ptr_rr->ttl = LLMNR_DEFAULT_TTL; + } + + r = dns_zone_put(&a->link->llmnr_ipv6_scope->zone, a->link->llmnr_ipv6_scope, a->llmnr_address_rr, true); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add AAAA record to LLMNR zone, ignoring: %m"); + + r = dns_zone_put(&a->link->llmnr_ipv6_scope->zone, a->link->llmnr_ipv6_scope, a->llmnr_ptr_rr, false); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add IPv6 PTR record to LLMNR zone, ignoring: %m"); + } else { + if (a->llmnr_address_rr) { + if (a->link->llmnr_ipv6_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv6_scope->zone, a->llmnr_address_rr); + a->llmnr_address_rr = dns_resource_record_unref(a->llmnr_address_rr); + } + + if (a->llmnr_ptr_rr) { + if (a->link->llmnr_ipv6_scope) + dns_zone_remove_rr(&a->link->llmnr_ipv6_scope->zone, a->llmnr_ptr_rr); + a->llmnr_ptr_rr = dns_resource_record_unref(a->llmnr_ptr_rr); + } + } + + if (!force_remove && + link_address_relevant(a, true) && + a->link->mdns_ipv6_scope && + a->link->mdns_support == RESOLVE_SUPPORT_YES && + a->link->manager->mdns_support == RESOLVE_SUPPORT_YES) { + + if (!a->link->manager->mdns_host_ipv6_key) { + a->link->manager->mdns_host_ipv6_key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_AAAA, a->link->manager->mdns_hostname); + if (!a->link->manager->mdns_host_ipv6_key) { + r = -ENOMEM; + goto fail; + } + } + + if (!a->mdns_address_rr) { + a->mdns_address_rr = dns_resource_record_new(a->link->manager->mdns_host_ipv6_key); + if (!a->mdns_address_rr) { + r = -ENOMEM; + goto fail; + } + + a->mdns_address_rr->aaaa.in6_addr = a->in_addr.in6; + a->mdns_address_rr->ttl = MDNS_DEFAULT_TTL; + } + + if (!a->mdns_ptr_rr) { + r = dns_resource_record_new_reverse(&a->mdns_ptr_rr, a->family, &a->in_addr, a->link->manager->mdns_hostname); + if (r < 0) + goto fail; + + a->mdns_ptr_rr->ttl = MDNS_DEFAULT_TTL; + } + + r = dns_zone_put(&a->link->mdns_ipv6_scope->zone, a->link->mdns_ipv6_scope, a->mdns_address_rr, true); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add AAAA record to MDNS zone, ignoring: %m"); + + r = dns_zone_put(&a->link->mdns_ipv6_scope->zone, a->link->mdns_ipv6_scope, a->mdns_ptr_rr, false); + if (r < 0) + log_link_warning_errno(a->link, r, "Failed to add IPv6 PTR record to MDNS zone, ignoring: %m"); + } else { + if (a->mdns_address_rr) { + if (a->link->mdns_ipv6_scope) + dns_zone_remove_rr(&a->link->mdns_ipv6_scope->zone, a->mdns_address_rr); + a->mdns_address_rr = dns_resource_record_unref(a->mdns_address_rr); + } + + if (a->mdns_ptr_rr) { + if (a->link->mdns_ipv6_scope) + dns_zone_remove_rr(&a->link->mdns_ipv6_scope->zone, a->mdns_ptr_rr); + a->mdns_ptr_rr = dns_resource_record_unref(a->mdns_ptr_rr); + } + } + } + + return; + +fail: + log_link_debug_errno(a->link, r, "Failed to update address RRs, ignoring: %m"); +} + +int link_address_update_rtnl(LinkAddress *a, sd_netlink_message *m) { + int r; + + assert(a); + assert(m); + + r = sd_rtnl_message_addr_get_flags(m, &a->flags); + if (r < 0) + return r; + + (void) sd_rtnl_message_addr_get_prefixlen(m, &a->prefixlen); + (void) sd_rtnl_message_addr_get_scope(m, &a->scope); + + link_allocate_scopes(a->link); + link_add_rrs(a->link, false); + + return 0; +} + +bool link_address_relevant(LinkAddress *a, bool local_multicast) { + assert(a); + + if (a->flags & (IFA_F_DEPRECATED|IFA_F_TENTATIVE)) + return false; + + if (a->scope >= (local_multicast ? RT_SCOPE_HOST : RT_SCOPE_LINK)) + return false; + + return true; +} + +static bool link_needs_save(Link *l) { + assert(l); + + /* Returns true if any of the settings where set different from the default */ + + if (l->is_managed) + return false; + + if (l->llmnr_support != RESOLVE_SUPPORT_YES || + l->mdns_support != RESOLVE_SUPPORT_NO || + l->dnssec_mode != _DNSSEC_MODE_INVALID || + l->dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID) + return true; + + if (l->dns_servers || + l->search_domains) + return true; + + if (!set_isempty(l->dnssec_negative_trust_anchors)) + return true; + + if (l->default_route >= 0) + return true; + + return false; +} + +int link_save_user(Link *l) { + _cleanup_free_ char *temp_path = NULL; + _cleanup_fclose_ FILE *f = NULL; + const char *v; + int r; + + assert(l); + assert(l->state_file); + + if (!link_needs_save(l)) { + (void) unlink(l->state_file); + return 0; + } + + r = mkdir_parents(l->state_file, 0700); + if (r < 0) + goto fail; + + r = fopen_temporary(l->state_file, &f, &temp_path); + if (r < 0) + goto fail; + + (void) fchmod(fileno(f), 0644); + + fputs("# This is private data. Do not parse.\n", f); + + v = resolve_support_to_string(l->llmnr_support); + if (v) + fprintf(f, "LLMNR=%s\n", v); + + v = resolve_support_to_string(l->mdns_support); + if (v) + fprintf(f, "MDNS=%s\n", v); + + v = dnssec_mode_to_string(l->dnssec_mode); + if (v) + fprintf(f, "DNSSEC=%s\n", v); + + v = dns_over_tls_mode_to_string(l->dns_over_tls_mode); + if (v) + fprintf(f, "DNSOVERTLS=%s\n", v); + + if (l->default_route >= 0) + fprintf(f, "DEFAULT_ROUTE=%s\n", yes_no(l->default_route)); + + if (l->dns_servers) { + fputs("SERVERS=", f); + LIST_FOREACH(servers, server, l->dns_servers) { + + if (server != l->dns_servers) + fputc(' ', f); + + v = dns_server_string_full(server); + if (!v) { + r = -ENOMEM; + goto fail; + } + + fputs(v, f); + } + fputc('\n', f); + } + + if (l->search_domains) { + fputs("DOMAINS=", f); + LIST_FOREACH(domains, domain, l->search_domains) { + + if (domain != l->search_domains) + fputc(' ', f); + + if (domain->route_only) + fputc('~', f); + + fputs(DNS_SEARCH_DOMAIN_NAME(domain), f); + } + fputc('\n', f); + } + + if (!set_isempty(l->dnssec_negative_trust_anchors)) { + bool space = false; + char *nta; + + fputs("NTAS=", f); + SET_FOREACH(nta, l->dnssec_negative_trust_anchors) { + + if (space) + fputc(' ', f); + + fputs(nta, f); + space = true; + } + fputc('\n', f); + } + + r = fflush_and_check(f); + if (r < 0) + goto fail; + + if (rename(temp_path, l->state_file) < 0) { + r = -errno; + goto fail; + } + + return 0; + +fail: + (void) unlink(l->state_file); + + if (temp_path) + (void) unlink(temp_path); + + return log_link_error_errno(l, r, "Failed to save link data %s: %m", l->state_file); +} + +int link_load_user(Link *l) { + _cleanup_free_ char + *llmnr = NULL, + *mdns = NULL, + *dnssec = NULL, + *dns_over_tls = NULL, + *servers = NULL, + *domains = NULL, + *ntas = NULL, + *default_route = NULL; + + ResolveSupport s; + const char *p; + int r; + + assert(l); + assert(l->state_file); + + /* Try to load only a single time */ + if (l->loaded) + return 0; + l->loaded = true; + + if (l->is_managed) + return 0; /* if the device is managed, then networkd is our configuration source, not the bus API */ + + r = parse_env_file(NULL, l->state_file, + "LLMNR", &llmnr, + "MDNS", &mdns, + "DNSSEC", &dnssec, + "DNSOVERTLS", &dns_over_tls, + "SERVERS", &servers, + "DOMAINS", &domains, + "NTAS", &ntas, + "DEFAULT_ROUTE", &default_route); + if (r == -ENOENT) + return 0; + if (r < 0) + goto fail; + + link_flush_settings(l); + + /* If we can't recognize the LLMNR or MDNS setting we don't override the default */ + s = resolve_support_from_string(llmnr); + if (s >= 0) + l->llmnr_support = s; + + s = resolve_support_from_string(mdns); + if (s >= 0) + l->mdns_support = s; + + r = parse_boolean(default_route); + if (r >= 0) + l->default_route = r; + + /* If we can't recognize the DNSSEC setting, then set it to invalid, so that the daemon default is used. */ + l->dnssec_mode = dnssec_mode_from_string(dnssec); + + /* Same for DNSOverTLS */ + l->dns_over_tls_mode = dns_over_tls_mode_from_string(dns_over_tls); + + for (p = servers;;) { + _cleanup_free_ char *word = NULL; + + r = extract_first_word(&p, &word, NULL, 0); + if (r < 0) + goto fail; + if (r == 0) + break; + + r = link_update_dns_server_one(l, word); + if (r < 0) { + log_link_debug_errno(l, r, "Failed to load DNS server '%s', ignoring: %m", word); + continue; + } + } + + for (p = domains;;) { + _cleanup_free_ char *word = NULL; + const char *n; + bool is_route; + + r = extract_first_word(&p, &word, NULL, 0); + if (r < 0) + goto fail; + if (r == 0) + break; + + is_route = word[0] == '~'; + n = is_route ? word + 1 : word; + + r = link_update_search_domain_one(l, n, is_route); + if (r < 0) { + log_link_debug_errno(l, r, "Failed to load search domain '%s', ignoring: %m", word); + continue; + } + } + + if (ntas) { + _cleanup_set_free_free_ Set *ns = NULL; + + ns = set_new(&dns_name_hash_ops); + if (!ns) { + r = -ENOMEM; + goto fail; + } + + r = set_put_strsplit(ns, ntas, NULL, 0); + if (r < 0) + goto fail; + + l->dnssec_negative_trust_anchors = TAKE_PTR(ns); + } + + return 0; + +fail: + return log_link_error_errno(l, r, "Failed to load link data %s: %m", l->state_file); +} + +void link_remove_user(Link *l) { + assert(l); + assert(l->state_file); + + (void) unlink(l->state_file); +} + +bool link_negative_trust_anchor_lookup(Link *l, const char *name) { + int r; + + assert(l); + assert(name); + + /* Checks whether the specified domain (or any of its parent domains) are listed as per-link NTA. */ + + for (;;) { + if (set_contains(l->dnssec_negative_trust_anchors, name)) + return true; + + /* And now, let's look at the parent, and check that too */ + r = dns_name_parent(&name); + if (r < 0) + return r; + if (r == 0) + break; + } + + return false; +} diff --git a/src/resolve/resolved-link.h b/src/resolve/resolved-link.h new file mode 100644 index 0000000..b5299e0 --- /dev/null +++ b/src/resolve/resolved-link.h @@ -0,0 +1,119 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <sys/stat.h> + +#include "sd-netlink.h" + +#include "in-addr-util.h" +#include "network-util.h" +#include "ratelimit.h" +#include "resolve-util.h" + +typedef struct Link Link; +typedef struct LinkAddress LinkAddress; + +#include "resolved-dns-rr.h" +#include "resolved-dns-scope.h" +#include "resolved-dns-search-domain.h" +#include "resolved-dns-server.h" + +#define LINK_SEARCH_DOMAINS_MAX 256 +#define LINK_DNS_SERVERS_MAX 256 + +struct LinkAddress { + Link *link; + + int family; + union in_addr_union in_addr; + unsigned char prefixlen; + + unsigned char flags, scope; + + DnsResourceRecord *llmnr_address_rr; + DnsResourceRecord *llmnr_ptr_rr; + DnsResourceRecord *mdns_address_rr; + DnsResourceRecord *mdns_ptr_rr; + + LIST_FIELDS(LinkAddress, addresses); +}; + +struct Link { + Manager *manager; + + int ifindex; + unsigned flags; + + LIST_HEAD(LinkAddress, addresses); + unsigned n_addresses; + + LIST_HEAD(DnsServer, dns_servers); + DnsServer *current_dns_server; + unsigned n_dns_servers; + + LIST_HEAD(DnsSearchDomain, search_domains); + unsigned n_search_domains; + + int default_route; + + ResolveSupport llmnr_support; + ResolveSupport mdns_support; + DnsOverTlsMode dns_over_tls_mode; + DnssecMode dnssec_mode; + Set *dnssec_negative_trust_anchors; + + DnsScope *unicast_scope; + DnsScope *llmnr_ipv4_scope; + DnsScope *llmnr_ipv6_scope; + DnsScope *mdns_ipv4_scope; + DnsScope *mdns_ipv6_scope; + + struct stat networkd_state_file_stat; + LinkOperationalState networkd_operstate; + bool is_managed; + + char *ifname; + uint32_t mtu; + uint8_t operstate; + + bool loaded; + char *state_file; + + bool unicast_relevant; +}; + +int link_new(Manager *m, Link **ret, int ifindex); +Link *link_free(Link *l); +int link_process_rtnl(Link *l, sd_netlink_message *m); +int link_update(Link *l); +bool link_relevant(Link *l, int family, bool local_multicast); +LinkAddress* link_find_address(Link *l, int family, const union in_addr_union *in_addr); +void link_add_rrs(Link *l, bool force_remove); + +void link_flush_settings(Link *l); +void link_set_dnssec_mode(Link *l, DnssecMode mode); +void link_set_dns_over_tls_mode(Link *l, DnsOverTlsMode mode); +void link_allocate_scopes(Link *l); + +DnsServer* link_set_dns_server(Link *l, DnsServer *s); +DnsServer* link_get_dns_server(Link *l); +void link_next_dns_server(Link *l, DnsServer *if_current); + +DnssecMode link_get_dnssec_mode(Link *l); +bool link_dnssec_supported(Link *l); + +DnsOverTlsMode link_get_dns_over_tls_mode(Link *l); + +int link_save_user(Link *l); +int link_load_user(Link *l); +void link_remove_user(Link *l); + +int link_address_new(Link *l, LinkAddress **ret, int family, const union in_addr_union *in_addr); +LinkAddress *link_address_free(LinkAddress *a); +int link_address_update_rtnl(LinkAddress *a, sd_netlink_message *m); +bool link_address_relevant(LinkAddress *l, bool local_multicast); +void link_address_add_rrs(LinkAddress *a, bool force_remove); + +bool link_negative_trust_anchor_lookup(Link *l, const char *name); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free); diff --git a/src/resolve/resolved-llmnr.c b/src/resolve/resolved-llmnr.c new file mode 100644 index 0000000..9f5a0b5 --- /dev/null +++ b/src/resolve/resolved-llmnr.c @@ -0,0 +1,471 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <resolv.h> + +#include "errno-util.h" +#include "fd-util.h" +#include "resolved-llmnr.h" +#include "resolved-manager.h" + +void manager_llmnr_stop(Manager *m) { + assert(m); + + m->llmnr_ipv4_udp_event_source = sd_event_source_disable_unref(m->llmnr_ipv4_udp_event_source); + m->llmnr_ipv4_udp_fd = safe_close(m->llmnr_ipv4_udp_fd); + + m->llmnr_ipv6_udp_event_source = sd_event_source_disable_unref(m->llmnr_ipv6_udp_event_source); + m->llmnr_ipv6_udp_fd = safe_close(m->llmnr_ipv6_udp_fd); + + m->llmnr_ipv4_tcp_event_source = sd_event_source_disable_unref(m->llmnr_ipv4_tcp_event_source); + m->llmnr_ipv4_tcp_fd = safe_close(m->llmnr_ipv4_tcp_fd); + + m->llmnr_ipv6_tcp_event_source = sd_event_source_disable_unref(m->llmnr_ipv6_tcp_event_source); + m->llmnr_ipv6_tcp_fd = safe_close(m->llmnr_ipv6_tcp_fd); +} + +int manager_llmnr_start(Manager *m) { + int r; + + assert(m); + + if (m->llmnr_support == RESOLVE_SUPPORT_NO) + return 0; + + r = manager_llmnr_ipv4_udp_fd(m); + if (r == -EADDRINUSE) + goto eaddrinuse; + if (r < 0) + return r; + + r = manager_llmnr_ipv4_tcp_fd(m); + if (r == -EADDRINUSE) + goto eaddrinuse; + if (r < 0) + return r; + + if (socket_ipv6_is_enabled()) { + r = manager_llmnr_ipv6_udp_fd(m); + if (r == -EADDRINUSE) + goto eaddrinuse; + if (r < 0) + return r; + + r = manager_llmnr_ipv6_tcp_fd(m); + if (r == -EADDRINUSE) + goto eaddrinuse; + if (r < 0) + return r; + } + + return 0; + +eaddrinuse: + log_warning("Another LLMNR responder prohibits binding the socket to the same port. Turning off LLMNR support."); + m->llmnr_support = RESOLVE_SUPPORT_NO; + manager_llmnr_stop(m); + + return 0; +} + +static int on_llmnr_packet(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + DnsTransaction *t = NULL; + Manager *m = ASSERT_PTR(userdata); + DnsScope *scope; + int r; + + assert(s); + assert(fd >= 0); + + r = manager_recv(m, fd, DNS_PROTOCOL_LLMNR, &p); + if (r <= 0) + return r; + + if (manager_packet_from_local_address(m, p)) + return 0; + + scope = manager_find_scope(m, p); + if (!scope) { + log_debug("Got LLMNR UDP packet on unknown scope. Ignoring."); + return 0; + } + + if (dns_packet_validate_reply(p) > 0) { + log_debug("Got LLMNR UDP reply packet for id %u", DNS_PACKET_ID(p)); + + dns_scope_check_conflicts(scope, p); + + t = hashmap_get(m->dns_transactions, UINT_TO_PTR(DNS_PACKET_ID(p))); + if (t) + dns_transaction_process_reply(t, p, false); + + } else if (dns_packet_validate_query(p) > 0) { + log_debug("Got LLMNR UDP query packet for id %u", DNS_PACKET_ID(p)); + + dns_scope_process_query(scope, NULL, p); + } else + log_debug("Invalid LLMNR UDP packet, ignoring."); + + return 0; +} + +static int set_llmnr_common_socket_options(int fd, int family) { + int r; + + r = socket_set_recvpktinfo(fd, family, true); + if (r < 0) + return r; + + r = socket_set_recvttl(fd, family, true); + if (r < 0) + return r; + + return 0; +} + +static int set_llmnr_common_udp_socket_options(int fd, int family) { + int r; + + /* RFC 4795, section 2.5 recommends setting the TTL of UDP packets to 255. */ + r = socket_set_ttl(fd, family, 255); + if (r < 0) + return r; + + return 0; +} + +int manager_llmnr_ipv4_udp_fd(Manager *m) { + union sockaddr_union sa = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(LLMNR_PORT), + }; + _cleanup_close_ int s = -1; + int r; + + assert(m); + + if (m->llmnr_ipv4_udp_fd >= 0) + return m->llmnr_ipv4_udp_fd; + + s = socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (s < 0) + return log_error_errno(errno, "LLMNR-IPv4(UDP): Failed to create socket: %m"); + + r = set_llmnr_common_socket_options(s, AF_INET); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set common socket options: %m"); + + r = set_llmnr_common_udp_socket_options(s, AF_INET); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set common UDP socket options: %m"); + + r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_TTL, 255); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_MULTICAST_TTL: %m"); + + r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_LOOP, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_MULTICAST_LOOP: %m"); + + /* Disable Don't-Fragment bit in the IP header */ + r = setsockopt_int(s, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DONT); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_MTU_DISCOVER: %m"); + + /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */ + r = bind(s, &sa.sa, sizeof(sa.in)); + if (r < 0) { + if (errno != EADDRINUSE) + return log_error_errno(errno, "LLMNR-IPv4(UDP): Failed to bind socket: %m"); + + log_warning("LLMNR-IPv4(UDP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers."); + + /* try again with SO_REUSEADDR */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set SO_REUSEADDR: %m"); + + r = bind(s, &sa.sa, sizeof(sa.in)); + if (r < 0) + return log_error_errno(errno, "LLMNR-IPv4(UDP): Failed to bind socket: %m"); + } else { + /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set SO_REUSEADDR: %m"); + } + + r = sd_event_add_io(m->event, &m->llmnr_ipv4_udp_event_source, s, EPOLLIN, on_llmnr_packet, m); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to create event source: %m"); + + (void) sd_event_source_set_description(m->llmnr_ipv4_udp_event_source, "llmnr-ipv4-udp"); + + return m->llmnr_ipv4_udp_fd = TAKE_FD(s); +} + +int manager_llmnr_ipv6_udp_fd(Manager *m) { + union sockaddr_union sa = { + .in6.sin6_family = AF_INET6, + .in6.sin6_port = htobe16(LLMNR_PORT), + }; + _cleanup_close_ int s = -1; + int r; + + assert(m); + + if (m->llmnr_ipv6_udp_fd >= 0) + return m->llmnr_ipv6_udp_fd; + + s = socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (s < 0) + return log_error_errno(errno, "LLMNR-IPv6(UDP): Failed to create socket: %m"); + + r = set_llmnr_common_socket_options(s, AF_INET6); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set common socket options: %m"); + + r = set_llmnr_common_udp_socket_options(s, AF_INET6); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set common UDP socket options: %m"); + + /* RFC 4795, section 2.5 recommends setting the TTL of UDP packets to 255. */ + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, 255); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_MULTICAST_HOPS: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_MULTICAST_LOOP: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_V6ONLY, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_V6ONLY: %m"); + + /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */ + r = bind(s, &sa.sa, sizeof(sa.in6)); + if (r < 0) { + if (errno != EADDRINUSE) + return log_error_errno(errno, "LLMNR-IPv6(UDP): Failed to bind socket: %m"); + + log_warning("LLMNR-IPv6(UDP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers."); + + /* try again with SO_REUSEADDR */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set SO_REUSEADDR: %m"); + + r = bind(s, &sa.sa, sizeof(sa.in6)); + if (r < 0) + return log_error_errno(errno, "LLMNR-IPv6(UDP): Failed to bind socket: %m"); + } else { + /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set SO_REUSEADDR: %m"); + } + + r = sd_event_add_io(m->event, &m->llmnr_ipv6_udp_event_source, s, EPOLLIN, on_llmnr_packet, m); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to create event source: %m"); + + (void) sd_event_source_set_description(m->llmnr_ipv6_udp_event_source, "llmnr-ipv6-udp"); + + return m->llmnr_ipv6_udp_fd = TAKE_FD(s); +} + +static int on_llmnr_stream_packet(DnsStream *s, DnsPacket *p) { + DnsScope *scope; + + assert(s); + assert(s->manager); + assert(p); + + scope = manager_find_scope(s->manager, p); + if (!scope) + log_debug("Got LLMNR TCP packet on unknown scope. Ignoring."); + else if (dns_packet_validate_query(p) > 0) { + log_debug("Got LLMNR TCP query packet for id %u", DNS_PACKET_ID(p)); + + dns_scope_process_query(scope, s, p); + } else + log_debug("Invalid LLMNR TCP packet, ignoring."); + + return 0; +} + +static int on_llmnr_stream(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + DnsStream *stream; + Manager *m = userdata; + int cfd, r; + + cfd = accept4(fd, NULL, NULL, SOCK_NONBLOCK|SOCK_CLOEXEC); + if (cfd < 0) { + if (ERRNO_IS_ACCEPT_AGAIN(errno)) + return 0; + + return -errno; + } + + /* We don't configure a "complete" handler here, we rely on the default handler, thus freeing it */ + r = dns_stream_new(m, &stream, DNS_STREAM_LLMNR_RECV, DNS_PROTOCOL_LLMNR, cfd, NULL, + on_llmnr_stream_packet, NULL, DNS_STREAM_DEFAULT_TIMEOUT_USEC); + if (r < 0) { + safe_close(cfd); + return r; + } + + return 0; +} + +static int set_llmnr_common_tcp_socket_options(int fd, int family) { + int r; + + /* RFC 4795, section 2.5. requires setting the TTL of TCP streams to 1 */ + r = socket_set_ttl(fd, family, 1); + if (r < 0) + return r; + + r = setsockopt_int(fd, IPPROTO_TCP, TCP_FASTOPEN, 5); /* Everybody appears to pick qlen=5, let's do the same here. */ + if (r < 0) + log_debug_errno(r, "Failed to enable TCP_FASTOPEN on TCP listening socket, ignoring: %m"); + + r = setsockopt_int(fd, IPPROTO_TCP, TCP_NODELAY, true); + if (r < 0) + log_debug_errno(r, "Failed to enable TCP_NODELAY mode, ignoring: %m"); + + return 0; +} + +int manager_llmnr_ipv4_tcp_fd(Manager *m) { + union sockaddr_union sa = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(LLMNR_PORT), + }; + _cleanup_close_ int s = -1; + int r; + + assert(m); + + if (m->llmnr_ipv4_tcp_fd >= 0) + return m->llmnr_ipv4_tcp_fd; + + s = socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (s < 0) + return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to create socket: %m"); + + r = set_llmnr_common_socket_options(s, AF_INET); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set common socket options: %m"); + + r = set_llmnr_common_tcp_socket_options(s, AF_INET); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set common TCP socket options: %m"); + + /* Disable Don't-Fragment bit in the IP header */ + r = setsockopt_int(s, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DONT); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set IP_MTU_DISCOVER: %m"); + + /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */ + r = bind(s, &sa.sa, sizeof(sa.in)); + if (r < 0) { + if (errno != EADDRINUSE) + return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to bind socket: %m"); + + log_warning("LLMNR-IPv4(TCP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers."); + + /* try again with SO_REUSEADDR */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set SO_REUSEADDR: %m"); + + r = bind(s, &sa.sa, sizeof(sa.in)); + if (r < 0) + return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to bind socket: %m"); + } else { + /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set SO_REUSEADDR: %m"); + } + + r = listen(s, SOMAXCONN_DELUXE); + if (r < 0) + return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to listen the stream: %m"); + + r = sd_event_add_io(m->event, &m->llmnr_ipv4_tcp_event_source, s, EPOLLIN, on_llmnr_stream, m); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to create event source: %m"); + + (void) sd_event_source_set_description(m->llmnr_ipv4_tcp_event_source, "llmnr-ipv4-tcp"); + + return m->llmnr_ipv4_tcp_fd = TAKE_FD(s); +} + +int manager_llmnr_ipv6_tcp_fd(Manager *m) { + union sockaddr_union sa = { + .in6.sin6_family = AF_INET6, + .in6.sin6_port = htobe16(LLMNR_PORT), + }; + _cleanup_close_ int s = -1; + int r; + + assert(m); + + if (m->llmnr_ipv6_tcp_fd >= 0) + return m->llmnr_ipv6_tcp_fd; + + s = socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (s < 0) + return log_error_errno(errno, "LLMNR-IPv6(TCP): Failed to create socket: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_V6ONLY, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set IPV6_V6ONLY: %m"); + + r = set_llmnr_common_socket_options(s, AF_INET6); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set common socket options: %m"); + + r = set_llmnr_common_tcp_socket_options(s, AF_INET6); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set common TCP socket options: %m"); + + /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */ + r = bind(s, &sa.sa, sizeof(sa.in6)); + if (r < 0) { + if (errno != EADDRINUSE) + return log_error_errno(errno, "LLMNR-IPv6(TCP): Failed to bind socket: %m"); + + log_warning("LLMNR-IPv6(TCP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers."); + + /* try again with SO_REUSEADDR */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set SO_REUSEADDR: %m"); + + r = bind(s, &sa.sa, sizeof(sa.in6)); + if (r < 0) + return log_error_errno(errno, "LLMNR-IPv6(TCP): Failed to bind socket: %m"); + } else { + /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set SO_REUSEADDR: %m"); + } + + r = listen(s, SOMAXCONN_DELUXE); + if (r < 0) + return log_error_errno(errno, "LLMNR-IPv6(TCP): Failed to listen the stream: %m"); + + r = sd_event_add_io(m->event, &m->llmnr_ipv6_tcp_event_source, s, EPOLLIN, on_llmnr_stream, m); + if (r < 0) + return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to create event source: %m"); + + (void) sd_event_source_set_description(m->llmnr_ipv6_tcp_event_source, "llmnr-ipv6-tcp"); + + return m->llmnr_ipv6_tcp_fd = TAKE_FD(s); +} diff --git a/src/resolve/resolved-llmnr.h b/src/resolve/resolved-llmnr.h new file mode 100644 index 0000000..4cdd260 --- /dev/null +++ b/src/resolve/resolved-llmnr.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "resolved-manager.h" + +#define LLMNR_PORT 5355 + +int manager_llmnr_ipv4_udp_fd(Manager *m); +int manager_llmnr_ipv6_udp_fd(Manager *m); +int manager_llmnr_ipv4_tcp_fd(Manager *m); +int manager_llmnr_ipv6_tcp_fd(Manager *m); + +void manager_llmnr_stop(Manager *m); +int manager_llmnr_start(Manager *m); diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c new file mode 100644 index 0000000..34b785e --- /dev/null +++ b/src/resolve/resolved-manager.c @@ -0,0 +1,1775 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <fcntl.h> +#include <netinet/in.h> +#include <poll.h> +#include <sys/ioctl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "af-list.h" +#include "alloc-util.h" +#include "bus-polkit.h" +#include "dirent-util.h" +#include "dns-domain.h" +#include "event-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "hostname-util.h" +#include "idn-util.h" +#include "io-util.h" +#include "missing_network.h" +#include "missing_socket.h" +#include "netlink-util.h" +#include "ordered-set.h" +#include "parse-util.h" +#include "random-util.h" +#include "resolved-bus.h" +#include "resolved-conf.h" +#include "resolved-dns-stub.h" +#include "resolved-dnssd.h" +#include "resolved-etc-hosts.h" +#include "resolved-llmnr.h" +#include "resolved-manager.h" +#include "resolved-mdns.h" +#include "resolved-resolv-conf.h" +#include "resolved-util.h" +#include "resolved-varlink.h" +#include "socket-util.h" +#include "string-table.h" +#include "string-util.h" +#include "utf8.h" + +#define SEND_TIMEOUT_USEC (200 * USEC_PER_MSEC) + +static int manager_process_link(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + uint16_t type; + Link *l; + int ifindex, r; + + assert(rtnl); + assert(mm); + + r = sd_netlink_message_get_type(mm, &type); + if (r < 0) + goto fail; + + r = sd_rtnl_message_link_get_ifindex(mm, &ifindex); + if (r < 0) + goto fail; + + l = hashmap_get(m->links, INT_TO_PTR(ifindex)); + + switch (type) { + + case RTM_NEWLINK:{ + bool is_new = !l; + + if (!l) { + r = link_new(m, &l, ifindex); + if (r < 0) + goto fail; + } + + r = link_process_rtnl(l, mm); + if (r < 0) + goto fail; + + r = link_update(l); + if (r < 0) + goto fail; + + if (is_new) + log_debug("Found new link %i/%s", ifindex, l->ifname); + + break; + } + + case RTM_DELLINK: + if (l) { + log_debug("Removing link %i/%s", l->ifindex, l->ifname); + link_remove_user(l); + link_free(l); + } + + break; + } + + return 0; + +fail: + log_warning_errno(r, "Failed to process RTNL link message: %m"); + return 0; +} + +static int manager_process_address(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + union in_addr_union address; + uint16_t type; + int r, ifindex, family; + LinkAddress *a; + Link *l; + + assert(rtnl); + assert(mm); + + r = sd_netlink_message_get_type(mm, &type); + if (r < 0) + goto fail; + + r = sd_rtnl_message_addr_get_ifindex(mm, &ifindex); + if (r < 0) + goto fail; + + l = hashmap_get(m->links, INT_TO_PTR(ifindex)); + if (!l) + return 0; + + r = sd_rtnl_message_addr_get_family(mm, &family); + if (r < 0) + goto fail; + + switch (family) { + + case AF_INET: + r = sd_netlink_message_read_in_addr(mm, IFA_LOCAL, &address.in); + if (r < 0) { + r = sd_netlink_message_read_in_addr(mm, IFA_ADDRESS, &address.in); + if (r < 0) + goto fail; + } + + break; + + case AF_INET6: + r = sd_netlink_message_read_in6_addr(mm, IFA_LOCAL, &address.in6); + if (r < 0) { + r = sd_netlink_message_read_in6_addr(mm, IFA_ADDRESS, &address.in6); + if (r < 0) + goto fail; + } + + break; + + default: + return 0; + } + + a = link_find_address(l, family, &address); + + switch (type) { + + case RTM_NEWADDR: + + if (!a) { + r = link_address_new(l, &a, family, &address); + if (r < 0) + return r; + } + + r = link_address_update_rtnl(a, mm); + if (r < 0) + return r; + + break; + + case RTM_DELADDR: + link_address_free(a); + break; + } + + return 0; + +fail: + log_warning_errno(r, "Failed to process RTNL address message: %m"); + return 0; +} + +static int manager_rtnl_listen(Manager *m) { + _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; + int r; + + assert(m); + + /* First, subscribe to interfaces coming and going */ + r = sd_netlink_open(&m->rtnl); + if (r < 0) + return r; + + r = sd_netlink_attach_event(m->rtnl, m->event, SD_EVENT_PRIORITY_IMPORTANT); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, NULL, RTM_NEWLINK, manager_process_link, NULL, m, "resolve-NEWLINK"); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, NULL, RTM_DELLINK, manager_process_link, NULL, m, "resolve-DELLINK"); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, NULL, RTM_NEWADDR, manager_process_address, NULL, m, "resolve-NEWADDR"); + if (r < 0) + return r; + + r = sd_netlink_add_match(m->rtnl, NULL, RTM_DELADDR, manager_process_address, NULL, m, "resolve-DELADDR"); + if (r < 0) + return r; + + /* Then, enumerate all links */ + r = sd_rtnl_message_new_link(m->rtnl, &req, RTM_GETLINK, 0); + if (r < 0) + return r; + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return r; + + r = sd_netlink_call(m->rtnl, req, 0, &reply); + if (r < 0) + return r; + + for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) { + r = manager_process_link(m->rtnl, i, m); + if (r < 0) + return r; + } + + req = sd_netlink_message_unref(req); + reply = sd_netlink_message_unref(reply); + + /* Finally, enumerate all addresses, too */ + r = sd_rtnl_message_new_addr(m->rtnl, &req, RTM_GETADDR, 0, AF_UNSPEC); + if (r < 0) + return r; + + r = sd_netlink_message_set_request_dump(req, true); + if (r < 0) + return r; + + r = sd_netlink_call(m->rtnl, req, 0, &reply); + if (r < 0) + return r; + + for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) { + r = manager_process_address(m->rtnl, i, m); + if (r < 0) + return r; + } + + return r; +} + +static int on_network_event(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + Link *l; + int r; + + sd_network_monitor_flush(m->network_monitor); + + HASHMAP_FOREACH(l, m->links) { + r = link_update(l); + if (r < 0) + log_warning_errno(r, "Failed to update monitor information for %i: %m", l->ifindex); + } + + (void) manager_write_resolv_conf(m); + (void) manager_send_changed(m, "DNS"); + + return 0; +} + +static int manager_network_monitor_listen(Manager *m) { + int r, fd, events; + + assert(m); + + r = sd_network_monitor_new(&m->network_monitor, NULL); + if (r < 0) + return r; + + fd = sd_network_monitor_get_fd(m->network_monitor); + if (fd < 0) + return fd; + + events = sd_network_monitor_get_events(m->network_monitor); + if (events < 0) + return events; + + r = sd_event_add_io(m->event, &m->network_event_source, fd, events, &on_network_event, m); + if (r < 0) + return r; + + r = sd_event_source_set_priority(m->network_event_source, SD_EVENT_PRIORITY_IMPORTANT+5); + if (r < 0) + return r; + + (void) sd_event_source_set_description(m->network_event_source, "network-monitor"); + + return 0; +} + +static int manager_clock_change_listen(Manager *m); + +static int on_clock_change(sd_event_source *source, int fd, uint32_t revents, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + /* The clock has changed, let's flush all caches. Why that? That's because DNSSEC validation takes + * the system clock into consideration, and if the clock changes the old validations might have been + * wrong. Let's redo all validation with the new, correct time. + * + * (Also, this is triggered after system suspend, which is also a good reason to drop caches, since + * we might be connected to a different network now without this being visible in a dropped link + * carrier or so.) */ + + log_info("Clock change detected. Flushing caches."); + manager_flush_caches(m, LOG_DEBUG /* downgrade the functions own log message, since we already logged here at LOG_INFO level */); + + /* The clock change timerfd is unusable after it triggered once, create a new one. */ + return manager_clock_change_listen(m); +} + +static int manager_clock_change_listen(Manager *m) { + int r; + + assert(m); + + m->clock_change_event_source = sd_event_source_disable_unref(m->clock_change_event_source); + + r = event_add_time_change(m->event, &m->clock_change_event_source, on_clock_change, m); + if (r < 0) + return log_error_errno(r, "Failed to create clock change event source: %m"); + + return 0; +} + +static int determine_hostnames(char **full_hostname, char **llmnr_hostname, char **mdns_hostname) { + _cleanup_free_ char *h = NULL, *n = NULL; + int r; + + assert(full_hostname); + assert(llmnr_hostname); + assert(mdns_hostname); + + r = resolve_system_hostname(&h, &n); + if (r < 0) + return r; + + r = dns_name_concat(n, "local", 0, mdns_hostname); + if (r < 0) + return log_error_errno(r, "Failed to determine mDNS hostname: %m"); + + *llmnr_hostname = TAKE_PTR(n); + *full_hostname = TAKE_PTR(h); + + return 0; +} + +static char* fallback_hostname(void) { + + /* Determine the fall back hostname. For exposing this system to the outside world, we cannot have it + * to be "localhost" even if that's the default hostname. In this case, let's revert to "linux" + * instead. */ + + _cleanup_free_ char *n = get_default_hostname(); + if (!n) + return NULL; + + if (is_localhost(n)) + return strdup("linux"); + + return TAKE_PTR(n); +} + +static int make_fallback_hostnames(char **full_hostname, char **llmnr_hostname, char **mdns_hostname) { + _cleanup_free_ char *h = NULL, *n = NULL, *m = NULL; + char label[DNS_LABEL_MAX]; + const char *p; + int r; + + assert(full_hostname); + assert(llmnr_hostname); + assert(mdns_hostname); + + p = h = fallback_hostname(); + if (!h) + return log_oom(); + + r = dns_label_unescape(&p, label, sizeof label, 0); + if (r < 0) + return log_error_errno(r, "Failed to unescape fallback hostname: %m"); + + assert(r > 0); /* The fallback hostname must have at least one label */ + + r = dns_label_escape_new(label, r, &n); + if (r < 0) + return log_error_errno(r, "Failed to escape fallback hostname: %m"); + + r = dns_name_concat(n, "local", 0, &m); + if (r < 0) + return log_error_errno(r, "Failed to concatenate mDNS hostname: %m"); + + *llmnr_hostname = TAKE_PTR(n); + *mdns_hostname = TAKE_PTR(m); + *full_hostname = TAKE_PTR(h); + + return 0; +} + +static int on_hostname_change(sd_event_source *es, int fd, uint32_t revents, void *userdata) { + _cleanup_free_ char *full_hostname = NULL, *llmnr_hostname = NULL, *mdns_hostname = NULL; + Manager *m = ASSERT_PTR(userdata); + bool llmnr_hostname_changed; + int r; + + r = determine_hostnames(&full_hostname, &llmnr_hostname, &mdns_hostname); + if (r < 0) { + log_warning_errno(r, "Failed to determine the local hostname and LLMNR/mDNS names, ignoring: %m"); + return 0; /* ignore invalid hostnames */ + } + + llmnr_hostname_changed = !streq(llmnr_hostname, m->llmnr_hostname); + if (streq(full_hostname, m->full_hostname) && + !llmnr_hostname_changed && + streq(mdns_hostname, m->mdns_hostname)) + return 0; + + log_info("System hostname changed to '%s'.", full_hostname); + + free_and_replace(m->full_hostname, full_hostname); + free_and_replace(m->llmnr_hostname, llmnr_hostname); + free_and_replace(m->mdns_hostname, mdns_hostname); + + manager_refresh_rrs(m); + (void) manager_send_changed(m, "LLMNRHostname"); + + return 0; +} + +static int manager_watch_hostname(Manager *m) { + int r; + + assert(m); + + m->hostname_fd = open("/proc/sys/kernel/hostname", + O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY); + if (m->hostname_fd < 0) { + log_warning_errno(errno, "Failed to watch hostname: %m"); + return 0; + } + + r = sd_event_add_io(m->event, &m->hostname_event_source, m->hostname_fd, 0, on_hostname_change, m); + if (r < 0) { + if (r == -EPERM) + /* kernels prior to 3.2 don't support polling this file. Ignore the failure. */ + m->hostname_fd = safe_close(m->hostname_fd); + else + return log_error_errno(r, "Failed to add hostname event source: %m"); + } + + (void) sd_event_source_set_description(m->hostname_event_source, "hostname"); + + r = determine_hostnames(&m->full_hostname, &m->llmnr_hostname, &m->mdns_hostname); + if (r < 0) { + _cleanup_free_ char *d = NULL; + + d = fallback_hostname(); + if (!d) + return log_oom(); + + log_info("Defaulting to hostname '%s'.", d); + + r = make_fallback_hostnames(&m->full_hostname, &m->llmnr_hostname, &m->mdns_hostname); + if (r < 0) + return r; + } else + log_info("Using system hostname '%s'.", m->full_hostname); + + return 0; +} + +static int manager_sigusr1(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + _cleanup_free_ char *buffer = NULL; + _cleanup_fclose_ FILE *f = NULL; + Manager *m = ASSERT_PTR(userdata); + size_t size = 0; + Link *l; + + assert(s); + assert(si); + + f = open_memstream_unlocked(&buffer, &size); + if (!f) + return log_oom(); + + LIST_FOREACH(scopes, scope, m->dns_scopes) + dns_scope_dump(scope, f); + + LIST_FOREACH(servers, server, m->dns_servers) + dns_server_dump(server, f); + LIST_FOREACH(servers, server, m->fallback_dns_servers) + dns_server_dump(server, f); + HASHMAP_FOREACH(l, m->links) + LIST_FOREACH(servers, server, l->dns_servers) + dns_server_dump(server, f); + + if (fflush_and_check(f) < 0) + return log_oom(); + + log_dump(LOG_INFO, buffer); + return 0; +} + +static int manager_sigusr2(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + assert(si); + + manager_flush_caches(m, LOG_INFO); + + return 0; +} + +static int manager_sigrtmin1(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + assert(si); + + manager_reset_server_features(m); + return 0; +} + +int manager_new(Manager **ret) { + _cleanup_(manager_freep) Manager *m = NULL; + int r; + + assert(ret); + + m = new(Manager, 1); + if (!m) + return -ENOMEM; + + *m = (Manager) { + .llmnr_ipv4_udp_fd = -1, + .llmnr_ipv6_udp_fd = -1, + .llmnr_ipv4_tcp_fd = -1, + .llmnr_ipv6_tcp_fd = -1, + .mdns_ipv4_fd = -1, + .mdns_ipv6_fd = -1, + .hostname_fd = -1, + + .llmnr_support = DEFAULT_LLMNR_MODE, + .mdns_support = DEFAULT_MDNS_MODE, + .dnssec_mode = DEFAULT_DNSSEC_MODE, + .dns_over_tls_mode = DEFAULT_DNS_OVER_TLS_MODE, + .enable_cache = DNS_CACHE_MODE_YES, + .dns_stub_listener_mode = DNS_STUB_LISTENER_YES, + .read_resolv_conf = true, + .need_builtin_fallbacks = true, + .etc_hosts_last = USEC_INFINITY, + .read_etc_hosts = true, + }; + + r = dns_trust_anchor_load(&m->trust_anchor); + if (r < 0) + return r; + + r = manager_parse_config_file(m); + if (r < 0) + log_warning_errno(r, "Failed to parse configuration file: %m"); + +#if ENABLE_DNS_OVER_TLS + r = dnstls_manager_init(m); + if (r < 0) + return r; +#endif + + r = sd_event_default(&m->event); + if (r < 0) + return r; + + (void) sd_event_add_signal(m->event, NULL, SIGTERM, NULL, NULL); + (void) sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL); + + (void) sd_event_set_watchdog(m->event, true); + + r = manager_watch_hostname(m); + if (r < 0) + return r; + + r = dnssd_load(m); + if (r < 0) + log_warning_errno(r, "Failed to load DNS-SD configuration files: %m"); + + r = dns_scope_new(m, &m->unicast_scope, NULL, DNS_PROTOCOL_DNS, AF_UNSPEC); + if (r < 0) + return r; + + r = manager_network_monitor_listen(m); + if (r < 0) + return r; + + r = manager_rtnl_listen(m); + if (r < 0) + return r; + + r = manager_clock_change_listen(m); + if (r < 0) + return r; + + r = manager_connect_bus(m); + if (r < 0) + return r; + + (void) sd_event_add_signal(m->event, &m->sigusr1_event_source, SIGUSR1, manager_sigusr1, m); + (void) sd_event_add_signal(m->event, &m->sigusr2_event_source, SIGUSR2, manager_sigusr2, m); + (void) sd_event_add_signal(m->event, &m->sigrtmin1_event_source, SIGRTMIN+1, manager_sigrtmin1, m); + + manager_cleanup_saved_user(m); + + *ret = TAKE_PTR(m); + + return 0; +} + +int manager_start(Manager *m) { + int r; + + assert(m); + + r = manager_dns_stub_start(m); + if (r < 0) + return r; + + r = manager_varlink_init(m); + if (r < 0) + return r; + + return 0; +} + +Manager *manager_free(Manager *m) { + Link *l; + DnssdService *s; + + if (!m) + return NULL; + + dns_server_unlink_all(m->dns_servers); + dns_server_unlink_all(m->fallback_dns_servers); + dns_search_domain_unlink_all(m->search_domains); + + while ((l = hashmap_first(m->links))) + link_free(l); + + while (m->dns_queries) + dns_query_free(m->dns_queries); + + m->stub_queries_by_packet = hashmap_free(m->stub_queries_by_packet); + + dns_scope_free(m->unicast_scope); + + /* At this point only orphaned streams should remain. All others should have been freed already by their + * owners */ + while (m->dns_streams) + dns_stream_unref(m->dns_streams); + +#if ENABLE_DNS_OVER_TLS + dnstls_manager_free(m); +#endif + + hashmap_free(m->links); + hashmap_free(m->dns_transactions); + + sd_event_source_unref(m->network_event_source); + sd_network_monitor_unref(m->network_monitor); + + sd_netlink_unref(m->rtnl); + sd_event_source_unref(m->rtnl_event_source); + sd_event_source_unref(m->clock_change_event_source); + + manager_llmnr_stop(m); + manager_mdns_stop(m); + manager_dns_stub_stop(m); + manager_varlink_done(m); + + manager_socket_graveyard_clear(m); + + ordered_set_free(m->dns_extra_stub_listeners); + + bus_verify_polkit_async_registry_free(m->polkit_registry); + + sd_bus_flush_close_unref(m->bus); + + sd_event_source_unref(m->sigusr1_event_source); + sd_event_source_unref(m->sigusr2_event_source); + sd_event_source_unref(m->sigrtmin1_event_source); + + dns_resource_key_unref(m->llmnr_host_ipv4_key); + dns_resource_key_unref(m->llmnr_host_ipv6_key); + dns_resource_key_unref(m->mdns_host_ipv4_key); + dns_resource_key_unref(m->mdns_host_ipv6_key); + + sd_event_source_unref(m->hostname_event_source); + safe_close(m->hostname_fd); + + sd_event_unref(m->event); + + free(m->full_hostname); + free(m->llmnr_hostname); + free(m->mdns_hostname); + + while ((s = hashmap_first(m->dnssd_services))) + dnssd_service_free(s); + hashmap_free(m->dnssd_services); + + dns_trust_anchor_flush(&m->trust_anchor); + manager_etc_hosts_flush(m); + + return mfree(m); +} + +int manager_recv(Manager *m, int fd, DnsProtocol protocol, DnsPacket **ret) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + CMSG_BUFFER_TYPE(CMSG_SPACE(MAXSIZE(struct in_pktinfo, struct in6_pktinfo)) + + CMSG_SPACE(int) /* ttl/hoplimit */ + + EXTRA_CMSG_SPACE /* kernel appears to require extra buffer space */) control; + union sockaddr_union sa; + struct iovec iov; + struct msghdr mh = { + .msg_name = &sa.sa, + .msg_namelen = sizeof(sa), + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = &control, + .msg_controllen = sizeof(control), + }; + struct cmsghdr *cmsg; + ssize_t ms, l; + int r; + + assert(m); + assert(fd >= 0); + assert(ret); + + ms = next_datagram_size_fd(fd); + if (ms < 0) + return ms; + + r = dns_packet_new(&p, protocol, ms, DNS_PACKET_SIZE_MAX); + if (r < 0) + return r; + + iov = IOVEC_MAKE(DNS_PACKET_DATA(p), p->allocated); + + l = recvmsg_safe(fd, &mh, 0); + if (l < 0) { + if (ERRNO_IS_TRANSIENT(l)) + return 0; + return l; + } + if (l == 0) + return 0; + + assert(!(mh.msg_flags & MSG_TRUNC)); + + p->size = (size_t) l; + + p->family = sa.sa.sa_family; + p->ipproto = IPPROTO_UDP; + if (p->family == AF_INET) { + p->sender.in = sa.in.sin_addr; + p->sender_port = be16toh(sa.in.sin_port); + } else if (p->family == AF_INET6) { + p->sender.in6 = sa.in6.sin6_addr; + p->sender_port = be16toh(sa.in6.sin6_port); + p->ifindex = sa.in6.sin6_scope_id; + } else + return -EAFNOSUPPORT; + + p->timestamp = now(CLOCK_BOOTTIME); + + CMSG_FOREACH(cmsg, &mh) { + + if (cmsg->cmsg_level == IPPROTO_IPV6) { + assert(p->family == AF_INET6); + + switch (cmsg->cmsg_type) { + + case IPV6_PKTINFO: { + struct in6_pktinfo *i = (struct in6_pktinfo*) CMSG_DATA(cmsg); + + if (p->ifindex <= 0) + p->ifindex = i->ipi6_ifindex; + + p->destination.in6 = i->ipi6_addr; + break; + } + + case IPV6_HOPLIMIT: + p->ttl = *(int *) CMSG_DATA(cmsg); + break; + + case IPV6_RECVFRAGSIZE: + p->fragsize = *(int *) CMSG_DATA(cmsg); + break; + } + } else if (cmsg->cmsg_level == IPPROTO_IP) { + assert(p->family == AF_INET); + + switch (cmsg->cmsg_type) { + + case IP_PKTINFO: { + struct in_pktinfo *i = (struct in_pktinfo*) CMSG_DATA(cmsg); + + if (p->ifindex <= 0) + p->ifindex = i->ipi_ifindex; + + p->destination.in = i->ipi_addr; + break; + } + + case IP_TTL: + p->ttl = *(int *) CMSG_DATA(cmsg); + break; + + case IP_RECVFRAGSIZE: + p->fragsize = *(int *) CMSG_DATA(cmsg); + break; + } + } + } + + /* The Linux kernel sets the interface index to the loopback + * device if the packet came from the local host since it + * avoids the routing table in such a case. Let's unset the + * interface index in such a case. */ + if (p->ifindex == LOOPBACK_IFINDEX) + p->ifindex = 0; + + if (protocol != DNS_PROTOCOL_DNS) { + /* If we don't know the interface index still, we look for the + * first local interface with a matching address. Yuck! */ + if (p->ifindex <= 0) + p->ifindex = manager_find_ifindex(m, p->family, &p->destination); + } + + log_debug("Received %s UDP packet of size %zu, ifindex=%i, ttl=%u, fragsize=%zu, sender=%s, destination=%s", + dns_protocol_to_string(protocol), p->size, p->ifindex, p->ttl, p->fragsize, + IN_ADDR_TO_STRING(p->family, &p->sender), + IN_ADDR_TO_STRING(p->family, &p->destination)); + + *ret = TAKE_PTR(p); + return 1; +} + +static int sendmsg_loop(int fd, struct msghdr *mh, int flags) { + int r; + + assert(fd >= 0); + assert(mh); + + for (;;) { + if (sendmsg(fd, mh, flags) >= 0) + return 0; + if (errno == EINTR) + continue; + if (errno != EAGAIN) + return -errno; + + r = fd_wait_for_event(fd, POLLOUT, SEND_TIMEOUT_USEC); + if (r < 0) + return r; + if (r == 0) + return -ETIMEDOUT; + } +} + +static int write_loop(int fd, void *message, size_t length) { + int r; + + assert(fd >= 0); + assert(message); + + for (;;) { + if (write(fd, message, length) >= 0) + return 0; + if (errno == EINTR) + continue; + if (errno != EAGAIN) + return -errno; + + r = fd_wait_for_event(fd, POLLOUT, SEND_TIMEOUT_USEC); + if (r < 0) + return r; + if (r == 0) + return -ETIMEDOUT; + } +} + +int manager_write(Manager *m, int fd, DnsPacket *p) { + int r; + + log_debug("Sending %s%s packet with id %" PRIu16 " of size %zu.", + DNS_PACKET_TC(p) ? "truncated (!) " : "", + DNS_PACKET_QR(p) ? "response" : "query", + DNS_PACKET_ID(p), + p->size); + + r = write_loop(fd, DNS_PACKET_DATA(p), p->size); + if (r < 0) + return r; + + return 0; +} + +static int manager_ipv4_send( + Manager *m, + int fd, + int ifindex, + const struct in_addr *destination, + uint16_t port, + const struct in_addr *source, + DnsPacket *p) { + + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct in_pktinfo))) control = {}; + union sockaddr_union sa; + struct iovec iov; + struct msghdr mh = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_name = &sa.sa, + .msg_namelen = sizeof(sa.in), + }; + + assert(m); + assert(fd >= 0); + assert(destination); + assert(port > 0); + assert(p); + + iov = IOVEC_MAKE(DNS_PACKET_DATA(p), p->size); + + sa = (union sockaddr_union) { + .in.sin_family = AF_INET, + .in.sin_addr = *destination, + .in.sin_port = htobe16(port), + }; + + if (ifindex > 0) { + struct cmsghdr *cmsg; + struct in_pktinfo *pi; + + mh.msg_control = &control; + mh.msg_controllen = sizeof(control); + + cmsg = CMSG_FIRSTHDR(&mh); + cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); + cmsg->cmsg_level = IPPROTO_IP; + cmsg->cmsg_type = IP_PKTINFO; + + pi = (struct in_pktinfo*) CMSG_DATA(cmsg); + pi->ipi_ifindex = ifindex; + + if (source) + pi->ipi_spec_dst = *source; + } + + return sendmsg_loop(fd, &mh, 0); +} + +static int manager_ipv6_send( + Manager *m, + int fd, + int ifindex, + const struct in6_addr *destination, + uint16_t port, + const struct in6_addr *source, + DnsPacket *p) { + + CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct in6_pktinfo))) control = {}; + union sockaddr_union sa; + struct iovec iov; + struct msghdr mh = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_name = &sa.sa, + .msg_namelen = sizeof(sa.in6), + }; + + assert(m); + assert(fd >= 0); + assert(destination); + assert(port > 0); + assert(p); + + iov = IOVEC_MAKE(DNS_PACKET_DATA(p), p->size); + + sa = (union sockaddr_union) { + .in6.sin6_family = AF_INET6, + .in6.sin6_addr = *destination, + .in6.sin6_port = htobe16(port), + .in6.sin6_scope_id = ifindex, + }; + + if (ifindex > 0) { + struct cmsghdr *cmsg; + struct in6_pktinfo *pi; + + mh.msg_control = &control; + mh.msg_controllen = sizeof(control); + + cmsg = CMSG_FIRSTHDR(&mh); + cmsg->cmsg_len = CMSG_LEN(sizeof(struct in6_pktinfo)); + cmsg->cmsg_level = IPPROTO_IPV6; + cmsg->cmsg_type = IPV6_PKTINFO; + + pi = (struct in6_pktinfo*) CMSG_DATA(cmsg); + pi->ipi6_ifindex = ifindex; + + if (source) + pi->ipi6_addr = *source; + } + + return sendmsg_loop(fd, &mh, 0); +} + +static int dns_question_to_json(DnsQuestion *q, JsonVariant **ret) { + _cleanup_(json_variant_unrefp) JsonVariant *l = NULL; + DnsResourceKey *key; + int r; + + assert(ret); + + DNS_QUESTION_FOREACH(key, q) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL; + + r = dns_resource_key_to_json(key, &v); + if (r < 0) + return r; + + r = json_variant_append_array(&l, v); + if (r < 0) + return r; + } + + *ret = TAKE_PTR(l); + return 0; +} + +int manager_monitor_send( + Manager *m, + int state, + int rcode, + int error, + DnsQuestion *question_idna, + DnsQuestion *question_utf8, + DnsPacket *question_bypass, + DnsQuestion *collected_questions, + DnsAnswer *answer) { + + _cleanup_(json_variant_unrefp) JsonVariant *jquestion = NULL, *jcollected_questions = NULL, *janswer = NULL; + _cleanup_(dns_question_unrefp) DnsQuestion *merged = NULL; + Varlink *connection; + DnsAnswerItem *rri; + int r; + + assert(m); + + if (set_isempty(m->varlink_subscription)) + return 0; + + /* Merge all questions into one */ + r = dns_question_merge(question_idna, question_utf8, &merged); + if (r < 0) + return log_error_errno(r, "Failed to merge UTF8/IDNA questions: %m"); + + if (question_bypass) { + _cleanup_(dns_question_unrefp) DnsQuestion *merged2 = NULL; + + r = dns_question_merge(merged, question_bypass->question, &merged2); + if (r < 0) + return log_error_errno(r, "Failed to merge UTF8/IDNA questions and DNS packet question: %m"); + + dns_question_unref(merged); + merged = TAKE_PTR(merged2); + } + + /* Convert the current primary question to JSON */ + r = dns_question_to_json(merged, &jquestion); + if (r < 0) + return log_error_errno(r, "Failed to convert question to JSON: %m"); + + /* Generate a JSON array of the questions preceding the current one in the CNAME chain */ + r = dns_question_to_json(collected_questions, &jcollected_questions); + if (r < 0) + return log_error_errno(r, "Failed to convert question to JSON: %m"); + + DNS_ANSWER_FOREACH_ITEM(rri, answer) { + _cleanup_(json_variant_unrefp) JsonVariant *v = NULL, *w = NULL; + + r = dns_resource_record_to_json(rri->rr, &v); + if (r < 0) + return log_error_errno(r, "Failed to convert answer resource record to JSON: %m"); + + r = dns_resource_record_to_wire_format(rri->rr, /* canonical= */ false); /* don't use DNSSEC canonical format, since it removes casing, but we want that for DNS_SD compat */ + if (r < 0) + return log_error_errno(r, "Failed to generate RR wire format: %m"); + + r = json_build(&w, JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(v, "rr", JSON_BUILD_VARIANT(v)), + JSON_BUILD_PAIR("raw", JSON_BUILD_BASE64(rri->rr->wire_format, rri->rr->wire_format_size)), + JSON_BUILD_PAIR_CONDITION(rri->ifindex > 0, "ifindex", JSON_BUILD_INTEGER(rri->ifindex)))); + if (r < 0) + return log_error_errno(r, "Failed to make answer RR object: %m"); + + r = json_variant_append_array(&janswer, w); + if (r < 0) + return log_debug_errno(r, "Failed to append notification entry to array: %m"); + } + + SET_FOREACH(connection, m->varlink_subscription) { + r = varlink_notifyb(connection, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("state", JSON_BUILD_STRING(dns_transaction_state_to_string(state))), + JSON_BUILD_PAIR_CONDITION(state == DNS_TRANSACTION_RCODE_FAILURE, "rcode", JSON_BUILD_INTEGER(rcode)), + JSON_BUILD_PAIR_CONDITION(state == DNS_TRANSACTION_ERRNO, "errno", JSON_BUILD_INTEGER(error)), + JSON_BUILD_PAIR("question", JSON_BUILD_VARIANT(jquestion)), + JSON_BUILD_PAIR_CONDITION(jcollected_questions, "collectedQuestions", JSON_BUILD_VARIANT(jcollected_questions)), + JSON_BUILD_PAIR_CONDITION(janswer, "answer", JSON_BUILD_VARIANT(janswer)))); + if (r < 0) + log_debug_errno(r, "Failed to send monitor event, ignoring: %m"); + } + + return 0; +} + +int manager_send( + Manager *m, + int fd, + int ifindex, + int family, + const union in_addr_union *destination, + uint16_t port, + const union in_addr_union *source, + DnsPacket *p) { + + assert(m); + assert(fd >= 0); + assert(destination); + assert(port > 0); + assert(p); + + /* For mDNS, it is natural that the packet have truncated flag when we have many known answers. */ + bool truncated = DNS_PACKET_TC(p) && (p->protocol != DNS_PROTOCOL_MDNS || !p->more); + + log_debug("Sending %s%s packet with id %" PRIu16 " on interface %i/%s of size %zu.", + truncated ? "truncated (!) " : "", + DNS_PACKET_QR(p) ? "response" : "query", + DNS_PACKET_ID(p), + ifindex, af_to_name(family), + p->size); + + if (family == AF_INET) + return manager_ipv4_send(m, fd, ifindex, &destination->in, port, source ? &source->in : NULL, p); + if (family == AF_INET6) + return manager_ipv6_send(m, fd, ifindex, &destination->in6, port, source ? &source->in6 : NULL, p); + + return -EAFNOSUPPORT; +} + +uint32_t manager_find_mtu(Manager *m) { + uint32_t mtu = 0; + Link *l; + + /* If we don't know on which link a DNS packet would be delivered, let's find the largest MTU that + * works on all interfaces we know of that have an IP address associated */ + + HASHMAP_FOREACH(l, m->links) { + /* Let's filter out links without IP addresses (e.g. AF_CAN links and suchlike) */ + if (!l->addresses) + continue; + + /* Safety check: MTU shorter than what we need for the absolutely shortest DNS request? Then + * let's ignore this link. */ + if (l->mtu < MIN(UDP4_PACKET_HEADER_SIZE + DNS_PACKET_HEADER_SIZE, + UDP6_PACKET_HEADER_SIZE + DNS_PACKET_HEADER_SIZE)) + continue; + + if (mtu <= 0 || l->mtu < mtu) + mtu = l->mtu; + } + + if (mtu == 0) /* found nothing? then let's assume the typical Ethernet MTU for lack of anything more precise */ + return 1500; + + return mtu; +} + +int manager_find_ifindex(Manager *m, int family, const union in_addr_union *in_addr) { + LinkAddress *a; + + assert(m); + + if (!IN_SET(family, AF_INET, AF_INET6)) + return 0; + + if (!in_addr) + return 0; + + a = manager_find_link_address(m, family, in_addr); + if (a) + return a->link->ifindex; + + return 0; +} + +void manager_refresh_rrs(Manager *m) { + Link *l; + DnssdService *s; + + assert(m); + + m->llmnr_host_ipv4_key = dns_resource_key_unref(m->llmnr_host_ipv4_key); + m->llmnr_host_ipv6_key = dns_resource_key_unref(m->llmnr_host_ipv6_key); + m->mdns_host_ipv4_key = dns_resource_key_unref(m->mdns_host_ipv4_key); + m->mdns_host_ipv6_key = dns_resource_key_unref(m->mdns_host_ipv6_key); + + HASHMAP_FOREACH(l, m->links) + link_add_rrs(l, true); + + if (m->mdns_support == RESOLVE_SUPPORT_YES) + HASHMAP_FOREACH(s, m->dnssd_services) + if (dnssd_update_rrs(s) < 0) + log_warning("Failed to refresh DNS-SD service '%s'", s->name); + + HASHMAP_FOREACH(l, m->links) + link_add_rrs(l, false); +} + +static int manager_next_random_name(const char *old, char **ret_new) { + const char *p; + uint64_t u, a; + char *n; + + p = strchr(old, 0); + assert(p); + + while (p > old) { + if (!ascii_isdigit(p[-1])) + break; + + p--; + } + + if (*p == 0 || safe_atou64(p, &u) < 0 || u <= 0) + u = 1; + + /* Add a random number to the old value. This way we can avoid + * that two hosts pick the same hostname, win on IPv4 and lose + * on IPv6 (or vice versa), and pick the same hostname + * replacement hostname, ad infinitum. We still want the + * numbers to go up monotonically, hence we just add a random + * value 1..10 */ + + random_bytes(&a, sizeof(a)); + u += 1 + a % 10; + + if (asprintf(&n, "%.*s%" PRIu64, (int) (p - old), old, u) < 0) + return -ENOMEM; + + *ret_new = n; + + return 0; +} + +int manager_next_hostname(Manager *m) { + _cleanup_free_ char *h = NULL, *k = NULL; + int r; + + assert(m); + + r = manager_next_random_name(m->llmnr_hostname, &h); + if (r < 0) + return r; + + r = dns_name_concat(h, "local", 0, &k); + if (r < 0) + return r; + + log_info("Hostname conflict, changing published hostname from '%s' to '%s'.", m->llmnr_hostname, h); + + free_and_replace(m->llmnr_hostname, h); + free_and_replace(m->mdns_hostname, k); + + manager_refresh_rrs(m); + (void) manager_send_changed(m, "LLMNRHostname"); + + return 0; +} + +LinkAddress* manager_find_link_address(Manager *m, int family, const union in_addr_union *in_addr) { + Link *l; + + assert(m); + + if (!IN_SET(family, AF_INET, AF_INET6)) + return NULL; + + if (!in_addr) + return NULL; + + HASHMAP_FOREACH(l, m->links) { + LinkAddress *a; + + a = link_find_address(l, family, in_addr); + if (a) + return a; + } + + return NULL; +} + +bool manager_packet_from_local_address(Manager *m, DnsPacket *p) { + assert(m); + assert(p); + + /* Let's see if this packet comes from an IP address we have on any local interface */ + + return !!manager_find_link_address(m, p->family, &p->sender); +} + +bool manager_packet_from_our_transaction(Manager *m, DnsPacket *p) { + DnsTransaction *t; + + assert(m); + assert(p); + + /* Let's see if we have a transaction with a query message with the exact same binary contents as the + * one we just got. If so, it's almost definitely a packet loop of some kind. */ + + t = hashmap_get(m->dns_transactions, UINT_TO_PTR(DNS_PACKET_ID(p))); + if (!t) + return false; + + return t->sent && dns_packet_equal(t->sent, p); +} + +DnsScope* manager_find_scope(Manager *m, DnsPacket *p) { + Link *l; + + assert(m); + assert(p); + + l = hashmap_get(m->links, INT_TO_PTR(p->ifindex)); + if (!l) + return NULL; + + switch (p->protocol) { + case DNS_PROTOCOL_LLMNR: + if (p->family == AF_INET) + return l->llmnr_ipv4_scope; + else if (p->family == AF_INET6) + return l->llmnr_ipv6_scope; + + break; + + case DNS_PROTOCOL_MDNS: + if (p->family == AF_INET) + return l->mdns_ipv4_scope; + else if (p->family == AF_INET6) + return l->mdns_ipv6_scope; + + break; + + default: + break; + } + + return NULL; +} + +void manager_verify_all(Manager *m) { + assert(m); + + LIST_FOREACH(scopes, s, m->dns_scopes) + dns_zone_verify_all(&s->zone); +} + +int manager_is_own_hostname(Manager *m, const char *name) { + int r; + + assert(m); + assert(name); + + if (m->llmnr_hostname) { + r = dns_name_equal(name, m->llmnr_hostname); + if (r != 0) + return r; + } + + if (m->mdns_hostname) { + r = dns_name_equal(name, m->mdns_hostname); + if (r != 0) + return r; + } + + if (m->full_hostname) + return dns_name_equal(name, m->full_hostname); + + return 0; +} + +int manager_compile_dns_servers(Manager *m, OrderedSet **dns) { + Link *l; + int r; + + assert(m); + assert(dns); + + r = ordered_set_ensure_allocated(dns, &dns_server_hash_ops); + if (r < 0) + return r; + + /* First add the system-wide servers and domains */ + LIST_FOREACH(servers, s, m->dns_servers) { + r = ordered_set_put(*dns, s); + if (r == -EEXIST) + continue; + if (r < 0) + return r; + } + + /* Then, add the per-link servers */ + HASHMAP_FOREACH(l, m->links) { + LIST_FOREACH(servers, s, l->dns_servers) { + r = ordered_set_put(*dns, s); + if (r == -EEXIST) + continue; + if (r < 0) + return r; + } + } + + /* If we found nothing, add the fallback servers */ + if (ordered_set_isempty(*dns)) { + LIST_FOREACH(servers, s, m->fallback_dns_servers) { + r = ordered_set_put(*dns, s); + if (r == -EEXIST) + continue; + if (r < 0) + return r; + } + } + + return 0; +} + +/* filter_route is a tri-state: + * < 0: no filtering + * = 0 or false: return only domains which should be used for searching + * > 0 or true: return only domains which are for routing only + */ +int manager_compile_search_domains(Manager *m, OrderedSet **domains, int filter_route) { + Link *l; + int r; + + assert(m); + assert(domains); + + r = ordered_set_ensure_allocated(domains, &dns_name_hash_ops); + if (r < 0) + return r; + + LIST_FOREACH(domains, d, m->search_domains) { + + if (filter_route >= 0 && + d->route_only != !!filter_route) + continue; + + r = ordered_set_put(*domains, d->name); + if (r == -EEXIST) + continue; + if (r < 0) + return r; + } + + HASHMAP_FOREACH(l, m->links) { + + LIST_FOREACH(domains, d, l->search_domains) { + + if (filter_route >= 0 && + d->route_only != !!filter_route) + continue; + + r = ordered_set_put(*domains, d->name); + if (r == -EEXIST) + continue; + if (r < 0) + return r; + } + } + + return 0; +} + +DnssecMode manager_get_dnssec_mode(Manager *m) { + assert(m); + + if (m->dnssec_mode != _DNSSEC_MODE_INVALID) + return m->dnssec_mode; + + return DNSSEC_NO; +} + +bool manager_dnssec_supported(Manager *m) { + DnsServer *server; + Link *l; + + assert(m); + + if (manager_get_dnssec_mode(m) == DNSSEC_NO) + return false; + + server = manager_get_dns_server(m); + if (server && !dns_server_dnssec_supported(server)) + return false; + + HASHMAP_FOREACH(l, m->links) + if (!link_dnssec_supported(l)) + return false; + + return true; +} + +DnsOverTlsMode manager_get_dns_over_tls_mode(Manager *m) { + assert(m); + + if (m->dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID) + return m->dns_over_tls_mode; + + return DNS_OVER_TLS_NO; +} + +void manager_dnssec_verdict(Manager *m, DnssecVerdict verdict, const DnsResourceKey *key) { + + assert(verdict >= 0); + assert(verdict < _DNSSEC_VERDICT_MAX); + + if (DEBUG_LOGGING) { + char s[DNS_RESOURCE_KEY_STRING_MAX]; + + log_debug("Found verdict for lookup %s: %s", + dns_resource_key_to_string(key, s, sizeof s), + dnssec_verdict_to_string(verdict)); + } + + m->n_dnssec_verdict[verdict]++; +} + +bool manager_routable(Manager *m) { + Link *l; + + assert(m); + + /* Returns true if the host has at least one interface with a routable address (regardless if IPv4 or IPv6) */ + + HASHMAP_FOREACH(l, m->links) + if (link_relevant(l, AF_UNSPEC, false)) + return true; + + return false; +} + +void manager_flush_caches(Manager *m, int log_level) { + assert(m); + + LIST_FOREACH(scopes, scope, m->dns_scopes) + dns_cache_flush(&scope->cache); + + log_full(log_level, "Flushed all caches."); +} + +void manager_reset_server_features(Manager *m) { + Link *l; + + dns_server_reset_features_all(m->dns_servers); + dns_server_reset_features_all(m->fallback_dns_servers); + + HASHMAP_FOREACH(l, m->links) + dns_server_reset_features_all(l->dns_servers); + + log_info("Resetting learnt feature levels on all servers."); +} + +void manager_cleanup_saved_user(Manager *m) { + _cleanup_closedir_ DIR *d = NULL; + + assert(m); + + /* Clean up all saved per-link files in /run/systemd/resolve/netif/ that don't have a matching interface + * anymore. These files are created to persist settings pushed in by the user via the bus, so that resolved can + * be restarted without losing this data. */ + + d = opendir("/run/systemd/resolve/netif/"); + if (!d) { + if (errno == ENOENT) + return; + + log_warning_errno(errno, "Failed to open interface directory: %m"); + return; + } + + FOREACH_DIRENT_ALL(de, d, log_error_errno(errno, "Failed to read interface directory: %m")) { + _cleanup_free_ char *p = NULL; + int ifindex; + Link *l; + + if (!IN_SET(de->d_type, DT_UNKNOWN, DT_REG)) + continue; + + if (dot_or_dot_dot(de->d_name)) + continue; + + ifindex = parse_ifindex(de->d_name); + if (ifindex < 0) /* Probably some temporary file from a previous run. Delete it */ + goto rm; + + l = hashmap_get(m->links, INT_TO_PTR(ifindex)); + if (!l) /* link vanished */ + goto rm; + + if (l->is_managed) /* now managed by networkd, hence the bus settings are useless */ + goto rm; + + continue; + + rm: + p = path_join("/run/systemd/resolve/netif", de->d_name); + if (!p) { + log_oom(); + return; + } + + (void) unlink(p); + } +} + +bool manager_next_dnssd_names(Manager *m) { + DnssdService *s; + bool tried = false; + int r; + + assert(m); + + HASHMAP_FOREACH(s, m->dnssd_services) { + _cleanup_free_ char * new_name = NULL; + + if (!s->withdrawn) + continue; + + r = manager_next_random_name(s->name_template, &new_name); + if (r < 0) { + log_warning_errno(r, "Failed to get new name for service '%s': %m", s->name); + continue; + } + + free_and_replace(s->name_template, new_name); + + s->withdrawn = false; + + tried = true; + } + + if (tried) + manager_refresh_rrs(m); + + return tried; +} + +bool manager_server_is_stub(Manager *m, DnsServer *s) { + DnsStubListenerExtra *l; + + assert(m); + assert(s); + + /* Safety check: we generally already skip the main stub when parsing configuration. But let's be + * extra careful, and check here again */ + if (s->family == AF_INET && + s->address.in.s_addr == htobe32(INADDR_DNS_STUB) && + dns_server_port(s) == 53) + return true; + + /* Main reason to call this is to check server data against the extra listeners, and filter things + * out. */ + ORDERED_SET_FOREACH(l, m->dns_extra_stub_listeners) + if (s->family == l->family && + in_addr_equal(s->family, &s->address, &l->address) && + dns_server_port(s) == dns_stub_listener_extra_port(l)) + return true; + + return false; +} + +int socket_disable_pmtud(int fd, int af) { + int r; + + assert(fd >= 0); + + if (af == AF_UNSPEC) { + af = socket_get_family(fd); + if (af < 0) + return af; + } + + switch (af) { + + case AF_INET: { + /* Turn off path MTU discovery, let's rather fragment on the way than to open us up against + * PMTU forgery vulnerabilities. + * + * There appears to be no documentation about IP_PMTUDISC_OMIT, but it has the effect that + * the "Don't Fragment" bit in the IPv4 header is turned off, thus enforcing fragmentation if + * our datagram size exceeds the MTU of a router in the path, and turning off path MTU + * discovery. + * + * This helps mitigating the PMTUD vulnerability described here: + * + * https://blog.apnic.net/2019/07/12/its-time-to-consider-avoiding-ip-fragmentation-in-the-dns/ + * + * Similar logic is in place in most DNS servers. + * + * There are multiple conflicting goals: we want to allow the largest datagrams possible (for + * efficiency reasons), but not have fragmentation (for security reasons), nor use PMTUD (for + * security reasons, too). Our strategy to deal with this is: use large packets, turn off + * PMTUD, but watch fragmentation taking place, and then size our packets to the max of the + * fragments seen — and if we need larger packets always go to TCP. + */ + + r = setsockopt_int(fd, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_OMIT); + if (r < 0) + return r; + + return 0; + } + + case AF_INET6: { + /* On IPv6 fragmentation only is done by the sender — never by routers on the path. PMTUD is + * mandatory. If we want to turn off PMTUD, the only way is by sending with minimal MTU only, + * so that we apply maximum fragmentation locally already, and thus PMTUD doesn't happen + * because there's nothing that could be fragmented further anymore. */ + + r = setsockopt_int(fd, IPPROTO_IPV6, IPV6_MTU, IPV6_MIN_MTU); + if (r < 0) + return r; + + return 0; + } + + default: + return -EAFNOSUPPORT; + } +} diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h new file mode 100644 index 0000000..341bcbf --- /dev/null +++ b/src/resolve/resolved-manager.h @@ -0,0 +1,217 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include <sys/stat.h> + +#include "sd-event.h" +#include "sd-netlink.h" +#include "sd-network.h" + +#include "hashmap.h" +#include "list.h" +#include "ordered-set.h" +#include "resolve-util.h" +#include "varlink.h" + +typedef struct Manager Manager; + +#include "resolved-dns-query.h" +#include "resolved-dns-search-domain.h" +#include "resolved-dns-stream.h" +#include "resolved-dns-stub.h" +#include "resolved-dns-trust-anchor.h" +#include "resolved-link.h" +#include "resolved-socket-graveyard.h" + +#define MANAGER_SEARCH_DOMAINS_MAX 256 +#define MANAGER_DNS_SERVERS_MAX 256 + +typedef struct EtcHosts { + Hashmap *by_address; + Hashmap *by_name; + Set *no_address; +} EtcHosts; + +struct Manager { + sd_event *event; + + ResolveSupport llmnr_support; + ResolveSupport mdns_support; + DnssecMode dnssec_mode; + DnsOverTlsMode dns_over_tls_mode; + DnsCacheMode enable_cache; + bool cache_from_localhost; + DnsStubListenerMode dns_stub_listener_mode; + +#if ENABLE_DNS_OVER_TLS + DnsTlsManagerData dnstls_data; +#endif + + /* Network */ + Hashmap *links; + + sd_netlink *rtnl; + sd_event_source *rtnl_event_source; + + sd_network_monitor *network_monitor; + sd_event_source *network_event_source; + + /* DNS query management */ + Hashmap *dns_transactions; + LIST_HEAD(DnsQuery, dns_queries); + unsigned n_dns_queries; + Hashmap *stub_queries_by_packet; + + LIST_HEAD(DnsStream, dns_streams); + unsigned n_dns_streams[_DNS_STREAM_TYPE_MAX]; + + /* Unicast dns */ + LIST_HEAD(DnsServer, dns_servers); + LIST_HEAD(DnsServer, fallback_dns_servers); + unsigned n_dns_servers; /* counts both main and fallback */ + DnsServer *current_dns_server; + + LIST_HEAD(DnsSearchDomain, search_domains); + unsigned n_search_domains; + + bool need_builtin_fallbacks; + bool read_resolv_conf; + bool resolve_unicast_single_label; + + struct stat resolv_conf_stat; + + DnsTrustAnchor trust_anchor; + + LIST_HEAD(DnsScope, dns_scopes); + DnsScope *unicast_scope; + + /* LLMNR */ + int llmnr_ipv4_udp_fd; + int llmnr_ipv6_udp_fd; + int llmnr_ipv4_tcp_fd; + int llmnr_ipv6_tcp_fd; + + sd_event_source *llmnr_ipv4_udp_event_source; + sd_event_source *llmnr_ipv6_udp_event_source; + sd_event_source *llmnr_ipv4_tcp_event_source; + sd_event_source *llmnr_ipv6_tcp_event_source; + + /* mDNS */ + int mdns_ipv4_fd; + int mdns_ipv6_fd; + sd_event_source *mdns_ipv4_event_source; + sd_event_source *mdns_ipv6_event_source; + + /* DNS-SD */ + Hashmap *dnssd_services; + + /* dbus */ + sd_bus *bus; + + /* The hostname we publish on LLMNR and mDNS */ + char *full_hostname; + char *llmnr_hostname; + char *mdns_hostname; + DnsResourceKey *llmnr_host_ipv4_key; + DnsResourceKey *llmnr_host_ipv6_key; + DnsResourceKey *mdns_host_ipv4_key; + DnsResourceKey *mdns_host_ipv6_key; + + /* Watch the system hostname */ + int hostname_fd; + sd_event_source *hostname_event_source; + + sd_event_source *sigusr1_event_source; + sd_event_source *sigusr2_event_source; + sd_event_source *sigrtmin1_event_source; + + unsigned n_transactions_total; + unsigned n_dnssec_verdict[_DNSSEC_VERDICT_MAX]; + + /* Data from /etc/hosts */ + EtcHosts etc_hosts; + usec_t etc_hosts_last; + struct stat etc_hosts_stat; + bool read_etc_hosts; + + OrderedSet *dns_extra_stub_listeners; + + /* Local DNS stub on 127.0.0.53:53 */ + sd_event_source *dns_stub_udp_event_source; + sd_event_source *dns_stub_tcp_event_source; + + /* Local DNS proxy stub on 127.0.0.54:53 */ + sd_event_source *dns_proxy_stub_udp_event_source; + sd_event_source *dns_proxy_stub_tcp_event_source; + + Hashmap *polkit_registry; + + VarlinkServer *varlink_server; + VarlinkServer *varlink_monitor_server; + + Set *varlink_subscription; + + sd_event_source *clock_change_event_source; + + LIST_HEAD(SocketGraveyard, socket_graveyard); + SocketGraveyard *socket_graveyard_oldest; + size_t n_socket_graveyard; +}; + +/* Manager */ + +int manager_new(Manager **ret); +Manager* manager_free(Manager *m); + +int manager_start(Manager *m); + +uint32_t manager_find_mtu(Manager *m); + +int manager_monitor_send(Manager *m, int state, int rcode, int error, DnsQuestion *question_idna, DnsQuestion *question_utf8, DnsPacket *question_bypass, DnsQuestion *collected_questions, DnsAnswer *answer); + +int manager_write(Manager *m, int fd, DnsPacket *p); +int manager_send(Manager *m, int fd, int ifindex, int family, const union in_addr_union *destination, uint16_t port, const union in_addr_union *source, DnsPacket *p); +int manager_recv(Manager *m, int fd, DnsProtocol protocol, DnsPacket **ret); + +int manager_find_ifindex(Manager *m, int family, const union in_addr_union *in_addr); +LinkAddress* manager_find_link_address(Manager *m, int family, const union in_addr_union *in_addr); + +void manager_refresh_rrs(Manager *m); +int manager_next_hostname(Manager *m); + +bool manager_packet_from_local_address(Manager *m, DnsPacket *p); +bool manager_packet_from_our_transaction(Manager *m, DnsPacket *p); + +DnsScope* manager_find_scope(Manager *m, DnsPacket *p); + +void manager_verify_all(Manager *m); + +DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free); + +/* For some reason we need some extra cmsg space on some kernels/archs. One of those days we need to figure out why */ +#define EXTRA_CMSG_SPACE 1024 + +int manager_is_own_hostname(Manager *m, const char *name); + +int manager_compile_dns_servers(Manager *m, OrderedSet **servers); +int manager_compile_search_domains(Manager *m, OrderedSet **domains, int filter_route); + +DnssecMode manager_get_dnssec_mode(Manager *m); +bool manager_dnssec_supported(Manager *m); + +DnsOverTlsMode manager_get_dns_over_tls_mode(Manager *m); + +void manager_dnssec_verdict(Manager *m, DnssecVerdict verdict, const DnsResourceKey *key); + +bool manager_routable(Manager *m); + +void manager_flush_caches(Manager *m, int log_level); +void manager_reset_server_features(Manager *m); + +void manager_cleanup_saved_user(Manager *m); + +bool manager_next_dnssd_names(Manager *m); + +bool manager_server_is_stub(Manager *m, DnsServer *s); + +int socket_disable_pmtud(int fd, int af); diff --git a/src/resolve/resolved-mdns.c b/src/resolve/resolved-mdns.c new file mode 100644 index 0000000..f5f1560 --- /dev/null +++ b/src/resolve/resolved-mdns.c @@ -0,0 +1,613 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <resolv.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include "alloc-util.h" +#include "fd-util.h" +#include "resolved-manager.h" +#include "resolved-mdns.h" +#include "sort-util.h" + +#define CLEAR_CACHE_FLUSH(x) (~MDNS_RR_CACHE_FLUSH_OR_QU & (x)) + +void manager_mdns_stop(Manager *m) { + assert(m); + + m->mdns_ipv4_event_source = sd_event_source_disable_unref(m->mdns_ipv4_event_source); + m->mdns_ipv4_fd = safe_close(m->mdns_ipv4_fd); + + m->mdns_ipv6_event_source = sd_event_source_disable_unref(m->mdns_ipv6_event_source); + m->mdns_ipv6_fd = safe_close(m->mdns_ipv6_fd); +} + +int manager_mdns_start(Manager *m) { + int r; + + assert(m); + + if (m->mdns_support == RESOLVE_SUPPORT_NO) + return 0; + + r = manager_mdns_ipv4_fd(m); + if (r == -EADDRINUSE) + goto eaddrinuse; + if (r < 0) + return r; + + if (socket_ipv6_is_enabled()) { + r = manager_mdns_ipv6_fd(m); + if (r == -EADDRINUSE) + goto eaddrinuse; + if (r < 0) + return r; + } + + return 0; + +eaddrinuse: + log_warning("Another mDNS responder prohibits binding the socket to the same port. Turning off mDNS support."); + m->mdns_support = RESOLVE_SUPPORT_NO; + manager_mdns_stop(m); + + return 0; +} + +static int mdns_rr_compare(DnsResourceRecord * const *a, DnsResourceRecord * const *b) { + DnsResourceRecord *x = *(DnsResourceRecord **) a, *y = *(DnsResourceRecord **) b; + size_t m; + int r; + + assert(x); + assert(y); + + r = CMP(CLEAR_CACHE_FLUSH(x->key->class), CLEAR_CACHE_FLUSH(y->key->class)); + if (r != 0) + return r; + + r = CMP(x->key->type, y->key->type); + if (r != 0) + return r; + + r = dns_resource_record_to_wire_format(x, false); + if (r < 0) { + log_warning_errno(r, "Can't wire-format RR: %m"); + return 0; + } + + r = dns_resource_record_to_wire_format(y, false); + if (r < 0) { + log_warning_errno(r, "Can't wire-format RR: %m"); + return 0; + } + + m = MIN(DNS_RESOURCE_RECORD_RDATA_SIZE(x), DNS_RESOURCE_RECORD_RDATA_SIZE(y)); + + r = memcmp(DNS_RESOURCE_RECORD_RDATA(x), DNS_RESOURCE_RECORD_RDATA(y), m); + if (r != 0) + return r; + + return CMP(DNS_RESOURCE_RECORD_RDATA_SIZE(x), DNS_RESOURCE_RECORD_RDATA_SIZE(y)); +} + +static int proposed_rrs_cmp(DnsResourceRecord **x, unsigned x_size, DnsResourceRecord **y, unsigned y_size) { + unsigned m; + int r; + + m = MIN(x_size, y_size); + for (unsigned i = 0; i < m; i++) { + r = mdns_rr_compare(&x[i], &y[i]); + if (r != 0) + return r; + } + + return CMP(x_size, y_size); +} + +static int mdns_packet_extract_matching_rrs(DnsPacket *p, DnsResourceKey *key, DnsResourceRecord ***ret_rrs) { + _cleanup_free_ DnsResourceRecord **list = NULL; + size_t i, n = 0, size = 0; + DnsResourceRecord *rr; + int r; + + assert(p); + assert(key); + assert(ret_rrs); + assert_return(DNS_PACKET_NSCOUNT(p) > 0, -EINVAL); + + i = 0; + DNS_ANSWER_FOREACH(rr, p->answer) { + if (i >= DNS_PACKET_ANCOUNT(p) && i < DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)) { + r = dns_resource_key_match_rr(key, rr, NULL); + if (r < 0) + return r; + if (r > 0) + size++; + } + i++; + } + + if (size == 0) { + *ret_rrs = NULL; + return 0; + } + + list = new(DnsResourceRecord *, size); + if (!list) + return -ENOMEM; + + i = 0; + DNS_ANSWER_FOREACH(rr, p->answer) { + if (i >= DNS_PACKET_ANCOUNT(p) && i < DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)) { + r = dns_resource_key_match_rr(key, rr, NULL); + if (r < 0) + return r; + if (r > 0) + list[n++] = rr; + } + i++; + } + + assert(n == size); + typesafe_qsort(list, size, mdns_rr_compare); + + *ret_rrs = TAKE_PTR(list); + + return size; +} + +static int mdns_do_tiebreak(DnsResourceKey *key, DnsAnswer *answer, DnsPacket *p) { + _cleanup_free_ DnsResourceRecord **our = NULL, **remote = NULL; + DnsResourceRecord *rr; + size_t i = 0, size; + int r; + + size = dns_answer_size(answer); + our = new(DnsResourceRecord *, size); + if (!our) + return -ENOMEM; + + DNS_ANSWER_FOREACH(rr, answer) + our[i++] = rr; + + typesafe_qsort(our, size, mdns_rr_compare); + + r = mdns_packet_extract_matching_rrs(p, key, &remote); + if (r < 0) + return r; + + if (proposed_rrs_cmp(remote, r, our, size) > 0) + return 1; + + return 0; +} + +static bool mdns_should_reply_using_unicast(DnsPacket *p) { + DnsQuestionItem *item; + + /* Work out if we should respond using multicast or unicast. */ + + /* The query was a legacy "one-shot mDNS query", RFC 6762, sections 5.1 and 6.7 */ + if (p->sender_port != MDNS_PORT) + return true; + + /* The query was a "direct unicast query", RFC 6762, section 5.5 */ + switch (p->family) { + case AF_INET: + if (!in4_addr_equal(&p->destination.in, &MDNS_MULTICAST_IPV4_ADDRESS)) + return true; + break; + case AF_INET6: + if (!in6_addr_equal(&p->destination.in6, &MDNS_MULTICAST_IPV6_ADDRESS)) + return true; + break; + } + + /* All the questions in the query had a QU bit set, RFC 6762, section 5.4 */ + DNS_QUESTION_FOREACH_ITEM(item, p->question) + if (!FLAGS_SET(item->flags, DNS_QUESTION_WANTS_UNICAST_REPLY)) + return false; + + return true; +} + +static bool sender_on_local_subnet(DnsScope *s, DnsPacket *p) { + int r; + + /* Check whether the sender is on a local subnet. */ + + if (!s->link) + return false; + + LIST_FOREACH(addresses, a, s->link->addresses) { + if (a->family != p->family) + continue; + if (a->prefixlen == UCHAR_MAX) /* don't know subnet mask */ + continue; + + r = in_addr_prefix_covers(a->family, &a->in_addr, a->prefixlen, &p->sender); + if (r < 0) + log_debug_errno(r, "Failed to determine whether link address covers sender address: %m"); + if (r > 0) + return true; + } + + return false; +} + + +static int mdns_scope_process_query(DnsScope *s, DnsPacket *p) { + _cleanup_(dns_answer_unrefp) DnsAnswer *full_answer = NULL; + _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL; + DnsResourceKey *key = NULL; + DnsResourceRecord *rr; + bool tentative = false; + bool legacy_query = p->sender_port != MDNS_PORT; + bool unicast_reply; + int r; + + assert(s); + assert(p); + + r = dns_packet_extract(p); + if (r < 0) + return log_debug_errno(r, "Failed to extract resource records from incoming packet: %m"); + + /* TODO: Support Known-Answers only packets gracefully. */ + if (dns_question_size(p->question) <= 0) + return 0; + + unicast_reply = mdns_should_reply_using_unicast(p); + if (unicast_reply && !sender_on_local_subnet(s, p)) { + /* RFC 6762, section 5.5 recommends silently ignoring unicast queries + * from senders outside the local network, so that we don't reveal our + * internal network structure to outsiders. */ + log_debug("Sender wants a unicast reply, but is not on a local subnet. Ignoring."); + return 0; + } + + DNS_QUESTION_FOREACH(key, p->question) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; + DnsAnswerItem *item; + + r = dns_zone_lookup(&s->zone, key, 0, &answer, &soa, &tentative); + if (r < 0) + return log_debug_errno(r, "Failed to look up key: %m"); + + if (tentative && DNS_PACKET_NSCOUNT(p) > 0) { + /* + * A race condition detected with the probe packet from + * a remote host. + * Do simultaneous probe tiebreaking as described in + * RFC 6762, Section 8.2. In case we lost don't reply + * the question and withdraw conflicting RRs. + */ + r = mdns_do_tiebreak(key, answer, p); + if (r < 0) + return log_debug_errno(r, "Failed to do tiebreaking"); + + if (r > 0) { /* we lost */ + DNS_ANSWER_FOREACH(rr, answer) { + DnsZoneItem *i; + + i = dns_zone_get(&s->zone, rr); + if (i) + dns_zone_item_conflict(i); + } + + continue; + } + } + + if (dns_answer_isempty(answer)) + continue; + + /* Copy answer items from full_answer to answer, tweaking them if needed. */ + if (full_answer) { + r = dns_answer_reserve(&full_answer, dns_answer_size(answer)); + if (r < 0) + return log_debug_errno(r, "Failed to reserve space in answer"); + } else { + full_answer = dns_answer_new(dns_answer_size(answer)); + if (!full_answer) + return log_oom(); + } + + DNS_ANSWER_FOREACH_ITEM(item, answer) { + DnsAnswerFlags flags = item->flags; + /* The cache-flush bit must not be set in legacy unicast responses. + * See section 6.7 of RFC 6762. */ + if (legacy_query) + flags &= ~DNS_ANSWER_CACHE_FLUSH; + r = dns_answer_add(full_answer, item->rr, item->ifindex, flags, item->rrsig); + if (r < 0) + return log_debug_errno(r, "Failed to extend answer: %m"); + } + } + + if (dns_answer_isempty(full_answer)) + return 0; + + r = dns_scope_make_reply_packet(s, DNS_PACKET_ID(p), DNS_RCODE_SUCCESS, + legacy_query ? p->question : NULL, full_answer, + NULL, false, &reply); + if (r < 0) + return log_debug_errno(r, "Failed to build reply packet: %m"); + + if (!ratelimit_below(&s->ratelimit)) + return 0; + + if (unicast_reply) { + reply->destination = p->sender; + reply->destination_port = p->sender_port; + } + r = dns_scope_emit_udp(s, -1, AF_UNSPEC, reply); + if (r < 0) + return log_debug_errno(r, "Failed to send reply packet: %m"); + + return 0; +} + +static int on_mdns_packet(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + Manager *m = userdata; + DnsScope *scope; + int r; + + r = manager_recv(m, fd, DNS_PROTOCOL_MDNS, &p); + if (r <= 0) + return r; + + if (manager_packet_from_local_address(m, p)) + return 0; + + scope = manager_find_scope(m, p); + if (!scope) { + log_debug("Got mDNS UDP packet on unknown scope. Ignoring."); + return 0; + } + + if (dns_packet_validate_reply(p) > 0) { + DnsResourceRecord *rr; + + log_debug("Got mDNS reply packet"); + + /* + * mDNS is different from regular DNS and LLMNR with regard to handling responses. + * While on other protocols, we can ignore every answer that doesn't match a question + * we broadcast earlier, RFC6762, section 18.1 recommends looking at and caching all + * incoming information, regardless of the DNS packet ID. + * + * Hence, extract the packet here, and try to find a transaction for answer the we got + * and complete it. Also store the new information in scope's cache. + */ + r = dns_packet_extract(p); + if (r < 0) { + log_debug("mDNS packet extraction failed."); + return 0; + } + + dns_scope_check_conflicts(scope, p); + + DNS_ANSWER_FOREACH(rr, p->answer) { + const char *name; + + name = dns_resource_key_name(rr->key); + + /* If the received reply packet contains ANY record that is not .local + * or .in-addr.arpa or .ip6.arpa, we assume someone's playing tricks on + * us and discard the packet completely. */ + if (!(dns_name_endswith(name, "in-addr.arpa") > 0 || + dns_name_endswith(name, "ip6.arpa") > 0 || + dns_name_endswith(name, "local") > 0)) + return 0; + + if (rr->ttl == 0) { + log_debug("Got a goodbye packet"); + /* See the section 10.1 of RFC6762 */ + rr->ttl = 1; + } + } + + for (bool match = true; match;) { + match = false; + LIST_FOREACH(transactions_by_scope, t, scope->transactions) { + if (t->state != DNS_TRANSACTION_PENDING) + continue; + + r = dns_answer_match_key(p->answer, dns_transaction_key(t), NULL); + if (r <= 0) { + if (r < 0) + log_debug_errno(r, "Failed to match resource key, ignoring: %m"); + continue; + } + + /* This packet matches the transaction, let's pass it on as reply */ + dns_transaction_process_reply(t, p, false); + + /* The dns_transaction_process_reply() -> dns_transaction_complete() -> + * dns_query_candidate_stop() may free multiple transactions. Hence, restart + * the loop. */ + match = true; + break; + } + } + + dns_cache_put( + &scope->cache, + scope->manager->enable_cache, + DNS_PROTOCOL_MDNS, + NULL, + DNS_PACKET_RCODE(p), + p->answer, + NULL, + false, + _DNSSEC_RESULT_INVALID, + UINT32_MAX, + p->family, + &p->sender); + + } else if (dns_packet_validate_query(p) > 0) { + log_debug("Got mDNS query packet for id %u", DNS_PACKET_ID(p)); + + r = mdns_scope_process_query(scope, p); + if (r < 0) { + log_debug_errno(r, "mDNS query processing failed: %m"); + return 0; + } + } else + log_debug("Invalid mDNS UDP packet."); + + return 0; +} + +int manager_mdns_ipv4_fd(Manager *m) { + union sockaddr_union sa = { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(MDNS_PORT), + }; + _cleanup_close_ int s = -1; + int r; + + assert(m); + + if (m->mdns_ipv4_fd >= 0) + return m->mdns_ipv4_fd; + + s = socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (s < 0) + return log_error_errno(errno, "mDNS-IPv4: Failed to create socket: %m"); + + r = setsockopt_int(s, IPPROTO_IP, IP_TTL, 255); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set IP_TTL: %m"); + + r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_TTL, 255); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set IP_MULTICAST_TTL: %m"); + + r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_LOOP, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set IP_MULTICAST_LOOP: %m"); + + r = setsockopt_int(s, IPPROTO_IP, IP_PKTINFO, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set IP_PKTINFO: %m"); + + r = setsockopt_int(s, IPPROTO_IP, IP_RECVTTL, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set IP_RECVTTL: %m"); + + /* Disable Don't-Fragment bit in the IP header */ + r = setsockopt_int(s, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DONT); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set IP_MTU_DISCOVER: %m"); + + /* See the section 15.1 of RFC6762 */ + /* first try to bind without SO_REUSEADDR to detect another mDNS responder */ + r = bind(s, &sa.sa, sizeof(sa.in)); + if (r < 0) { + if (errno != EADDRINUSE) + return log_error_errno(errno, "mDNS-IPv4: Failed to bind socket: %m"); + + log_warning("mDNS-IPv4: There appears to be another mDNS responder running, or previously systemd-resolved crashed with some outstanding transfers."); + + /* try again with SO_REUSEADDR */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set SO_REUSEADDR: %m"); + + r = bind(s, &sa.sa, sizeof(sa.in)); + if (r < 0) + return log_error_errno(errno, "mDNS-IPv4: Failed to bind socket: %m"); + } else { + /* enable SO_REUSEADDR for the case that the user really wants multiple mDNS responders */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to set SO_REUSEADDR: %m"); + } + + r = sd_event_add_io(m->event, &m->mdns_ipv4_event_source, s, EPOLLIN, on_mdns_packet, m); + if (r < 0) + return log_error_errno(r, "mDNS-IPv4: Failed to create event source: %m"); + + (void) sd_event_source_set_description(m->mdns_ipv4_event_source, "mdns-ipv4"); + + return m->mdns_ipv4_fd = TAKE_FD(s); +} + +int manager_mdns_ipv6_fd(Manager *m) { + union sockaddr_union sa = { + .in6.sin6_family = AF_INET6, + .in6.sin6_port = htobe16(MDNS_PORT), + }; + _cleanup_close_ int s = -1; + int r; + + assert(m); + + if (m->mdns_ipv6_fd >= 0) + return m->mdns_ipv6_fd; + + s = socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); + if (s < 0) + return log_error_errno(errno, "mDNS-IPv6: Failed to create socket: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, 255); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set IPV6_UNICAST_HOPS: %m"); + + /* RFC 6762, section 11 recommends setting the TTL of UDP packets to 255. */ + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, 255); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set IPV6_MULTICAST_HOPS: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set IPV6_MULTICAST_LOOP: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_V6ONLY, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set IPV6_V6ONLY: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set IPV6_RECVPKTINFO: %m"); + + r = setsockopt_int(s, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set IPV6_RECVHOPLIMIT: %m"); + + /* See the section 15.1 of RFC6762 */ + /* first try to bind without SO_REUSEADDR to detect another mDNS responder */ + r = bind(s, &sa.sa, sizeof(sa.in6)); + if (r < 0) { + if (errno != EADDRINUSE) + return log_error_errno(errno, "mDNS-IPv6: Failed to bind socket: %m"); + + log_warning("mDNS-IPv6: There appears to be another mDNS responder running, or previously systemd-resolved crashed with some outstanding transfers."); + + /* try again with SO_REUSEADDR */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set SO_REUSEADDR: %m"); + + r = bind(s, &sa.sa, sizeof(sa.in6)); + if (r < 0) + return log_error_errno(errno, "mDNS-IPv6: Failed to bind socket: %m"); + } else { + /* enable SO_REUSEADDR for the case that the user really wants multiple mDNS responders */ + r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to set SO_REUSEADDR: %m"); + } + + r = sd_event_add_io(m->event, &m->mdns_ipv6_event_source, s, EPOLLIN, on_mdns_packet, m); + if (r < 0) + return log_error_errno(r, "mDNS-IPv6: Failed to create event source: %m"); + + (void) sd_event_source_set_description(m->mdns_ipv6_event_source, "mdns-ipv6"); + + return m->mdns_ipv6_fd = TAKE_FD(s); +} diff --git a/src/resolve/resolved-mdns.h b/src/resolve/resolved-mdns.h new file mode 100644 index 0000000..38ef180 --- /dev/null +++ b/src/resolve/resolved-mdns.h @@ -0,0 +1,13 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "resolved-manager.h" + +#define MDNS_PORT 5353 +#define MDNS_ANNOUNCE_DELAY (1 * USEC_PER_SEC) + +int manager_mdns_ipv4_fd(Manager *m); +int manager_mdns_ipv6_fd(Manager *m); + +void manager_mdns_stop(Manager *m); +int manager_mdns_start(Manager *m); diff --git a/src/resolve/resolved-resolv-conf.c b/src/resolve/resolved-resolv-conf.c new file mode 100644 index 0000000..5749aa1 --- /dev/null +++ b/src/resolve/resolved-resolv-conf.c @@ -0,0 +1,430 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <resolv.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "alloc-util.h" +#include "dns-domain.h" +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "label.h" +#include "ordered-set.h" +#include "resolved-conf.h" +#include "resolved-dns-server.h" +#include "resolved-resolv-conf.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util-label.h" + +int manager_check_resolv_conf(const Manager *m) { + struct stat st, own; + + assert(m); + + /* This warns only when our stub listener is disabled and /etc/resolv.conf is a symlink to + * PRIVATE_STATIC_RESOLV_CONF. */ + + if (m->dns_stub_listener_mode != DNS_STUB_LISTENER_NO) + return 0; + + if (stat("/etc/resolv.conf", &st) < 0) { + if (errno == ENOENT) + return 0; + + return log_warning_errno(errno, "Failed to stat /etc/resolv.conf: %m"); + } + + /* Is it symlinked to our own uplink file? */ + if (stat(PRIVATE_STATIC_RESOLV_CONF, &own) >= 0 && + stat_inode_same(&st, &own)) + return log_warning_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "DNSStubListener= is disabled, but /etc/resolv.conf is a symlink to " + PRIVATE_STATIC_RESOLV_CONF " which expects DNSStubListener= to be enabled."); + + return 0; +} + +static bool file_is_our_own(const struct stat *st) { + assert(st); + + FOREACH_STRING(path, + PRIVATE_UPLINK_RESOLV_CONF, + PRIVATE_STUB_RESOLV_CONF, + PRIVATE_STATIC_RESOLV_CONF) { + + struct stat own; + + /* Is it symlinked to our own uplink file? */ + if (stat(path, &own) >= 0 && + stat_inode_same(st, &own)) + return true; + } + + return false; +} + +int manager_read_resolv_conf(Manager *m) { + _cleanup_fclose_ FILE *f = NULL; + struct stat st; + unsigned n = 0; + int r; + + assert(m); + + /* Reads the system /etc/resolv.conf, if it exists and is not + * symlinked to our own resolv.conf instance */ + + if (!m->read_resolv_conf) + return 0; + + r = stat("/etc/resolv.conf", &st); + if (r < 0) { + if (errno == ENOENT) + return 0; + + r = log_warning_errno(errno, "Failed to stat /etc/resolv.conf: %m"); + goto clear; + } + + /* Have we already seen the file? */ + if (stat_inode_unmodified(&st, &m->resolv_conf_stat)) + return 0; + + if (file_is_our_own(&st)) + return 0; + + f = fopen("/etc/resolv.conf", "re"); + if (!f) { + if (errno == ENOENT) + return 0; + + r = log_warning_errno(errno, "Failed to open /etc/resolv.conf: %m"); + goto clear; + } + + if (fstat(fileno(f), &st) < 0) { + r = log_error_errno(errno, "Failed to stat open file: %m"); + goto clear; + } + + if (file_is_our_own(&st)) + return 0; + + dns_server_mark_all(m->dns_servers); + dns_search_domain_mark_all(m->search_domains); + + for (;;) { + _cleanup_free_ char *line = NULL; + const char *a; + char *l; + + r = read_line(f, LONG_LINE_MAX, &line); + if (r < 0) { + log_error_errno(r, "Failed to read /etc/resolv.conf: %m"); + goto clear; + } + if (r == 0) + break; + + n++; + + l = strstrip(line); + if (IN_SET(*l, '#', ';', 0)) + continue; + + a = first_word(l, "nameserver"); + if (a) { + r = manager_parse_dns_server_string_and_warn(m, DNS_SERVER_SYSTEM, a); + if (r < 0) + log_warning_errno(r, "Failed to parse DNS server address '%s', ignoring.", a); + + continue; + } + + a = first_word(l, "domain"); + if (!a) /* We treat "domain" lines, and "search" lines as equivalent, and add both to our list. */ + a = first_word(l, "search"); + if (a) { + r = manager_parse_search_domains_and_warn(m, a); + if (r < 0) + log_warning_errno(r, "Failed to parse search domain string '%s', ignoring.", a); + + continue; + } + + log_syntax(NULL, LOG_DEBUG, "/etc/resolv.conf", n, 0, "Ignoring resolv.conf line: %s", l); + } + + m->resolv_conf_stat = st; + + /* Flush out all servers and search domains that are still + * marked. Those are then ones that didn't appear in the new + * /etc/resolv.conf */ + dns_server_unlink_marked(m->dns_servers); + dns_search_domain_unlink_marked(m->search_domains); + + /* Whenever /etc/resolv.conf changes, start using the first + * DNS server of it. This is useful to deal with broken + * network managing implementations (like NetworkManager), + * that when connecting to a VPN place both the VPN DNS + * servers and the local ones in /etc/resolv.conf. Without + * resetting the DNS server to use back to the first entry we + * will continue to use the local one thus being unable to + * resolve VPN domains. */ + manager_set_dns_server(m, m->dns_servers); + + /* Unconditionally flush the cache when /etc/resolv.conf is + * modified, even if the data it contained was completely + * identical to the previous version we used. We do this + * because altering /etc/resolv.conf is typically done when + * the network configuration changes, and that should be + * enough to flush the global unicast DNS cache. */ + if (m->unicast_scope) + dns_cache_flush(&m->unicast_scope->cache); + + /* If /etc/resolv.conf changed, make sure to forget everything we learned about the DNS servers. After all we + * might now talk to a very different DNS server that just happens to have the same IP address as an old one + * (think 192.168.1.1). */ + dns_server_reset_features_all(m->dns_servers); + + return 0; + +clear: + dns_server_unlink_all(m->dns_servers); + dns_search_domain_unlink_all(m->search_domains); + return r; +} + +static void write_resolv_conf_server(DnsServer *s, FILE *f, unsigned *count) { + DnsScope *scope; + + assert(s); + assert(f); + assert(count); + + if (!dns_server_string(s)) { + log_warning("Out of memory, or invalid DNS address. Ignoring server."); + return; + } + + /* resolv.conf simply doesn't support any other ports than 53, hence there's nothing much we can + * do — we have to suppress these entries */ + if (dns_server_port(s) != 53) { + log_debug("DNS server %s with non-standard UDP port number, suppressing from generated resolv.conf.", dns_server_string(s)); + return; + } + + /* Check if the scope this DNS server belongs to is suitable as 'default' route for lookups; resolv.conf does + * not have a syntax to express that, so it must not appear as a global name server to avoid routing unrelated + * domains to it (which is a privacy violation, will most probably fail anyway, and adds unnecessary load) */ + scope = dns_server_scope(s); + if (scope && !dns_scope_is_default_route(scope)) { + log_debug("Scope of DNS server %s has only route-only domains, not using as global name server", dns_server_string(s)); + return; + } + + if (*count == MAXNS) + fputs("# Too many DNS servers configured, the following entries may be ignored.\n", f); + (*count)++; + + fprintf(f, "nameserver %s\n", dns_server_string(s)); +} + +static void write_resolv_conf_search( + OrderedSet *domains, + FILE *f) { + char *domain; + + assert(domains); + assert(f); + + fputs("search", f); + + ORDERED_SET_FOREACH(domain, domains) { + fputc(' ', f); + fputs(domain, f); + } + + fputs("\n", f); +} + +static int write_uplink_resolv_conf_contents(FILE *f, OrderedSet *dns, OrderedSet *domains) { + + fputs("# This is "PRIVATE_UPLINK_RESOLV_CONF" managed by man:systemd-resolved(8).\n" + "# Do not edit.\n" + "#\n" + "# This file might be symlinked as /etc/resolv.conf. If you're looking at\n" + "# /etc/resolv.conf and seeing this text, you have followed the symlink.\n" + "#\n" + "# This is a dynamic resolv.conf file for connecting local clients directly to\n" + "# all known uplink DNS servers. This file lists all configured search domains.\n" + "#\n" + "# Third party programs should typically not access this file directly, but only\n" + "# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a\n" + "# different way, replace this symlink by a static file or a different symlink.\n" + "#\n" + "# See man:systemd-resolved.service(8) for details about the supported modes of\n" + "# operation for /etc/resolv.conf.\n" + "\n", f); + + if (ordered_set_isempty(dns)) + fputs("# No DNS servers known.\n", f); + else { + unsigned count = 0; + DnsServer *s; + + ORDERED_SET_FOREACH(s, dns) + write_resolv_conf_server(s, f, &count); + } + + if (ordered_set_isempty(domains)) + fputs("search .\n", f); /* Make sure that if the local hostname is chosen as fqdn this does not + * imply a search domain */ + else + write_resolv_conf_search(domains, f); + + return fflush_and_check(f); +} + +static int write_stub_resolv_conf_contents(FILE *f, OrderedSet *dns, OrderedSet *domains) { + fputs("# This is "PRIVATE_STUB_RESOLV_CONF" managed by man:systemd-resolved(8).\n" + "# Do not edit.\n" + "#\n" + "# This file might be symlinked as /etc/resolv.conf. If you're looking at\n" + "# /etc/resolv.conf and seeing this text, you have followed the symlink.\n" + "#\n" + "# This is a dynamic resolv.conf file for connecting local clients to the\n" + "# internal DNS stub resolver of systemd-resolved. This file lists all\n" + "# configured search domains.\n" + "#\n" + "# Run \"resolvectl status\" to see details about the uplink DNS servers\n" + "# currently in use.\n" + "#\n" + "# Third party programs should typically not access this file directly, but only\n" + "# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a\n" + "# different way, replace this symlink by a static file or a different symlink.\n" + "#\n" + "# See man:systemd-resolved.service(8) for details about the supported modes of\n" + "# operation for /etc/resolv.conf.\n" + "\n" + "nameserver 127.0.0.53\n" + "options edns0 trust-ad\n", f); + + if (ordered_set_isempty(domains)) + fputs("search .\n", f); /* Make sure that if the local hostname is chosen as fqdn this does not + * imply a search domain */ + else + write_resolv_conf_search(domains, f); + + return fflush_and_check(f); +} + +int manager_write_resolv_conf(Manager *m) { + _cleanup_ordered_set_free_ OrderedSet *dns = NULL, *domains = NULL; + _cleanup_(unlink_and_freep) char *temp_path_uplink = NULL, *temp_path_stub = NULL; + _cleanup_fclose_ FILE *f_uplink = NULL, *f_stub = NULL; + int r; + + assert(m); + + /* Read the system /etc/resolv.conf first */ + (void) manager_read_resolv_conf(m); + + /* Add the full list to a set, to filter out duplicates */ + r = manager_compile_dns_servers(m, &dns); + if (r < 0) + return log_warning_errno(r, "Failed to compile list of DNS servers, ignoring: %m"); + + r = manager_compile_search_domains(m, &domains, false); + if (r < 0) + return log_warning_errno(r, "Failed to compile list of search domains, ignoring: %m"); + + r = fopen_temporary_label(PRIVATE_UPLINK_RESOLV_CONF, PRIVATE_UPLINK_RESOLV_CONF, &f_uplink, &temp_path_uplink); + if (r < 0) + return log_warning_errno(r, "Failed to open new %s for writing, ignoring: %m", PRIVATE_UPLINK_RESOLV_CONF); + + (void) fchmod(fileno(f_uplink), 0644); + + r = write_uplink_resolv_conf_contents(f_uplink, dns, domains); + if (r < 0) + return log_warning_errno(r, "Failed to write new %s, ignoring: %m", PRIVATE_UPLINK_RESOLV_CONF); + + if (m->dns_stub_listener_mode != DNS_STUB_LISTENER_NO) { + r = fopen_temporary_label(PRIVATE_STUB_RESOLV_CONF, PRIVATE_STUB_RESOLV_CONF, &f_stub, &temp_path_stub); + if (r < 0) + return log_warning_errno(r, "Failed to open new %s for writing, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); + + (void) fchmod(fileno(f_stub), 0644); + + r = write_stub_resolv_conf_contents(f_stub, dns, domains); + if (r < 0) + return log_warning_errno(r, "Failed to write new %s, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); + + r = conservative_rename(temp_path_stub, PRIVATE_STUB_RESOLV_CONF); + if (r < 0) + log_warning_errno(r, "Failed to move new %s into place, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); + + temp_path_stub = mfree(temp_path_stub); /* free the string explicitly, so that we don't unlink anymore */ + } else { + r = symlink_atomic_label(basename(PRIVATE_UPLINK_RESOLV_CONF), PRIVATE_STUB_RESOLV_CONF); + if (r < 0) + log_warning_errno(r, "Failed to symlink %s, ignoring: %m", PRIVATE_STUB_RESOLV_CONF); + } + + r = conservative_rename(temp_path_uplink, PRIVATE_UPLINK_RESOLV_CONF); + if (r < 0) + log_warning_errno(r, "Failed to move new %s into place: %m", PRIVATE_UPLINK_RESOLV_CONF); + + temp_path_uplink = mfree(temp_path_uplink); /* free the string explicitly, so that we don't unlink anymore */ + return r; +} + +int resolv_conf_mode(void) { + static const char * const table[_RESOLV_CONF_MODE_MAX] = { + [RESOLV_CONF_UPLINK] = PRIVATE_UPLINK_RESOLV_CONF, + [RESOLV_CONF_STUB] = PRIVATE_STUB_RESOLV_CONF, + [RESOLV_CONF_STATIC] = PRIVATE_STATIC_RESOLV_CONF, + }; + + struct stat system_st; + + if (stat("/etc/resolv.conf", &system_st) < 0) { + if (errno == ENOENT) + return RESOLV_CONF_MISSING; + + return -errno; + } + + for (ResolvConfMode m = 0; m < _RESOLV_CONF_MODE_MAX; m++) { + struct stat our_st; + + if (!table[m]) + continue; + + if (stat(table[m], &our_st) < 0) { + if (errno != ENOENT) + log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", table[m]); + + continue; + } + + if (stat_inode_same(&system_st, &our_st)) + return m; + } + + return RESOLV_CONF_FOREIGN; +} + +static const char* const resolv_conf_mode_table[_RESOLV_CONF_MODE_MAX] = { + [RESOLV_CONF_UPLINK] = "uplink", + [RESOLV_CONF_STUB] = "stub", + [RESOLV_CONF_STATIC] = "static", + [RESOLV_CONF_MISSING] = "missing", + [RESOLV_CONF_FOREIGN] = "foreign", +}; +DEFINE_STRING_TABLE_LOOKUP(resolv_conf_mode, ResolvConfMode); diff --git a/src/resolve/resolved-resolv-conf.h b/src/resolve/resolved-resolv-conf.h new file mode 100644 index 0000000..8c0dee8 --- /dev/null +++ b/src/resolve/resolved-resolv-conf.h @@ -0,0 +1,23 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "resolved-manager.h" + +int manager_check_resolv_conf(const Manager *m); +int manager_read_resolv_conf(Manager *m); +int manager_write_resolv_conf(Manager *m); + +typedef enum ResolvConfMode { + RESOLV_CONF_UPLINK, + RESOLV_CONF_STUB, + RESOLV_CONF_STATIC, + RESOLV_CONF_FOREIGN, + RESOLV_CONF_MISSING, + _RESOLV_CONF_MODE_MAX, + _RESOLV_CONF_MODE_INVALID = -EINVAL, +} ResolvConfMode; + +int resolv_conf_mode(void); + +const char* resolv_conf_mode_to_string(ResolvConfMode m) _const_; +ResolvConfMode resolv_conf_mode_from_string(const char *s) _pure_; diff --git a/src/resolve/resolved-socket-graveyard.c b/src/resolve/resolved-socket-graveyard.c new file mode 100644 index 0000000..9605d72 --- /dev/null +++ b/src/resolve/resolved-socket-graveyard.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "resolved-socket-graveyard.h" + +#define SOCKET_GRAVEYARD_USEC (5 * USEC_PER_SEC) +#define SOCKET_GRAVEYARD_MAX 100 + +/* This implements a socket "graveyard" for UDP sockets. If a socket fd is added to the graveyard it is kept + * open for a couple of more seconds, expecting one reply. Once the reply is received the fd is closed + * immediately, or if none is received it is closed after the timeout. Why all this? So that if we contact a + * DNS server, and it doesn't reply instantly, and we lose interest in the response and thus close the fd, we + * don't end up sending back an ICMP error once the server responds but we aren't listening anymore. (See + * https://github.com/systemd/systemd/issues/17421 for further information.) + * + * Note that we don't allocate any timer event source to clear up the graveyard once the socket's timeout is + * reached. Instead we operate lazily: we close old entries when adding a new fd to the graveyard, or + * whenever any code runs manager_socket_graveyard_process() — which the DNS transaction code does right + * before allocating a new UDP socket. */ + +static SocketGraveyard* socket_graveyard_free(SocketGraveyard *g) { + if (!g) + return NULL; + + if (g->manager) { + assert(g->manager->n_socket_graveyard > 0); + g->manager->n_socket_graveyard--; + + if (g->manager->socket_graveyard_oldest == g) + g->manager->socket_graveyard_oldest = g->graveyard_prev; + + LIST_REMOVE(graveyard, g->manager->socket_graveyard, g); + + assert((g->manager->n_socket_graveyard > 0) == !!g->manager->socket_graveyard); + assert((g->manager->n_socket_graveyard > 0) == !!g->manager->socket_graveyard_oldest); + } + + if (g->io_event_source) { + log_debug("Closing graveyard socket fd %i", sd_event_source_get_io_fd(g->io_event_source)); + sd_event_source_disable_unref(g->io_event_source); + } + + return mfree(g); +} + +DEFINE_TRIVIAL_CLEANUP_FUNC(SocketGraveyard*, socket_graveyard_free); + +void manager_socket_graveyard_process(Manager *m) { + usec_t n = USEC_INFINITY; + + assert(m); + + while (m->socket_graveyard_oldest) { + SocketGraveyard *g = m->socket_graveyard_oldest; + + if (n == USEC_INFINITY) + assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &n) >= 0); + + if (g->deadline > n) + break; + + socket_graveyard_free(g); + } +} + +void manager_socket_graveyard_clear(Manager *m) { + assert(m); + + while (m->socket_graveyard) + socket_graveyard_free(m->socket_graveyard); +} + +static int on_io_event(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + SocketGraveyard *g = ASSERT_PTR(userdata); + + /* An IO event happened on the graveyard fd. We don't actually care which event that is, and we don't + * read any incoming packet off the socket. We just close the fd, that's enough to not trigger the + * ICMP unreachable port event */ + + socket_graveyard_free(g); + return 0; +} + +static void manager_socket_graveyard_make_room(Manager *m) { + assert(m); + + while (m->n_socket_graveyard >= SOCKET_GRAVEYARD_MAX) + socket_graveyard_free(m->socket_graveyard_oldest); +} + +int manager_add_socket_to_graveyard(Manager *m, int fd) { + _cleanup_(socket_graveyard_freep) SocketGraveyard *g = NULL; + int r; + + assert(m); + assert(fd >= 0); + + manager_socket_graveyard_process(m); + manager_socket_graveyard_make_room(m); + + g = new(SocketGraveyard, 1); + if (!g) + return log_oom(); + + *g = (SocketGraveyard) { + .manager = m, + }; + + LIST_PREPEND(graveyard, m->socket_graveyard, g); + if (!m->socket_graveyard_oldest) + m->socket_graveyard_oldest = g; + + m->n_socket_graveyard++; + + assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &g->deadline) >= 0); + g->deadline += SOCKET_GRAVEYARD_USEC; + + r = sd_event_add_io(m->event, &g->io_event_source, fd, EPOLLIN, on_io_event, g); + if (r < 0) + return log_error_errno(r, "Failed to create graveyard IO source: %m"); + + r = sd_event_source_set_io_fd_own(g->io_event_source, true); + if (r < 0) + return log_error_errno(r, "Failed to enable graveyard IO source fd ownership: %m"); + + (void) sd_event_source_set_description(g->io_event_source, "graveyard"); + + log_debug("Added socket %i to graveyard", fd); + + TAKE_PTR(g); + return 0; +} diff --git a/src/resolve/resolved-socket-graveyard.h b/src/resolve/resolved-socket-graveyard.h new file mode 100644 index 0000000..50c6aad --- /dev/null +++ b/src/resolve/resolved-socket-graveyard.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +typedef struct SocketGraveyard SocketGraveyard; + +#include "resolved-manager.h" + +struct SocketGraveyard { + Manager *manager; + usec_t deadline; + sd_event_source *io_event_source; + LIST_FIELDS(SocketGraveyard, graveyard); +}; + +void manager_socket_graveyard_process(Manager *m); +void manager_socket_graveyard_clear(Manager *m); + +int manager_add_socket_to_graveyard(Manager *m, int fd); diff --git a/src/resolve/resolved-util.c b/src/resolve/resolved-util.c new file mode 100644 index 0000000..00abada --- /dev/null +++ b/src/resolve/resolved-util.c @@ -0,0 +1,84 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "dns-def.h" +#include "dns-domain.h" +#include "hostname-util.h" +#include "idn-util.h" +#include "resolved-util.h" +#include "utf8.h" + +int resolve_system_hostname(char **full_hostname, char **first_label) { + _cleanup_free_ char *h = NULL, *n = NULL; +#if HAVE_LIBIDN2 + _cleanup_free_ char *utf8 = NULL; +#elif HAVE_LIBIDN + int k; +#endif + char label[DNS_LABEL_MAX]; + const char *p, *decoded; + int r; + + /* Return the full hostname in *full_hostname, if nonnull. + * + * Extract and normalize the first label of the locally configured hostname, check it's not + * "localhost", and return it in *first_label, if nonnull. */ + + r = gethostname_strict(&h); + if (r < 0) + return log_debug_errno(r, "Can't determine system hostname: %m"); + + p = h; + r = dns_label_unescape(&p, label, sizeof label, 0); + if (r < 0) + return log_debug_errno(r, "Failed to unescape hostname: %m"); + if (r == 0) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "Couldn't find a single label in hostname."); + +#if HAVE_LIBIDN || HAVE_LIBIDN2 + r = dlopen_idn(); + if (r < 0) { + log_debug_errno(r, "Failed to initialize IDN support, ignoring: %m"); + decoded = label; /* no decoding */ + } else +#endif + { +#if HAVE_LIBIDN2 + r = sym_idn2_to_unicode_8z8z(label, &utf8, 0); + if (r != IDN2_OK) + return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN), + "Failed to undo IDNA: %s", sym_idn2_strerror(r)); + assert(utf8_is_valid(utf8)); + + r = strlen(utf8); + decoded = utf8; +#elif HAVE_LIBIDN + k = dns_label_undo_idna(label, r, label, sizeof label); + if (k < 0) + return log_debug_errno(k, "Failed to undo IDNA: %m"); + if (k > 0) + r = k; + + if (!utf8_is_valid(label)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "System hostname is not UTF-8 clean."); + decoded = label; +#else + decoded = label; /* no decoding */ +#endif + } + + r = dns_label_escape_new(decoded, r, &n); + if (r < 0) + return log_debug_errno(r, "Failed to escape hostname: %m"); + + if (is_localhost(n)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "System hostname is 'localhost', ignoring."); + + if (full_hostname) + *full_hostname = TAKE_PTR(h); + if (first_label) + *first_label = TAKE_PTR(n); + return 0; +} diff --git a/src/resolve/resolved-util.h b/src/resolve/resolved-util.h new file mode 100644 index 0000000..446b7c9 --- /dev/null +++ b/src/resolve/resolved-util.h @@ -0,0 +1,4 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +int resolve_system_hostname(char **full_hostname, char **first_label); diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c new file mode 100644 index 0000000..f878d9e --- /dev/null +++ b/src/resolve/resolved-varlink.c @@ -0,0 +1,662 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "glyph-util.h" +#include "in-addr-util.h" +#include "resolved-dns-synthesize.h" +#include "resolved-varlink.h" +#include "socket-netlink.h" + +typedef struct LookupParameters { + int ifindex; + uint64_t flags; + int family; + union in_addr_union address; + size_t address_size; + char *name; +} LookupParameters; + +static void lookup_parameters_destroy(LookupParameters *p) { + assert(p); + free(p->name); +} + +static int reply_query_state(DnsQuery *q) { + + assert(q); + assert(q->varlink_request); + + switch (q->state) { + + case DNS_TRANSACTION_NO_SERVERS: + return varlink_error(q->varlink_request, "io.systemd.Resolve.NoNameServers", NULL); + + case DNS_TRANSACTION_TIMEOUT: + return varlink_error(q->varlink_request, "io.systemd.Resolve.QueryTimedOut", NULL); + + case DNS_TRANSACTION_ATTEMPTS_MAX_REACHED: + return varlink_error(q->varlink_request, "io.systemd.Resolve.MaxAttemptsReached", NULL); + + case DNS_TRANSACTION_INVALID_REPLY: + return varlink_error(q->varlink_request, "io.systemd.Resolve.InvalidReply", NULL); + + case DNS_TRANSACTION_ERRNO: + return varlink_error_errno(q->varlink_request, q->answer_errno); + + case DNS_TRANSACTION_ABORTED: + return varlink_error(q->varlink_request, "io.systemd.Resolve.QueryAborted", NULL); + + case DNS_TRANSACTION_DNSSEC_FAILED: + return varlink_errorb(q->varlink_request, "io.systemd.Resolve.DNSSECValidationFailed", + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("result", JSON_BUILD_STRING(dnssec_result_to_string(q->answer_dnssec_result))))); + + case DNS_TRANSACTION_NO_TRUST_ANCHOR: + return varlink_error(q->varlink_request, "io.systemd.Resolve.NoTrustAnchor", NULL); + + case DNS_TRANSACTION_RR_TYPE_UNSUPPORTED: + return varlink_error(q->varlink_request, "io.systemd.Resolve.ResourceRecordTypeUnsupported", NULL); + + case DNS_TRANSACTION_NETWORK_DOWN: + return varlink_error(q->varlink_request, "io.systemd.Resolve.NetworkDown", NULL); + + case DNS_TRANSACTION_NO_SOURCE: + return varlink_error(q->varlink_request, "io.systemd.Resolve.NoSource", NULL); + + case DNS_TRANSACTION_STUB_LOOP: + return varlink_error(q->varlink_request, "io.systemd.Resolve.StubLoop", NULL); + + case DNS_TRANSACTION_NOT_FOUND: + /* We return this as NXDOMAIN. This is only generated when a host doesn't implement LLMNR/TCP, and we + * thus quickly know that we cannot resolve an in-addr.arpa or ip6.arpa address. */ + return varlink_errorb(q->varlink_request, "io.systemd.Resolve.DNSError", + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("rcode", JSON_BUILD_INTEGER(DNS_RCODE_NXDOMAIN)))); + + case DNS_TRANSACTION_RCODE_FAILURE: + return varlink_errorb(q->varlink_request, "io.systemd.Resolve.DNSError", + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("rcode", JSON_BUILD_INTEGER(q->answer_rcode)))); + + case DNS_TRANSACTION_NULL: + case DNS_TRANSACTION_PENDING: + case DNS_TRANSACTION_VALIDATING: + case DNS_TRANSACTION_SUCCESS: + default: + assert_not_reached(); + } +} + +static void vl_on_disconnect(VarlinkServer *s, Varlink *link, void *userdata) { + DnsQuery *q; + + assert(s); + assert(link); + + q = varlink_get_userdata(link); + if (!q) + return; + + if (!DNS_TRANSACTION_IS_LIVE(q->state)) + return; + + log_debug("Client of active query vanished, aborting query."); + dns_query_complete(q, DNS_TRANSACTION_ABORTED); +} + +static void vl_on_notification_disconnect(VarlinkServer *s, Varlink *link, void *userdata) { + Manager *m = ASSERT_PTR(userdata); + + assert(s); + assert(link); + + Varlink *removed_link = set_remove(m->varlink_subscription, link); + if (removed_link) { + varlink_unref(removed_link); + log_debug("%u monitor clients remain active", set_size(m->varlink_subscription)); + } +} + +static bool validate_and_mangle_flags( + const char *name, + uint64_t *flags, + uint64_t ok) { + + assert(flags); + + /* This checks that the specified client-provided flags parameter actually makes sense, and mangles + * it slightly. Specifically: + * + * 1. We check that only the protocol flags and a bunch of NO_XYZ flags are on at most, plus the + * method-specific flags specified in 'ok'. + * + * 2. If no protocols are enabled we automatically convert that to "all protocols are enabled". + * + * The second rule means that clients can just pass 0 as flags for the common case, and all supported + * protocols are enabled. Moreover it's useful so that client's do not have to be aware of all + * protocols implemented in resolved, but can use 0 as protocols flags set as indicator for + * "everything". + */ + + if (*flags & ~(SD_RESOLVED_PROTOCOLS_ALL| + SD_RESOLVED_NO_CNAME| + SD_RESOLVED_NO_VALIDATE| + SD_RESOLVED_NO_SYNTHESIZE| + SD_RESOLVED_NO_CACHE| + SD_RESOLVED_NO_ZONE| + SD_RESOLVED_NO_TRUST_ANCHOR| + SD_RESOLVED_NO_NETWORK| + ok)) + return false; + + if ((*flags & SD_RESOLVED_PROTOCOLS_ALL) == 0) /* If no protocol is enabled, enable all */ + *flags |= SD_RESOLVED_PROTOCOLS_ALL; + + /* If the SD_RESOLVED_NO_SEARCH flag is acceptable, and the query name is dot-suffixed, turn off + * search domains. Note that DNS name normalization drops the dot suffix, hence we propagate this + * into the flags field as early as we can. */ + if (name && FLAGS_SET(ok, SD_RESOLVED_NO_SEARCH) && dns_name_dot_suffixed(name) > 0) + *flags |= SD_RESOLVED_NO_SEARCH; + + return true; +} + +static void vl_method_resolve_hostname_complete(DnsQuery *query) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL; + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = query; + _cleanup_free_ char *normalized = NULL; + DnsResourceRecord *rr; + DnsQuestion *question; + int ifindex, r; + + assert(q); + + if (q->state != DNS_TRANSACTION_SUCCESS) { + r = reply_query_state(q); + goto finish; + } + + r = dns_query_process_cname_many(q); + if (r == -ELOOP) { + r = varlink_error(q->varlink_request, "io.systemd.Resolve.CNAMELoop", NULL); + goto finish; + } + if (r < 0) + goto finish; + if (r == DNS_QUERY_CNAME) { + /* This was a cname, and the query was restarted. */ + TAKE_PTR(q); + return; + } + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) { + _cleanup_(json_variant_unrefp) JsonVariant *entry = NULL; + int family; + const void *p; + + r = dns_question_matches_rr(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain)); + if (r < 0) + goto finish; + if (r == 0) + continue; + + if (rr->key->type == DNS_TYPE_A) { + family = AF_INET; + p = &rr->a.in_addr; + } else if (rr->key->type == DNS_TYPE_AAAA) { + family = AF_INET6; + p = &rr->aaaa.in6_addr; + } else { + r = -EAFNOSUPPORT; + goto finish; + } + + r = json_build(&entry, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)), + JSON_BUILD_PAIR("family", JSON_BUILD_INTEGER(family)), + JSON_BUILD_PAIR("address", JSON_BUILD_BYTE_ARRAY(p, FAMILY_ADDRESS_SIZE(family))))); + if (r < 0) + goto finish; + + if (!canonical) + canonical = dns_resource_record_ref(rr); + + r = json_variant_append_array(&array, entry); + if (r < 0) + goto finish; + } + + if (json_variant_is_blank_object(array)) { + r = varlink_error(q->varlink_request, "io.systemd.Resolve.NoSuchResourceRecord", NULL); + goto finish; + } + + assert(canonical); + r = dns_name_normalize(dns_resource_key_name(canonical->key), 0, &normalized); + if (r < 0) + goto finish; + + r = varlink_replyb(q->varlink_request, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("addresses", JSON_BUILD_VARIANT(array)), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(normalized)), + JSON_BUILD_PAIR("flags", JSON_BUILD_INTEGER(dns_query_reply_flags_make(q))))); +finish: + if (r < 0) { + log_full_errno(ERRNO_IS_DISCONNECT(r) ? LOG_DEBUG : LOG_ERR, r, "Failed to send hostname reply: %m"); + r = varlink_error_errno(q->varlink_request, r); + } +} + +static int parse_as_address(Varlink *link, LookupParameters *p) { + _cleanup_free_ char *canonical = NULL; + int r, ff, parsed_ifindex, ifindex; + union in_addr_union parsed; + + assert(link); + assert(p); + + /* Check if this parses as literal address. If so, just parse it and return that, do not involve networking */ + r = in_addr_ifindex_from_string_auto(p->name, &ff, &parsed, &parsed_ifindex); + if (r < 0) + return 0; /* not a literal address */ + + /* Make sure the data we parsed matches what is requested */ + if ((p->family != AF_UNSPEC && ff != p->family) || + (p->ifindex > 0 && parsed_ifindex > 0 && parsed_ifindex != p->ifindex)) + return varlink_error(link, "io.systemd.Resolve.NoSuchResourceRecord", NULL); + + ifindex = parsed_ifindex > 0 ? parsed_ifindex : p->ifindex; + + /* Reformat the address as string, to return as canonicalized name */ + r = in_addr_ifindex_to_string(ff, &parsed, ifindex, &canonical); + if (r < 0) + return r; + + return varlink_replyb( + link, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("addresses", + JSON_BUILD_ARRAY( + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)), + JSON_BUILD_PAIR("family", JSON_BUILD_INTEGER(ff)), + JSON_BUILD_PAIR("address", JSON_BUILD_BYTE_ARRAY(&parsed, FAMILY_ADDRESS_SIZE(ff)))))), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(canonical)), + JSON_BUILD_PAIR("flags", JSON_BUILD_INTEGER(SD_RESOLVED_FLAGS_MAKE(dns_synthesize_protocol(p->flags), ff, true, true)| + SD_RESOLVED_SYNTHETIC)))); +} + +static int vl_method_resolve_hostname(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch dispatch_table[] = { + { "ifindex", JSON_VARIANT_UNSIGNED, json_dispatch_int, offsetof(LookupParameters, ifindex), 0 }, + { "name", JSON_VARIANT_STRING, json_dispatch_string, offsetof(LookupParameters, name), JSON_MANDATORY }, + { "family", JSON_VARIANT_UNSIGNED, json_dispatch_int, offsetof(LookupParameters, family), 0 }, + { "flags", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(LookupParameters, flags), 0 }, + {} + }; + + _cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL; + _cleanup_(lookup_parameters_destroy) LookupParameters p = { + .family = AF_UNSPEC, + }; + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + Manager *m; + int r; + + assert(link); + + m = varlink_server_get_userdata(varlink_get_server(link)); + assert(m); + + if (FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) + return -EINVAL; + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (p.ifindex < 0) + return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("ifindex")); + + r = dns_name_is_valid(p.name); + if (r < 0) + return r; + if (r == 0) + return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("name")); + + if (!IN_SET(p.family, AF_UNSPEC, AF_INET, AF_INET6)) + return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("family")); + + if (!validate_and_mangle_flags(p.name, &p.flags, SD_RESOLVED_NO_SEARCH)) + return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("flags")); + + r = parse_as_address(link, &p); + if (r != 0) + return r; + + r = dns_question_new_address(&question_utf8, p.family, p.name, false); + if (r < 0) + return r; + + r = dns_question_new_address(&question_idna, p.family, p.name, true); + if (r < 0 && r != -EALREADY) + return r; + + r = dns_query_new(m, &q, question_utf8, question_idna ?: question_utf8, NULL, p.ifindex, p.flags); + if (r < 0) + return r; + + q->varlink_request = varlink_ref(link); + varlink_set_userdata(link, q); + q->request_family = p.family; + q->complete = vl_method_resolve_hostname_complete; + + r = dns_query_go(q); + if (r < 0) + return r; + + TAKE_PTR(q); + return 1; +} + +static int json_dispatch_address(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) { + LookupParameters *p = ASSERT_PTR(userdata); + union in_addr_union buf = {}; + JsonVariant *i; + size_t n, k = 0; + + assert(variant); + + if (!json_variant_is_array(variant)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name)); + + n = json_variant_elements(variant); + if (!IN_SET(n, 4, 16)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is array of unexpected size.", strna(name)); + + JSON_VARIANT_ARRAY_FOREACH(i, variant) { + int64_t b; + + if (!json_variant_is_integer(i)) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "Element %zu of JSON field '%s' is not an integer.", k, strna(name)); + + b = json_variant_integer(i); + if (b < 0 || b > 0xff) + return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), + "Element %zu of JSON field '%s' is out of range 0%s255.", + k, strna(name), special_glyph(SPECIAL_GLYPH_ELLIPSIS)); + + buf.bytes[k++] = (uint8_t) b; + } + + p->address = buf; + p->address_size = k; + + return 0; +} + +static void vl_method_resolve_address_complete(DnsQuery *query) { + _cleanup_(json_variant_unrefp) JsonVariant *array = NULL; + _cleanup_(dns_query_freep) DnsQuery *q = query; + DnsQuestion *question; + DnsResourceRecord *rr; + int ifindex, r; + + assert(q); + + if (q->state != DNS_TRANSACTION_SUCCESS) { + r = reply_query_state(q); + goto finish; + } + + r = dns_query_process_cname_many(q); + if (r == -ELOOP) { + r = varlink_error(q->varlink_request, "io.systemd.Resolve.CNAMELoop", NULL); + goto finish; + } + if (r < 0) + goto finish; + if (r == DNS_QUERY_CNAME) { + /* This was a cname, and the query was restarted. */ + TAKE_PTR(q); + return; + } + + question = dns_query_question_for_protocol(q, q->answer_protocol); + + DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) { + _cleanup_(json_variant_unrefp) JsonVariant *entry = NULL; + _cleanup_free_ char *normalized = NULL; + + r = dns_question_matches_rr(question, rr, NULL); + if (r < 0) + goto finish; + if (r == 0) + continue; + + r = dns_name_normalize(rr->ptr.name, 0, &normalized); + if (r < 0) + goto finish; + + r = json_build(&entry, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)), + JSON_BUILD_PAIR("name", JSON_BUILD_STRING(normalized)))); + if (r < 0) + goto finish; + + r = json_variant_append_array(&array, entry); + if (r < 0) + goto finish; + } + + if (json_variant_is_blank_object(array)) { + r = varlink_error(q->varlink_request, "io.systemd.Resolve.NoSuchResourceRecord", NULL); + goto finish; + } + + r = varlink_replyb(q->varlink_request, + JSON_BUILD_OBJECT( + JSON_BUILD_PAIR("names", JSON_BUILD_VARIANT(array)), + JSON_BUILD_PAIR("flags", JSON_BUILD_INTEGER(dns_query_reply_flags_make(q))))); +finish: + if (r < 0) { + log_full_errno(ERRNO_IS_DISCONNECT(r) ? LOG_DEBUG : LOG_ERR, r, "Failed to send address reply: %m"); + r = varlink_error_errno(q->varlink_request, r); + } +} + +static int vl_method_resolve_address(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + static const JsonDispatch dispatch_table[] = { + { "ifindex", JSON_VARIANT_UNSIGNED, json_dispatch_int, offsetof(LookupParameters, ifindex), 0 }, + { "family", JSON_VARIANT_UNSIGNED, json_dispatch_int, offsetof(LookupParameters, family), JSON_MANDATORY }, + { "address", JSON_VARIANT_ARRAY, json_dispatch_address, 0, JSON_MANDATORY }, + { "flags", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(LookupParameters, flags), 0 }, + {} + }; + + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + _cleanup_(lookup_parameters_destroy) LookupParameters p = { + .family = AF_UNSPEC, + }; + _cleanup_(dns_query_freep) DnsQuery *q = NULL; + Manager *m; + int r; + + assert(link); + + m = varlink_server_get_userdata(varlink_get_server(link)); + assert(m); + + if (FLAGS_SET(flags, VARLINK_METHOD_ONEWAY)) + return -EINVAL; + + r = json_dispatch(parameters, dispatch_table, NULL, 0, &p); + if (r < 0) + return r; + + if (p.ifindex < 0) + return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("ifindex")); + + if (!IN_SET(p.family, AF_INET, AF_INET6)) + return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("family")); + + if (FAMILY_ADDRESS_SIZE(p.family) != p.address_size) + return varlink_error(link, "io.systemd.Resolve.BadAddressSize", NULL); + + if (!validate_and_mangle_flags(NULL, &p.flags, 0)) + return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("flags")); + + r = dns_question_new_reverse(&question, p.family, &p.address); + if (r < 0) + return r; + + r = dns_query_new(m, &q, question, question, NULL, p.ifindex, p.flags|SD_RESOLVED_NO_SEARCH); + if (r < 0) + return r; + + q->varlink_request = varlink_ref(link); + varlink_set_userdata(link, q); + + q->request_family = p.family; + q->request_address = p.address; + q->complete = vl_method_resolve_address_complete; + + r = dns_query_go(q); + if (r < 0) + return r; + + TAKE_PTR(q); + return 1; +} + +static int vl_method_subscribe_dns_resolves(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) { + Manager *m; + int r; + + assert(link); + + m = ASSERT_PTR(varlink_server_get_userdata(varlink_get_server(link))); + + /* if the client didn't set the more flag, it is using us incorrectly */ + if (!FLAGS_SET(flags, VARLINK_METHOD_MORE)) + return varlink_error_invalid_parameter(link, NULL); + + if (json_variant_elements(parameters) > 0) + return varlink_error_invalid_parameter(link, parameters); + + /* Send a ready message to the connecting client, to indicate that we are now listinening, and all + * queries issued after the point the client sees this will also be reported to the client. */ + r = varlink_notifyb(link, + JSON_BUILD_OBJECT(JSON_BUILD_PAIR("ready", JSON_BUILD_BOOLEAN(true)))); + if (r < 0) + return log_error_errno(r, "Failed to report monitor to be established: %m"); + + r = set_ensure_put(&m->varlink_subscription, NULL, link); + if (r < 0) + return log_error_errno(r, "Failed to add subscription to set: %m"); + varlink_ref(link); + + log_debug("%u clients now attached for varlink notifications", set_size(m->varlink_subscription)); + + return 1; +} + +static int varlink_monitor_server_init(Manager *m) { + _cleanup_(varlink_server_unrefp) VarlinkServer *server = NULL; + int r; + + assert(m); + + if (m->varlink_monitor_server) + return 0; + + r = varlink_server_new(&server, VARLINK_SERVER_ROOT_ONLY); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + varlink_server_set_userdata(server, m); + + r = varlink_server_bind_method( + server, + "io.systemd.Resolve.Monitor.SubscribeQueryResults", + vl_method_subscribe_dns_resolves); + if (r < 0) + return log_error_errno(r, "Failed to register varlink methods: %m"); + + r = varlink_server_bind_disconnect(server, vl_on_notification_disconnect); + if (r < 0) + return log_error_errno(r, "Failed to register varlink disconnect handler: %m"); + + r = varlink_server_listen_address(server, "/run/systemd/resolve/io.systemd.Resolve.Monitor", 0600); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = varlink_server_attach_event(server, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + m->varlink_monitor_server = TAKE_PTR(server); + + return 0; +} + +static int varlink_main_server_init(Manager *m) { + _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL; + int r; + + assert(m); + + if (m->varlink_server) + return 0; + + r = varlink_server_new(&s, VARLINK_SERVER_ACCOUNT_UID); + if (r < 0) + return log_error_errno(r, "Failed to allocate varlink server object: %m"); + + varlink_server_set_userdata(s, m); + + r = varlink_server_bind_method_many( + s, + "io.systemd.Resolve.ResolveHostname", vl_method_resolve_hostname, + "io.systemd.Resolve.ResolveAddress", vl_method_resolve_address); + if (r < 0) + return log_error_errno(r, "Failed to register varlink methods: %m"); + + r = varlink_server_bind_disconnect(s, vl_on_disconnect); + if (r < 0) + return log_error_errno(r, "Failed to register varlink disconnect handler: %m"); + + r = varlink_server_listen_address(s, "/run/systemd/resolve/io.systemd.Resolve", 0666); + if (r < 0) + return log_error_errno(r, "Failed to bind to varlink socket: %m"); + + r = varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL); + if (r < 0) + return log_error_errno(r, "Failed to attach varlink connection to event loop: %m"); + + m->varlink_server = TAKE_PTR(s); + return 0; +} + +int manager_varlink_init(Manager *m) { + int r; + + r = varlink_main_server_init(m); + if (r < 0) + return r; + + r = varlink_monitor_server_init(m); + if (r < 0) + return r; + + return 0; +} + +void manager_varlink_done(Manager *m) { + assert(m); + + m->varlink_server = varlink_server_unref(m->varlink_server); + m->varlink_monitor_server = varlink_server_unref(m->varlink_monitor_server); +} diff --git a/src/resolve/resolved-varlink.h b/src/resolve/resolved-varlink.h new file mode 100644 index 0000000..57fdfe9 --- /dev/null +++ b/src/resolve/resolved-varlink.h @@ -0,0 +1,7 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +#pragma once + +#include "resolved-manager.h" + +int manager_varlink_init(Manager *m); +void manager_varlink_done(Manager *m); diff --git a/src/resolve/resolved.c b/src/resolve/resolved.c new file mode 100644 index 0000000..d3bc902 --- /dev/null +++ b/src/resolve/resolved.c @@ -0,0 +1,99 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "sd-daemon.h" +#include "sd-event.h" + +#include "bus-log-control-api.h" +#include "capability-util.h" +#include "daemon-util.h" +#include "main-func.h" +#include "mkdir-label.h" +#include "resolved-bus.h" +#include "resolved-conf.h" +#include "resolved-manager.h" +#include "resolved-resolv-conf.h" +#include "selinux-util.h" +#include "service-util.h" +#include "signal-util.h" +#include "user-util.h" + +static int run(int argc, char *argv[]) { + _cleanup_(manager_freep) Manager *m = NULL; + _unused_ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL; + int r; + + log_setup(); + + r = service_parse_argv("systemd-resolved.service", + "Provide name resolution with caching using DNS, mDNS, LLMNR.", + BUS_IMPLEMENTATIONS(&manager_object, + &log_control_object), + argc, argv); + if (r <= 0) + return r; + + umask(0022); + + r = mac_selinux_init(); + if (r < 0) + return r; + + /* Drop privileges, but only if we have been started as root. If we are not running as root we assume most + * privileges are already dropped and we can't create our directory. */ + if (getuid() == 0) { + const char *user = "systemd-resolve"; + uid_t uid; + gid_t gid; + + r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0); + if (r < 0) + return log_error_errno(r, "Cannot resolve user name %s: %m", user); + + /* As we're root, we can create the directory where resolv.conf will live */ + r = mkdir_safe_label("/run/systemd/resolve", 0755, uid, gid, MKDIR_WARN_MODE); + if (r < 0) + return log_error_errno(r, "Could not create runtime directory: %m"); + + /* Drop privileges, but keep three caps. Note that we drop two of those too, later on (see below) */ + r = drop_privileges(uid, gid, + (UINT64_C(1) << CAP_NET_RAW)| /* needed for SO_BINDTODEVICE */ + (UINT64_C(1) << CAP_NET_BIND_SERVICE)| /* needed to bind on port 53 */ + (UINT64_C(1) << CAP_SETPCAP) /* needed in order to drop the caps later */); + if (r < 0) + return log_error_errno(r, "Failed to drop privileges: %m"); + } + + assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGUSR1, SIGUSR2, SIGRTMIN+1, -1) >= 0); + + r = manager_new(&m); + if (r < 0) + return log_error_errno(r, "Could not create manager: %m"); + + r = manager_start(m); + if (r < 0) + return log_error_errno(r, "Failed to start manager: %m"); + + /* Write finish default resolv.conf to avoid a dangling symlink */ + (void) manager_write_resolv_conf(m); + + (void) manager_check_resolv_conf(m); + + /* Let's drop the remaining caps now */ + r = capability_bounding_set_drop((UINT64_C(1) << CAP_NET_RAW), true); + if (r < 0) + return log_error_errno(r, "Failed to drop remaining caps: %m"); + + notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING); + + r = sd_event_loop(m->event); + if (r < 0) + return log_error_errno(r, "Event loop failed: %m"); + + return 0; +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/resolve/resolved.conf.in b/src/resolve/resolved.conf.in new file mode 100644 index 0000000..6d4176d --- /dev/null +++ b/src/resolve/resolved.conf.in @@ -0,0 +1,34 @@ +# This file is part of systemd. +# +# 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. +# +# Entries in this file show the compile time defaults. Local configuration +# should be created by either modifying this file, or by creating "drop-ins" in +# the resolved.conf.d/ subdirectory. The latter is generally recommended. +# Defaults can be restored by simply deleting this file and all drop-ins. +# +# Use 'systemd-analyze cat-config systemd/resolved.conf' to display the full config. +# +# See resolved.conf(5) for details. + +[Resolve] +# Some examples of DNS servers which may be used for DNS= and FallbackDNS=: +# Cloudflare: 1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com +# Google: 8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google +# Quad9: 9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net +#DNS= +#FallbackDNS={{DNS_SERVERS}} +#Domains= +#DNSSEC={{DEFAULT_DNSSEC_MODE_STR}} +#DNSOverTLS={{DEFAULT_DNS_OVER_TLS_MODE_STR}} +#MulticastDNS={{DEFAULT_MDNS_MODE_STR}} +#LLMNR={{DEFAULT_LLMNR_MODE_STR}} +#Cache=yes +#CacheFromLocalhost=no +#DNSStubListener=yes +#DNSStubListenerExtra= +#ReadEtcHosts=yes +#ResolveUnicastSingleLabel=no diff --git a/src/resolve/test-dns-packet.c b/src/resolve/test-dns-packet.c new file mode 100644 index 0000000..01f15ca --- /dev/null +++ b/src/resolve/test-dns-packet.c @@ -0,0 +1,155 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <net/if.h> + +#include "sd-id128.h" + +#include "alloc-util.h" +#include "fileio.h" +#include "glob-util.h" +#include "log.h" +#include "macro.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-rr.h" +#include "path-util.h" +#include "string-util.h" +#include "strv.h" +#include "tests.h" +#include "unaligned.h" + +#define HASH_KEY SD_ID128_MAKE(d3,1e,48,90,4b,fa,4c,fe,af,9d,d5,a1,d7,2e,8a,b1) + +static void verify_rr_copy(DnsResourceRecord *rr) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *copy = NULL; + const char *a, *b; + + assert_se(copy = dns_resource_record_copy(rr)); + assert_se(dns_resource_record_equal(copy, rr) > 0); + + assert_se(a = dns_resource_record_to_string(rr)); + assert_se(b = dns_resource_record_to_string(copy)); + + assert_se(streq(a, b)); +} + +static uint64_t hash(DnsResourceRecord *rr) { + struct siphash state; + + siphash24_init(&state, HASH_KEY.bytes); + dns_resource_record_hash_func(rr, &state); + return siphash24_finalize(&state); +} + +static void test_packet_from_file(const char* filename, bool canonical) { + _cleanup_free_ char *data = NULL; + size_t data_size, packet_size, offset; + + assert_se(read_full_file(filename, &data, &data_size) >= 0); + assert_se(data); + assert_se(data_size > 8); + + log_info("============== %s %s==============", filename, canonical ? "canonical " : ""); + + for (offset = 0; offset < data_size; offset += 8 + packet_size) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL, *p2 = NULL; + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL, *rr2 = NULL; + const char *s, *s2; + uint64_t hash1, hash2; + + packet_size = unaligned_read_le64(data + offset); + assert_se(packet_size > 0); + assert_se(offset + 8 + packet_size <= data_size); + + assert_se(dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0); + + assert_se(dns_packet_append_blob(p, data + offset + 8, packet_size, NULL) >= 0); + assert_se(dns_packet_read_rr(p, &rr, NULL, NULL) >= 0); + + verify_rr_copy(rr); + + s = dns_resource_record_to_string(rr); + assert_se(s); + puts(s); + + hash1 = hash(rr); + + assert_se(dns_resource_record_to_wire_format(rr, canonical) >= 0); + + assert_se(dns_packet_new(&p2, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0); + assert_se(dns_packet_append_blob(p2, rr->wire_format, rr->wire_format_size, NULL) >= 0); + assert_se(dns_packet_read_rr(p2, &rr2, NULL, NULL) >= 0); + + verify_rr_copy(rr); + + s2 = dns_resource_record_to_string(rr); + assert_se(s2); + assert_se(streq(s, s2)); + + hash2 = hash(rr); + assert_se(hash1 == hash2); + } +} + +static void test_dns_resource_record_get_cname_target(void) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *cname = NULL, *dname = NULL; + _cleanup_free_ char *target = NULL; + + assert_se(cname = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_CNAME, "quux.foobar")); + assert_se(cname->cname.name = strdup("wuff.wuff")); + + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "waldo"), cname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "foobar"), cname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux"), cname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, ""), cname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "."), cname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "nope.quux.foobar"), cname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux.foobar"), cname, &target) == 0); + assert_se(streq(target, "wuff.wuff")); + target = mfree(target); + + assert_se(dname = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNAME, "quux.foobar")); + assert_se(dname->dname.name = strdup("wuff.wuff")); + + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "waldo"), dname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "foobar"), dname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux"), dname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, ""), dname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "."), dname, &target) == -EUNATCH); + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "yupp.quux.foobar"), dname, &target) == 0); + assert_se(streq(target, "yupp.wuff.wuff")); + target = mfree(target); + + assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux.foobar"), cname, &target) == 0); + assert_se(streq(target, "wuff.wuff")); +} + +int main(int argc, char **argv) { + int N; + _cleanup_globfree_ glob_t g = {}; + char **fnames; + + log_parse_environment(); + + if (argc >= 2) { + N = argc - 1; + fnames = argv + 1; + } else { + _cleanup_free_ char *pkts_glob = NULL; + assert_se(get_testdata_dir("test-resolve/*.pkts", &pkts_glob) >= 0); + assert_se(glob(pkts_glob, GLOB_NOSORT, NULL, &g) == 0); + N = g.gl_pathc; + fnames = g.gl_pathv; + } + + for (int i = 0; i < N; i++) { + test_packet_from_file(fnames[i], false); + puts(""); + test_packet_from_file(fnames[i], true); + if (i + 1 < N) + puts(""); + } + + test_dns_resource_record_get_cname_target(); + + return EXIT_SUCCESS; +} diff --git a/src/resolve/test-dnssec-complex.c b/src/resolve/test-dnssec-complex.c new file mode 100644 index 0000000..d8aeb70 --- /dev/null +++ b/src/resolve/test-dnssec-complex.c @@ -0,0 +1,223 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <netinet/ip.h> + +#include "sd-bus.h" + +#include "af-list.h" +#include "alloc-util.h" +#include "bus-common-errors.h" +#include "dns-type.h" +#include "random-util.h" +#include "resolved-def.h" +#include "string-util.h" +#include "time-util.h" + +static void prefix_random(const char *name, char **ret) { + uint64_t i, u; + char *m = NULL; + + u = 1 + (random_u64() & 3); + + for (i = 0; i < u; i++) { + _cleanup_free_ char *b = NULL; + char *x; + + assert_se(asprintf(&b, "x%" PRIu64 "x", random_u64())); + x = strjoin(b, ".", name); + assert_se(x); + + free(m); + m = x; + } + + *ret = m; + } + +static void test_rr_lookup(sd_bus *bus, const char *name, uint16_t type, const char *result) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *m = NULL; + int r; + + /* If the name starts with a dot, we prefix one to three random labels */ + if (startswith(name, ".")) { + prefix_random(name + 1, &m); + name = m; + } + + assert_se(sd_bus_message_new_method_call( + bus, + &req, + "org.freedesktop.resolve1", + "/org/freedesktop/resolve1", + "org.freedesktop.resolve1.Manager", + "ResolveRecord") >= 0); + + assert_se(sd_bus_message_append(req, "isqqt", 0, name, DNS_CLASS_IN, type, UINT64_C(0)) >= 0); + + r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply); + + if (r < 0) { + assert_se(result); + assert_se(sd_bus_error_has_name(&error, result)); + log_info("[OK] %s/%s resulted in <%s>.", name, dns_type_to_string(type), error.name); + } else { + assert_se(!result); + log_info("[OK] %s/%s succeeded.", name, dns_type_to_string(type)); + } +} + +static void test_hostname_lookup(sd_bus *bus, const char *name, int family, const char *result) { + _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL; + _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL; + _cleanup_free_ char *m = NULL; + const char *af; + int r; + + af = family == AF_UNSPEC ? "AF_UNSPEC" : af_to_name(family); + + /* If the name starts with a dot, we prefix one to three random labels */ + if (startswith(name, ".")) { + prefix_random(name + 1, &m); + name = m; + } + + assert_se(sd_bus_message_new_method_call( + bus, + &req, + "org.freedesktop.resolve1", + "/org/freedesktop/resolve1", + "org.freedesktop.resolve1.Manager", + "ResolveHostname") >= 0); + + assert_se(sd_bus_message_append(req, "isit", 0, name, family, UINT64_C(0)) >= 0); + + r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply); + + if (r < 0) { + assert_se(result); + assert_se(sd_bus_error_has_name(&error, result)); + log_info("[OK] %s/%s resulted in <%s>.", name, af, error.name); + } else { + assert_se(!result); + log_info("[OK] %s/%s succeeded.", name, af); + } + +} + +int main(int argc, char* argv[]) { + _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL; + + /* Note that this is a manual test as it requires: + * + * Full network access + * A DNSSEC capable DNS server + * That zones contacted are still set up as they were when I wrote this. + */ + + assert_se(sd_bus_open_system(&bus) >= 0); + + /* Normally signed */ + test_rr_lookup(bus, "www.eurid.eu", DNS_TYPE_A, NULL); + test_hostname_lookup(bus, "www.eurid.eu", AF_UNSPEC, NULL); + + test_rr_lookup(bus, "sigok.verteiltesysteme.net", DNS_TYPE_A, NULL); + test_hostname_lookup(bus, "sigok.verteiltesysteme.net", AF_UNSPEC, NULL); + + /* Normally signed, NODATA */ + test_rr_lookup(bus, "www.eurid.eu", DNS_TYPE_RP, BUS_ERROR_NO_SUCH_RR); + test_rr_lookup(bus, "sigok.verteiltesysteme.net", DNS_TYPE_RP, BUS_ERROR_NO_SUCH_RR); + + /* Invalid signature */ + test_rr_lookup(bus, "sigfail.verteiltesysteme.net", DNS_TYPE_A, BUS_ERROR_DNSSEC_FAILED); + test_hostname_lookup(bus, "sigfail.verteiltesysteme.net", AF_INET, BUS_ERROR_DNSSEC_FAILED); + + /* Invalid signature, RSA, wildcard */ + test_rr_lookup(bus, ".wilda.rhybar.0skar.cz", DNS_TYPE_A, BUS_ERROR_DNSSEC_FAILED); + test_hostname_lookup(bus, ".wilda.rhybar.0skar.cz", AF_INET, BUS_ERROR_DNSSEC_FAILED); + + /* Invalid signature, ECDSA, wildcard */ + test_rr_lookup(bus, ".wilda.rhybar.ecdsa.0skar.cz", DNS_TYPE_A, BUS_ERROR_DNSSEC_FAILED); + test_hostname_lookup(bus, ".wilda.rhybar.ecdsa.0skar.cz", AF_INET, BUS_ERROR_DNSSEC_FAILED); + + /* Missing DS for DNSKEY */ + test_rr_lookup(bus, "www.dnssec-bogus.sg", DNS_TYPE_A, BUS_ERROR_DNSSEC_FAILED); + test_hostname_lookup(bus, "www.dnssec-bogus.sg", AF_INET, BUS_ERROR_DNSSEC_FAILED); + + /* NXDOMAIN in NSEC domain */ + test_rr_lookup(bus, "hhh.nasa.gov", DNS_TYPE_A, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, "hhh.nasa.gov", AF_UNSPEC, BUS_ERROR_DNS_NXDOMAIN); + test_rr_lookup(bus, "_pgpkey-https._tcp.hkps.pool.sks-keyservers.net", DNS_TYPE_SRV, BUS_ERROR_DNS_NXDOMAIN); + + /* wildcard, NSEC zone */ + test_rr_lookup(bus, ".wilda.nsec.0skar.cz", DNS_TYPE_A, NULL); + test_hostname_lookup(bus, ".wilda.nsec.0skar.cz", AF_INET, NULL); + + /* wildcard, NSEC zone, NODATA */ + test_rr_lookup(bus, ".wilda.nsec.0skar.cz", DNS_TYPE_RP, BUS_ERROR_NO_SUCH_RR); + + /* wildcard, NSEC3 zone */ + test_rr_lookup(bus, ".wilda.0skar.cz", DNS_TYPE_A, NULL); + test_hostname_lookup(bus, ".wilda.0skar.cz", AF_INET, NULL); + + /* wildcard, NSEC3 zone, NODATA */ + test_rr_lookup(bus, ".wilda.0skar.cz", DNS_TYPE_RP, BUS_ERROR_NO_SUCH_RR); + + /* wildcard, NSEC zone, CNAME */ + test_rr_lookup(bus, ".wild.nsec.0skar.cz", DNS_TYPE_A, NULL); + test_hostname_lookup(bus, ".wild.nsec.0skar.cz", AF_UNSPEC, NULL); + test_hostname_lookup(bus, ".wild.nsec.0skar.cz", AF_INET, NULL); + + /* wildcard, NSEC zone, NODATA, CNAME */ + test_rr_lookup(bus, ".wild.nsec.0skar.cz", DNS_TYPE_RP, BUS_ERROR_NO_SUCH_RR); + + /* wildcard, NSEC3 zone, CNAME */ + test_rr_lookup(bus, ".wild.0skar.cz", DNS_TYPE_A, NULL); + test_hostname_lookup(bus, ".wild.0skar.cz", AF_UNSPEC, NULL); + test_hostname_lookup(bus, ".wild.0skar.cz", AF_INET, NULL); + + /* wildcard, NSEC3 zone, NODATA, CNAME */ + test_rr_lookup(bus, ".wild.0skar.cz", DNS_TYPE_RP, BUS_ERROR_NO_SUCH_RR); + + /* NODATA due to empty non-terminal in NSEC domain */ + test_rr_lookup(bus, "herndon.nasa.gov", DNS_TYPE_A, BUS_ERROR_NO_SUCH_RR); + test_hostname_lookup(bus, "herndon.nasa.gov", AF_UNSPEC, BUS_ERROR_NO_SUCH_RR); + test_hostname_lookup(bus, "herndon.nasa.gov", AF_INET, BUS_ERROR_NO_SUCH_RR); + test_hostname_lookup(bus, "herndon.nasa.gov", AF_INET6, BUS_ERROR_NO_SUCH_RR); + + /* NXDOMAIN in NSEC root zone: */ + test_rr_lookup(bus, "jasdhjas.kjkfgjhfjg", DNS_TYPE_A, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, "jasdhjas.kjkfgjhfjg", AF_UNSPEC, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, "jasdhjas.kjkfgjhfjg", AF_INET, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, "jasdhjas.kjkfgjhfjg", AF_INET6, BUS_ERROR_DNS_NXDOMAIN); + + /* NXDOMAIN in NSEC3 .com zone: */ + test_rr_lookup(bus, "kjkfgjhfjgsdfdsfd.com", DNS_TYPE_A, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, "kjkfgjhfjgsdfdsfd.com", AF_INET, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, "kjkfgjhfjgsdfdsfd.com", AF_INET6, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, "kjkfgjhfjgsdfdsfd.com", AF_UNSPEC, BUS_ERROR_DNS_NXDOMAIN); + + /* Unsigned A */ + test_rr_lookup(bus, "poettering.de", DNS_TYPE_A, NULL); + test_rr_lookup(bus, "poettering.de", DNS_TYPE_AAAA, NULL); + test_hostname_lookup(bus, "poettering.de", AF_UNSPEC, NULL); + test_hostname_lookup(bus, "poettering.de", AF_INET, NULL); + test_hostname_lookup(bus, "poettering.de", AF_INET6, NULL); + +#if HAVE_LIBIDN2 || HAVE_LIBIDN + /* Unsigned A with IDNA conversion necessary */ + test_hostname_lookup(bus, "pöttering.de", AF_UNSPEC, NULL); + test_hostname_lookup(bus, "pöttering.de", AF_INET, NULL); + test_hostname_lookup(bus, "pöttering.de", AF_INET6, NULL); +#endif + + /* DNAME, pointing to NXDOMAIN */ + test_rr_lookup(bus, ".ireallyhpoethisdoesnexist.xn--kprw13d.", DNS_TYPE_A, BUS_ERROR_DNS_NXDOMAIN); + test_rr_lookup(bus, ".ireallyhpoethisdoesnexist.xn--kprw13d.", DNS_TYPE_RP, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, ".ireallyhpoethisdoesntexist.xn--kprw13d.", AF_UNSPEC, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, ".ireallyhpoethisdoesntexist.xn--kprw13d.", AF_INET, BUS_ERROR_DNS_NXDOMAIN); + test_hostname_lookup(bus, ".ireallyhpoethisdoesntexist.xn--kprw13d.", AF_INET6, BUS_ERROR_DNS_NXDOMAIN); + + return 0; +} diff --git a/src/resolve/test-dnssec.c b/src/resolve/test-dnssec.c new file mode 100644 index 0000000..d325b53 --- /dev/null +++ b/src/resolve/test-dnssec.c @@ -0,0 +1,787 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <netinet/in.h> +#include <sys/socket.h> + +#if HAVE_GCRYPT +# include <gcrypt.h> +#endif + +#include "alloc-util.h" +#include "hexdecoct.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-rr.h" +#include "string-util.h" +#include "tests.h" + +TEST(dnssec_verify_dns_key) { + static const uint8_t ds1_fprint[] = { + 0x46, 0x8B, 0xC8, 0xDD, 0xC7, 0xE8, 0x27, 0x03, 0x40, 0xBB, 0x8A, 0x1F, 0x3B, 0x2E, 0x45, 0x9D, + 0x80, 0x67, 0x14, 0x01, + }; + static const uint8_t ds2_fprint[] = { + 0x8A, 0xEE, 0x80, 0x47, 0x05, 0x5F, 0x83, 0xD1, 0x48, 0xBA, 0x8F, 0xF6, 0xDD, 0xA7, 0x60, 0xCE, + 0x94, 0xF7, 0xC7, 0x5E, 0x52, 0x4C, 0xF2, 0xE9, 0x50, 0xB9, 0x2E, 0xCB, 0xEF, 0x96, 0xB9, 0x98, + }; + static const uint8_t dnskey_blob[] = { + 0x03, 0x01, 0x00, 0x01, 0xa8, 0x12, 0xda, 0x4f, 0xd2, 0x7d, 0x54, 0x14, 0x0e, 0xcc, 0x5b, 0x5e, + 0x45, 0x9c, 0x96, 0x98, 0xc0, 0xc0, 0x85, 0x81, 0xb1, 0x47, 0x8c, 0x7d, 0xe8, 0x39, 0x50, 0xcc, + 0xc5, 0xd0, 0xf2, 0x00, 0x81, 0x67, 0x79, 0xf6, 0xcc, 0x9d, 0xad, 0x6c, 0xbb, 0x7b, 0x6f, 0x48, + 0x97, 0x15, 0x1c, 0xfd, 0x0b, 0xfe, 0xd3, 0xd7, 0x7d, 0x9f, 0x81, 0x26, 0xd3, 0xc5, 0x65, 0x49, + 0xcf, 0x46, 0x62, 0xb0, 0x55, 0x6e, 0x47, 0xc7, 0x30, 0xef, 0x51, 0xfb, 0x3e, 0xc6, 0xef, 0xde, + 0x27, 0x3f, 0xfa, 0x57, 0x2d, 0xa7, 0x1d, 0x80, 0x46, 0x9a, 0x5f, 0x14, 0xb3, 0xb0, 0x2c, 0xbe, + 0x72, 0xca, 0xdf, 0xb2, 0xff, 0x36, 0x5b, 0x4f, 0xec, 0x58, 0x8e, 0x8d, 0x01, 0xe9, 0xa9, 0xdf, + 0xb5, 0x60, 0xad, 0x52, 0x4d, 0xfc, 0xa9, 0x3e, 0x8d, 0x35, 0x95, 0xb3, 0x4e, 0x0f, 0xca, 0x45, + 0x1b, 0xf7, 0xef, 0x3a, 0x88, 0x25, 0x08, 0xc7, 0x4e, 0x06, 0xc1, 0x62, 0x1a, 0xce, 0xd8, 0x77, + 0xbd, 0x02, 0x65, 0xf8, 0x49, 0xfb, 0xce, 0xf6, 0xa8, 0x09, 0xfc, 0xde, 0xb2, 0x09, 0x9d, 0x39, + 0xf8, 0x63, 0x9c, 0x32, 0x42, 0x7c, 0xa0, 0x30, 0x86, 0x72, 0x7a, 0x4a, 0xc6, 0xd4, 0xb3, 0x2d, + 0x24, 0xef, 0x96, 0x3f, 0xc2, 0xda, 0xd3, 0xf2, 0x15, 0x6f, 0xda, 0x65, 0x4b, 0x81, 0x28, 0x68, + 0xf4, 0xfe, 0x3e, 0x71, 0x4f, 0x50, 0x96, 0x72, 0x58, 0xa1, 0x89, 0xdd, 0x01, 0x61, 0x39, 0x39, + 0xc6, 0x76, 0xa4, 0xda, 0x02, 0x70, 0x3d, 0xc0, 0xdc, 0x8d, 0x70, 0x72, 0x04, 0x90, 0x79, 0xd4, + 0xec, 0x65, 0xcf, 0x49, 0x35, 0x25, 0x3a, 0x14, 0x1a, 0x45, 0x20, 0xeb, 0x31, 0xaf, 0x92, 0xba, + 0x20, 0xd3, 0xcd, 0xa7, 0x13, 0x44, 0xdc, 0xcf, 0xf0, 0x27, 0x34, 0xb9, 0xe7, 0x24, 0x6f, 0x73, + 0xe7, 0xea, 0x77, 0x03, + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *ds1 = NULL, *ds2 = NULL; + + /* The two DS RRs in effect for nasa.gov on 2015-12-01. */ + ds1 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "nasa.gov"); + assert_se(ds1); + + ds1->ds.key_tag = 47857; + ds1->ds.algorithm = DNSSEC_ALGORITHM_RSASHA256; + ds1->ds.digest_type = DNSSEC_DIGEST_SHA1; + ds1->ds.digest_size = sizeof(ds1_fprint); + ds1->ds.digest = memdup(ds1_fprint, ds1->ds.digest_size); + assert_se(ds1->ds.digest); + + log_info("DS1: %s", strna(dns_resource_record_to_string(ds1))); + + ds2 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "NASA.GOV"); + assert_se(ds2); + + ds2->ds.key_tag = 47857; + ds2->ds.algorithm = DNSSEC_ALGORITHM_RSASHA256; + ds2->ds.digest_type = DNSSEC_DIGEST_SHA256; + ds2->ds.digest_size = sizeof(ds2_fprint); + ds2->ds.digest = memdup(ds2_fprint, ds2->ds.digest_size); + assert_se(ds2->ds.digest); + + log_info("DS2: %s", strna(dns_resource_record_to_string(ds2))); + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "nasa.GOV"); + assert_se(dnskey); + + dnskey->dnskey.flags = 257; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_RSASHA256; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + log_info("DNSKEY keytag: %u", dnssec_keytag(dnskey, false)); + + assert_se(dnssec_verify_dnskey_by_ds(dnskey, ds1, false) > 0); + assert_se(dnssec_verify_dnskey_by_ds(dnskey, ds2, false) > 0); +} + +TEST(dnssec_verify_rfc8080_ed25519_example1) { + static const uint8_t dnskey_blob[] = { + 0x97, 0x4d, 0x96, 0xa2, 0x2d, 0x22, 0x4b, 0xc0, 0x1a, 0xdb, 0x91, 0x50, 0x91, 0x47, 0x7d, + 0x44, 0xcc, 0xd9, 0x1c, 0x9a, 0x41, 0xa1, 0x14, 0x30, 0x01, 0x01, 0x17, 0xd5, 0x2c, 0x59, + 0x24, 0xe + }; + static const uint8_t ds_fprint[] = { + 0xdd, 0xa6, 0xb9, 0x69, 0xbd, 0xfb, 0x79, 0xf7, 0x1e, 0xe7, 0xb7, 0xfb, 0xdf, 0xb7, 0xdc, + 0xd7, 0xad, 0xbb, 0xd3, 0x5d, 0xdf, 0x79, 0xed, 0x3b, 0x6d, 0xd7, 0xf6, 0xe3, 0x56, 0xdd, + 0xd7, 0x47, 0xf7, 0x6f, 0x5f, 0x7a, 0xe1, 0xa6, 0xf9, 0xe5, 0xce, 0xfc, 0x7b, 0xbf, 0x5a, + 0xdf, 0x4e, 0x1b + }; + static const uint8_t signature_blob[] = { + 0xa0, 0xbf, 0x64, 0xac, 0x9b, 0xa7, 0xef, 0x17, 0xc1, 0x38, 0x85, 0x9c, 0x18, 0x78, 0xbb, + 0x99, 0xa8, 0x39, 0xfe, 0x17, 0x59, 0xac, 0xa5, 0xb0, 0xd7, 0x98, 0xcf, 0x1a, 0xb1, 0xe9, + 0x8d, 0x07, 0x91, 0x02, 0xf4, 0xdd, 0xb3, 0x36, 0x8f, 0x0f, 0xe4, 0x0b, 0xb3, 0x77, 0xf1, + 0xf0, 0x0e, 0x0c, 0xdd, 0xed, 0xb7, 0x99, 0x16, 0x7d, 0x56, 0xb6, 0xe9, 0x32, 0x78, 0x30, + 0x72, 0xba, 0x8d, 0x02 + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *ds = NULL, *mx = NULL, + *rrsig = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnssecResult result; + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "example.com."); + assert_se(dnskey); + + dnskey->dnskey.flags = 257; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_ED25519; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + + ds = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "example.com."); + assert_se(ds); + + ds->ds.key_tag = 3613; + ds->ds.algorithm = DNSSEC_ALGORITHM_ED25519; + ds->ds.digest_type = DNSSEC_DIGEST_SHA256; + ds->ds.digest_size = sizeof(ds_fprint); + ds->ds.digest = memdup(ds_fprint, ds->ds.digest_size); + assert_se(ds->ds.digest); + + log_info("DS: %s", strna(dns_resource_record_to_string(ds))); + + mx = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_MX, "example.com."); + assert_se(mx); + + mx->mx.priority = 10; + mx->mx.exchange = strdup("mail.example.com."); + assert_se(mx->mx.exchange); + + log_info("MX: %s", strna(dns_resource_record_to_string(mx))); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "example.com."); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_MX; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_ED25519; + rrsig->rrsig.labels = 2; + rrsig->rrsig.original_ttl = 3600; + rrsig->rrsig.expiration = 1440021600; + rrsig->rrsig.inception = 1438207200; + rrsig->rrsig.key_tag = 3613; + rrsig->rrsig.signer = strdup("example.com."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig))); + + assert_se(dnssec_key_match_rrsig(mx->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0); + + answer = dns_answer_new(1); + assert_se(answer); + assert_se(dns_answer_add(answer, mx, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + + assert_se(dnssec_verify_rrset(answer, mx->key, rrsig, dnskey, + rrsig->rrsig.inception * USEC_PER_SEC, &result) >= 0); +#if PREFER_OPENSSL || GCRYPT_VERSION_NUMBER >= 0x010600 + assert_se(result == DNSSEC_VALIDATED); +#else + assert_se(result == DNSSEC_UNSUPPORTED_ALGORITHM); +#endif +} + +TEST(dnssec_verify_rfc8080_ed25519_example2) { + static const uint8_t dnskey_blob[] = { + 0xcc, 0xf9, 0xd9, 0xfd, 0x0c, 0x04, 0x7b, 0xb4, 0xbc, 0x0b, 0x94, 0x8f, 0xcf, 0x63, 0x9f, + 0x4b, 0x94, 0x51, 0xe3, 0x40, 0x13, 0x93, 0x6f, 0xeb, 0x62, 0x71, 0x3d, 0xc4, 0x72, 0x4, + 0x8a, 0x3b + }; + static const uint8_t ds_fprint[] = { + 0xe3, 0x4d, 0x7b, 0xf3, 0x56, 0xfd, 0xdf, 0x87, 0xb7, 0xf7, 0x67, 0x5e, 0xe3, 0xdd, 0x9e, + 0x73, 0xbe, 0xda, 0x7b, 0x67, 0xb5, 0xe5, 0xde, 0xf4, 0x7f, 0xae, 0x7b, 0xe5, 0xad, 0x5c, + 0xd1, 0xb7, 0x39, 0xf5, 0xce, 0x76, 0xef, 0x97, 0x34, 0xe1, 0xe6, 0xde, 0xf3, 0x47, 0x3a, + 0xeb, 0x5e, 0x1c + }; + static const uint8_t signature_blob[] = { + 0xcd, 0x74, 0x34, 0x6e, 0x46, 0x20, 0x41, 0x31, 0x05, 0xc9, 0xf2, 0xf2, 0x8b, 0xd4, 0x28, + 0x89, 0x8e, 0x83, 0xf1, 0x97, 0x58, 0xa3, 0x8c, 0x32, 0x52, 0x15, 0x62, 0xa1, 0x86, 0x57, + 0x15, 0xd4, 0xf8, 0xd7, 0x44, 0x0f, 0x44, 0x84, 0xd0, 0x4a, 0xa2, 0x52, 0x9f, 0x34, 0x28, + 0x4a, 0x6e, 0x69, 0xa0, 0x9e, 0xe0, 0x0f, 0xb0, 0x10, 0x47, 0x43, 0xbb, 0x2a, 0xe2, 0x39, + 0x93, 0x6a, 0x5c, 0x06 + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *ds = NULL, *mx = NULL, + *rrsig = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnssecResult result; + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "example.com."); + assert_se(dnskey); + + dnskey->dnskey.flags = 257; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_ED25519; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + + ds = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "example.com."); + assert_se(ds); + + ds->ds.key_tag = 35217; + ds->ds.algorithm = DNSSEC_ALGORITHM_ED25519; + ds->ds.digest_type = DNSSEC_DIGEST_SHA256; + ds->ds.digest_size = sizeof(ds_fprint); + ds->ds.digest = memdup(ds_fprint, ds->ds.digest_size); + assert_se(ds->ds.digest); + + log_info("DS: %s", strna(dns_resource_record_to_string(ds))); + + mx = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_MX, "example.com."); + assert_se(mx); + + mx->mx.priority = 10; + mx->mx.exchange = strdup("mail.example.com."); + assert_se(mx->mx.exchange); + + log_info("MX: %s", strna(dns_resource_record_to_string(mx))); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "example.com."); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_MX; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_ED25519; + rrsig->rrsig.labels = 2; + rrsig->rrsig.original_ttl = 3600; + rrsig->rrsig.expiration = 1440021600; + rrsig->rrsig.inception = 1438207200; + rrsig->rrsig.key_tag = 35217; + rrsig->rrsig.signer = strdup("example.com."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig))); + + assert_se(dnssec_key_match_rrsig(mx->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0); + + answer = dns_answer_new(1); + assert_se(answer); + assert_se(dns_answer_add(answer, mx, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + + assert_se(dnssec_verify_rrset(answer, mx->key, rrsig, dnskey, + rrsig->rrsig.inception * USEC_PER_SEC, &result) >= 0); +#if PREFER_OPENSSL || GCRYPT_VERSION_NUMBER >= 0x010600 + assert_se(result == DNSSEC_VALIDATED); +#else + assert_se(result == DNSSEC_UNSUPPORTED_ALGORITHM); +#endif +} + +TEST(dnssec_verify_rfc6605_example1) { + static const uint8_t signature_blob[] = { + 0xab, 0x1e, 0xb0, 0x2d, 0x8a, 0xa6, 0x87, 0xe9, 0x7d, 0xa0, 0x22, 0x93, 0x37, 0xaa, 0x88, 0x73, + 0xe6, 0xf0, 0xeb, 0x26, 0xbe, 0x28, 0x9f, 0x28, 0x33, 0x3d, 0x18, 0x3f, 0x5d, 0x3b, 0x7a, 0x95, + 0xc0, 0xc8, 0x69, 0xad, 0xfb, 0x74, 0x8d, 0xae, 0xe3, 0xc5, 0x28, 0x6e, 0xed, 0x66, 0x82, 0xc1, + 0x2e, 0x55, 0x33, 0x18, 0x6b, 0xac, 0xed, 0x9c, 0x26, 0xc1, 0x67, 0xa9, 0xeb, 0xae, 0x95, 0x0b, + }; + + static const uint8_t ds_fprint[] = { + 0x6f, 0x87, 0x3c, 0x73, 0x57, 0xde, 0xd9, 0xee, 0xf8, 0xef, 0xbd, 0x76, 0xed, 0xbd, 0xbb, 0xd7, + 0x5e, 0x7a, 0xe7, 0xa6, 0x9d, 0xeb, 0x6e, 0x7a, 0x7f, 0x8d, 0xb8, 0xeb, 0x6e, 0x5b, 0x7f, 0x97, + 0x35, 0x7b, 0x6e, 0xfb, 0xd1, 0xc7, 0xba, 0x77, 0xa7, 0xb7, 0xed, 0xd7, 0xfa, 0xd5, 0xdd, 0x7b, + }; + + static const uint8_t dnskey_blob[] = { + 0x1a, 0x88, 0xc8, 0x86, 0x15, 0xd4, 0x37, 0xfb, 0xb8, 0xbf, 0x9e, 0x19, 0x42, 0xa1, 0x92, 0x9f, + 0x28, 0x56, 0x27, 0x06, 0xae, 0x6c, 0x2b, 0xd3, 0x99, 0xe7, 0xb1, 0xbf, 0xb6, 0xd1, 0xe9, 0xe7, + 0x5b, 0x92, 0xb4, 0xaa, 0x42, 0x91, 0x7a, 0xe1, 0xc6, 0x1b, 0x70, 0x1e, 0xf0, 0x35, 0xc3, 0xfe, + 0x7b, 0xe3, 0x00, 0x9c, 0xba, 0xfe, 0x5a, 0x2f, 0x71, 0x31, 0x6c, 0x90, 0x2d, 0xcf, 0x0d, 0x00, + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *ds = NULL, *a = NULL, + *rrsig = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnssecResult result; + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "example.net."); + assert_se(dnskey); + + dnskey->dnskey.flags = 257; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_ECDSAP256SHA256; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + + ds = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "example.net."); + assert_se(ds); + + ds->ds.key_tag = 55648; + ds->ds.algorithm = DNSSEC_ALGORITHM_ECDSAP256SHA256; + ds->ds.digest_type = DNSSEC_DIGEST_SHA256; + ds->ds.digest_size = sizeof(ds_fprint); + ds->ds.digest = memdup(ds_fprint, ds->ds.digest_size); + assert_se(ds->ds.digest); + + log_info("DS: %s", strna(dns_resource_record_to_string(ds))); + + a = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_A, "www.example.net"); + assert_se(a); + + a->a.in_addr.s_addr = inet_addr("192.0.2.1"); + + log_info("A: %s", strna(dns_resource_record_to_string(a))); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "www.example.net."); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_A; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_ECDSAP256SHA256; + rrsig->rrsig.labels = 3; + rrsig->rrsig.expiration = 1284026679; + rrsig->rrsig.inception = 1281607479; + rrsig->rrsig.key_tag = 55648; + rrsig->rrsig.original_ttl = 3600; + rrsig->rrsig.signer = strdup("example.net."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig))); + + assert_se(dnssec_key_match_rrsig(a->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0); + + answer = dns_answer_new(1); + assert_se(answer); + assert_se(dns_answer_add(answer, a, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + + assert_se(dnssec_verify_rrset(answer, a->key, rrsig, dnskey, + rrsig->rrsig.inception * USEC_PER_SEC, &result) >= 0); + assert_se(result == DNSSEC_VALIDATED); +} + +TEST(dnssec_verify_rfc6605_example2) { + static const uint8_t signature_blob[] = { + 0xfc, 0xbe, 0x61, 0x0c, 0xa2, 0x2f, 0x18, 0x3c, 0x88, 0xd5, 0xf7, 0x00, 0x45, 0x7d, 0xf3, 0xeb, + 0x9a, 0xab, 0x98, 0xfb, 0x15, 0xcf, 0xbd, 0xd0, 0x0f, 0x53, 0x2b, 0xe4, 0x21, 0x2a, 0x3a, 0x22, + 0xcf, 0xf7, 0x98, 0x71, 0x42, 0x8b, 0xae, 0xae, 0x81, 0x82, 0x79, 0x93, 0xaf, 0xcc, 0x56, 0xb1, + 0xb1, 0x3f, 0x06, 0x96, 0xbe, 0xf8, 0x85, 0xb6, 0xaf, 0x44, 0xa6, 0xb2, 0x24, 0xdb, 0xb2, 0x74, + 0x2b, 0xb3, 0x59, 0x34, 0x92, 0x3d, 0xdc, 0xfb, 0xc2, 0x7a, 0x97, 0x2f, 0x96, 0xdd, 0x70, 0x9c, + 0xee, 0xb1, 0xd9, 0xc8, 0xd1, 0x14, 0x8c, 0x44, 0xec, 0x71, 0xc0, 0x68, 0xa9, 0x59, 0xc2, 0x66, + + }; + + static const uint8_t ds_fprint[] = { + 0xef, 0x67, 0x7b, 0x6f, 0xad, 0xbd, 0xef, 0xa7, 0x1e, 0xd3, 0xae, 0x37, 0xf1, 0xef, 0x5c, 0xd1, + 0xb7, 0xf7, 0xd7, 0xdd, 0x35, 0xdd, 0xc7, 0xfc, 0xd3, 0x57, 0xf4, 0xf5, 0xe7, 0x1c, 0xf3, 0x86, + 0xfc, 0x77, 0xb7, 0xbd, 0xe3, 0xde, 0x5f, 0xdb, 0xb7, 0xb7, 0xd3, 0x97, 0x3a, 0x6b, 0xd6, 0xf4, + 0xe7, 0xad, 0xda, 0xf5, 0xbe, 0x5f, 0xe1, 0xdd, 0xbc, 0xf3, 0x8d, 0x39, 0x73, 0x7d, 0x34, 0xf1, + 0xaf, 0x78, 0xe9, 0xd7, 0xfd, 0xf3, 0x77, 0x7a, + }; + + static const uint8_t dnskey_blob[] = { + 0xc4, 0xa6, 0x1a, 0x36, 0x15, 0x9d, 0x18, 0xe7, 0xc9, 0xfa, 0x73, 0xeb, 0x2f, 0xcf, 0xda, 0xae, + 0x4c, 0x1f, 0xd8, 0x46, 0x37, 0x30, 0x32, 0x7e, 0x48, 0x4a, 0xca, 0x8a, 0xf0, 0x55, 0x4a, 0xe9, + 0xb5, 0xc3, 0xf7, 0xa0, 0xb1, 0x7b, 0xd2, 0x00, 0x3b, 0x4d, 0x26, 0x1c, 0x9e, 0x9b, 0x94, 0x42, + 0x3a, 0x98, 0x10, 0xe8, 0xaf, 0x17, 0xd4, 0x34, 0x52, 0x12, 0x4a, 0xdb, 0x61, 0x0f, 0x8e, 0x07, + 0xeb, 0xfc, 0xfe, 0xe5, 0xf8, 0xe4, 0xd0, 0x70, 0x63, 0xca, 0xe9, 0xeb, 0x91, 0x7a, 0x1a, 0x5b, + 0xab, 0xf0, 0x8f, 0xe6, 0x95, 0x53, 0x60, 0x17, 0xa5, 0xbf, 0xa9, 0x32, 0x37, 0xee, 0x6e, 0x34, + }; + + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *dnskey = NULL, *ds = NULL, *a = NULL, + *rrsig = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnssecResult result; + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "example.net."); + assert_se(dnskey); + + dnskey->dnskey.flags = 257; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_ECDSAP384SHA384; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + + ds = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, "example.net."); + assert_se(ds); + + ds->ds.key_tag = 10771; + ds->ds.algorithm = DNSSEC_ALGORITHM_ECDSAP384SHA384; + ds->ds.digest_type = DNSSEC_DIGEST_SHA384; + ds->ds.digest_size = sizeof(ds_fprint); + ds->ds.digest = memdup(ds_fprint, ds->ds.digest_size); + assert_se(ds->ds.digest); + + log_info("DS: %s", strna(dns_resource_record_to_string(ds))); + + a = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_A, "www.example.net"); + assert_se(a); + + a->a.in_addr.s_addr = inet_addr("192.0.2.1"); + + log_info("A: %s", strna(dns_resource_record_to_string(a))); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "www.example.net."); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_A; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_ECDSAP384SHA384; + rrsig->rrsig.labels = 3; + rrsig->rrsig.expiration = 1284027625; + rrsig->rrsig.inception = 1281608425; + rrsig->rrsig.key_tag = 10771; + rrsig->rrsig.original_ttl = 3600; + rrsig->rrsig.signer = strdup("example.net."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig))); + + assert_se(dnssec_key_match_rrsig(a->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0); + + answer = dns_answer_new(1); + assert_se(answer); + assert_se(dns_answer_add(answer, a, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + + assert_se(dnssec_verify_rrset(answer, a->key, rrsig, dnskey, + rrsig->rrsig.inception * USEC_PER_SEC, &result) >= 0); + assert_se(result == DNSSEC_VALIDATED); +} + +TEST(dnssec_verify_rrset) { + static const uint8_t signature_blob[] = { + 0x7f, 0x79, 0xdd, 0x5e, 0x89, 0x79, 0x18, 0xd0, 0x34, 0x86, 0x8c, 0x72, 0x77, 0x75, 0x48, 0x4d, + 0xc3, 0x7d, 0x38, 0x04, 0xab, 0xcd, 0x9e, 0x4c, 0x82, 0xb0, 0x92, 0xca, 0xe9, 0x66, 0xe9, 0x6e, + 0x47, 0xc7, 0x68, 0x8c, 0x94, 0xf6, 0x69, 0xcb, 0x75, 0x94, 0xe6, 0x30, 0xa6, 0xfb, 0x68, 0x64, + 0x96, 0x1a, 0x84, 0xe1, 0xdc, 0x16, 0x4c, 0x83, 0x6c, 0x44, 0xf2, 0x74, 0x4d, 0x74, 0x79, 0x8f, + 0xf3, 0xf4, 0x63, 0x0d, 0xef, 0x5a, 0xe7, 0xe2, 0xfd, 0xf2, 0x2b, 0x38, 0x7c, 0x28, 0x96, 0x9d, + 0xb6, 0xcd, 0x5c, 0x3b, 0x57, 0xe2, 0x24, 0x78, 0x65, 0xd0, 0x9e, 0x77, 0x83, 0x09, 0x6c, 0xff, + 0x3d, 0x52, 0x3f, 0x6e, 0xd1, 0xed, 0x2e, 0xf9, 0xee, 0x8e, 0xa6, 0xbe, 0x9a, 0xa8, 0x87, 0x76, + 0xd8, 0x77, 0xcc, 0x96, 0xa0, 0x98, 0xa1, 0xd1, 0x68, 0x09, 0x43, 0xcf, 0x56, 0xd9, 0xd1, 0x66, + }; + + static const uint8_t dnskey_blob[] = { + 0x03, 0x01, 0x00, 0x01, 0x9b, 0x49, 0x9b, 0xc1, 0xf9, 0x9a, 0xe0, 0x4e, 0xcf, 0xcb, 0x14, 0x45, + 0x2e, 0xc9, 0xf9, 0x74, 0xa7, 0x18, 0xb5, 0xf3, 0xde, 0x39, 0x49, 0xdf, 0x63, 0x33, 0x97, 0x52, + 0xe0, 0x8e, 0xac, 0x50, 0x30, 0x8e, 0x09, 0xd5, 0x24, 0x3d, 0x26, 0xa4, 0x49, 0x37, 0x2b, 0xb0, + 0x6b, 0x1b, 0xdf, 0xde, 0x85, 0x83, 0xcb, 0x22, 0x4e, 0x60, 0x0a, 0x91, 0x1a, 0x1f, 0xc5, 0x40, + 0xb1, 0xc3, 0x15, 0xc1, 0x54, 0x77, 0x86, 0x65, 0x53, 0xec, 0x10, 0x90, 0x0c, 0x91, 0x00, 0x5e, + 0x15, 0xdc, 0x08, 0x02, 0x4c, 0x8c, 0x0d, 0xc0, 0xac, 0x6e, 0xc4, 0x3e, 0x1b, 0x80, 0x19, 0xe4, + 0xf7, 0x5f, 0x77, 0x51, 0x06, 0x87, 0x61, 0xde, 0xa2, 0x18, 0x0f, 0x40, 0x8b, 0x79, 0x72, 0xfa, + 0x8d, 0x1a, 0x44, 0x47, 0x0d, 0x8e, 0x3a, 0x2d, 0xc7, 0x39, 0xbf, 0x56, 0x28, 0x97, 0xd9, 0x20, + 0x4f, 0x00, 0x51, 0x3b, + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *a = NULL, *rrsig = NULL, *dnskey = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnssecResult result; + + a = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_A, "nAsA.gov"); + assert_se(a); + + a->a.in_addr.s_addr = inet_addr("52.0.14.116"); + + log_info("A: %s", strna(dns_resource_record_to_string(a))); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "NaSa.GOV."); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_A; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_RSASHA256; + rrsig->rrsig.labels = 2; + rrsig->rrsig.original_ttl = 600; + rrsig->rrsig.expiration = 0x5683135c; + rrsig->rrsig.inception = 0x565b7da8; + rrsig->rrsig.key_tag = 63876; + rrsig->rrsig.signer = strdup("Nasa.Gov."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig))); + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "nASA.gOV"); + assert_se(dnskey); + + dnskey->dnskey.flags = 256; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_RSASHA256; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + log_info("DNSKEY keytag: %u", dnssec_keytag(dnskey, false)); + + assert_se(dnssec_key_match_rrsig(a->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0); + + answer = dns_answer_new(1); + assert_se(answer); + assert_se(dns_answer_add(answer, a, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + + /* Validate the RR as it if was 2015-12-2 today */ + assert_se(dnssec_verify_rrset(answer, a->key, rrsig, dnskey, 1449092754*USEC_PER_SEC, &result) >= 0); + assert_se(result == DNSSEC_VALIDATED); +} + +TEST(dnssec_verify_rrset2) { + static const uint8_t signature_blob[] = { + 0x48, 0x45, 0xc8, 0x8b, 0xc0, 0x14, 0x92, 0xf5, 0x15, 0xc6, 0x84, 0x9d, 0x2f, 0xe3, 0x32, 0x11, + 0x7d, 0xf1, 0xe6, 0x87, 0xb9, 0x42, 0xd3, 0x8b, 0x9e, 0xaf, 0x92, 0x31, 0x0a, 0x53, 0xad, 0x8b, + 0xa7, 0x5c, 0x83, 0x39, 0x8c, 0x28, 0xac, 0xce, 0x6e, 0x9c, 0x18, 0xe3, 0x31, 0x16, 0x6e, 0xca, + 0x38, 0x31, 0xaf, 0xd9, 0x94, 0xf1, 0x84, 0xb1, 0xdf, 0x5a, 0xc2, 0x73, 0x22, 0xf6, 0xcb, 0xa2, + 0xe7, 0x8c, 0x77, 0x0c, 0x74, 0x2f, 0xc2, 0x13, 0xb0, 0x93, 0x51, 0xa9, 0x4f, 0xae, 0x0a, 0xda, + 0x45, 0xcc, 0xfd, 0x43, 0x99, 0x36, 0x9a, 0x0d, 0x21, 0xe0, 0xeb, 0x30, 0x65, 0xd4, 0xa0, 0x27, + 0x37, 0x3b, 0xe4, 0xc1, 0xc5, 0xa1, 0x2a, 0xd1, 0x76, 0xc4, 0x7e, 0x64, 0x0e, 0x5a, 0xa6, 0x50, + 0x24, 0xd5, 0x2c, 0xcc, 0x6d, 0xe5, 0x37, 0xea, 0xbd, 0x09, 0x34, 0xed, 0x24, 0x06, 0xa1, 0x22, + }; + + static const uint8_t dnskey_blob[] = { + 0x03, 0x01, 0x00, 0x01, 0xc3, 0x7f, 0x1d, 0xd1, 0x1c, 0x97, 0xb1, 0x13, 0x34, 0x3a, 0x9a, 0xea, + 0xee, 0xd9, 0x5a, 0x11, 0x1b, 0x17, 0xc7, 0xe3, 0xd4, 0xda, 0x20, 0xbc, 0x5d, 0xba, 0x74, 0xe3, + 0x37, 0x99, 0xec, 0x25, 0xce, 0x93, 0x7f, 0xbd, 0x22, 0x73, 0x7e, 0x14, 0x71, 0xe0, 0x60, 0x07, + 0xd4, 0x39, 0x8b, 0x5e, 0xe9, 0xba, 0x25, 0xe8, 0x49, 0xe9, 0x34, 0xef, 0xfe, 0x04, 0x5c, 0xa5, + 0x27, 0xcd, 0xa9, 0xda, 0x70, 0x05, 0x21, 0xab, 0x15, 0x82, 0x24, 0xc3, 0x94, 0xf5, 0xd7, 0xb7, + 0xc4, 0x66, 0xcb, 0x32, 0x6e, 0x60, 0x2b, 0x55, 0x59, 0x28, 0x89, 0x8a, 0x72, 0xde, 0x88, 0x56, + 0x27, 0x95, 0xd9, 0xac, 0x88, 0x4f, 0x65, 0x2b, 0x68, 0xfc, 0xe6, 0x41, 0xc1, 0x1b, 0xef, 0x4e, + 0xd6, 0xc2, 0x0f, 0x64, 0x88, 0x95, 0x5e, 0xdd, 0x3a, 0x02, 0x07, 0x50, 0xa9, 0xda, 0xa4, 0x49, + 0x74, 0x62, 0xfe, 0xd7, + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *nsec = NULL, *rrsig = NULL, *dnskey = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnssecResult result; + + nsec = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_NSEC, "nasa.gov"); + assert_se(nsec); + + nsec->nsec.next_domain_name = strdup("3D-Printing.nasa.gov"); + assert_se(nsec->nsec.next_domain_name); + + nsec->nsec.types = bitmap_new(); + assert_se(nsec->nsec.types); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_A) >= 0); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_NS) >= 0); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_SOA) >= 0); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_MX) >= 0); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_TXT) >= 0); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_RRSIG) >= 0); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_NSEC) >= 0); + assert_se(bitmap_set(nsec->nsec.types, DNS_TYPE_DNSKEY) >= 0); + assert_se(bitmap_set(nsec->nsec.types, 65534) >= 0); + + log_info("NSEC: %s", strna(dns_resource_record_to_string(nsec))); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "NaSa.GOV."); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_NSEC; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_RSASHA256; + rrsig->rrsig.labels = 2; + rrsig->rrsig.original_ttl = 300; + rrsig->rrsig.expiration = 0x5689002f; + rrsig->rrsig.inception = 0x56617230; + rrsig->rrsig.key_tag = 30390; + rrsig->rrsig.signer = strdup("Nasa.Gov."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig))); + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "nASA.gOV"); + assert_se(dnskey); + + dnskey->dnskey.flags = 256; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_RSASHA256; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + log_info("DNSKEY keytag: %u", dnssec_keytag(dnskey, false)); + + assert_se(dnssec_key_match_rrsig(nsec->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0); + + answer = dns_answer_new(1); + assert_se(answer); + assert_se(dns_answer_add(answer, nsec, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + + /* Validate the RR as it if was 2015-12-11 today */ + assert_se(dnssec_verify_rrset(answer, nsec->key, rrsig, dnskey, 1449849318*USEC_PER_SEC, &result) >= 0); + assert_se(result == DNSSEC_VALIDATED); +} + +TEST(dnssec_verify_rrset3) { + static const uint8_t signature_blob[] = { + 0x41, 0x09, 0x08, 0x67, 0x51, 0x6d, 0x02, 0xf2, 0x17, 0x1e, 0x61, 0x03, 0xc6, 0x80, 0x7a, 0x82, + 0x8f, 0x6c, 0x8c, 0x4c, 0x68, 0x6f, 0x1c, 0xaa, 0x4a, 0xe0, 0x9b, 0x72, 0xdf, 0x7f, 0x15, 0xfa, + 0x2b, 0xc5, 0x63, 0x6f, 0x52, 0xa2, 0x60, 0x59, 0x24, 0xb6, 0xc3, 0x43, 0x3d, 0x47, 0x38, 0xd8, + 0x0c, 0xcc, 0x6c, 0x10, 0x49, 0x92, 0x97, 0x6c, 0x7d, 0x32, 0xc2, 0x62, 0x83, 0x34, 0x96, 0xdf, + 0xbd, 0xf9, 0xcc, 0xcf, 0xd9, 0x4d, 0x8b, 0x8a, 0xa9, 0x3c, 0x1f, 0x89, 0xc4, 0xad, 0xd5, 0xbb, + 0x74, 0xf8, 0xee, 0x60, 0x54, 0x7a, 0xec, 0x36, 0x45, 0xf2, 0xec, 0xb9, 0x73, 0x66, 0xae, 0x57, + 0x2d, 0xd4, 0x91, 0x02, 0x99, 0xcd, 0xba, 0xbd, 0x6e, 0xfb, 0xa6, 0xf6, 0x34, 0xce, 0x4c, 0x44, + 0x0b, 0xd2, 0x66, 0xdb, 0x4e, 0x5e, 0x00, 0x72, 0x1b, 0xe5, 0x2f, 0x24, 0xd2, 0xc8, 0x72, 0x37, + 0x97, 0x2b, 0xd0, 0xcd, 0xa9, 0x6b, 0x84, 0x32, 0x56, 0x7a, 0x89, 0x6e, 0x3d, 0x8f, 0x03, 0x9a, + 0x9d, 0x6d, 0xf7, 0xe5, 0x13, 0xd7, 0x4b, 0xbc, 0xe2, 0x6c, 0xd1, 0x18, 0x60, 0x0e, 0x1a, 0xe3, + 0xf9, 0xc0, 0x34, 0x4b, 0x1c, 0x82, 0x17, 0x5e, 0xdf, 0x81, 0x32, 0xd7, 0x5b, 0x30, 0x1d, 0xe0, + 0x29, 0x80, 0x6b, 0xb1, 0x69, 0xbf, 0x3f, 0x12, 0x56, 0xb0, 0x80, 0x91, 0x22, 0x1a, 0x31, 0xd5, + 0x5d, 0x3d, 0xdd, 0x70, 0x5e, 0xcb, 0xc7, 0x2d, 0xb8, 0x3e, 0x54, 0x34, 0xd3, 0x50, 0x89, 0x77, + 0x08, 0xc1, 0xf7, 0x11, 0x6e, 0x57, 0xd7, 0x09, 0x94, 0x20, 0x03, 0x38, 0xc3, 0x3a, 0xd3, 0x93, + 0x8f, 0xd0, 0x65, 0xc5, 0xa1, 0xe0, 0x69, 0x2c, 0xf6, 0x0a, 0xce, 0x01, 0xb6, 0x0d, 0x95, 0xa0, + 0x5d, 0x97, 0x94, 0xc3, 0xf1, 0xcd, 0x49, 0xea, 0x20, 0xd3, 0xa9, 0xa6, 0x67, 0x94, 0x64, 0x17 + }; + + static const uint8_t dnskey_blob[] = { + 0x03, 0x01, 0x00, 0x01, 0xbf, 0xdd, 0x24, 0x95, 0x21, 0x70, 0xa8, 0x5b, 0x19, 0xa6, 0x76, 0xd3, + 0x5b, 0x37, 0xcf, 0x59, 0x0d, 0x3c, 0xdb, 0x0c, 0xcf, 0xd6, 0x19, 0x02, 0xc7, 0x8e, 0x56, 0x4d, + 0x14, 0xb7, 0x9d, 0x71, 0xf4, 0xdd, 0x24, 0x36, 0xc8, 0x32, 0x1c, 0x63, 0xf7, 0xc0, 0xfc, 0xe3, + 0x83, 0xa6, 0x22, 0x8b, 0x6a, 0x34, 0x41, 0x72, 0xaa, 0x95, 0x98, 0x06, 0xac, 0x03, 0xec, 0xc3, + 0xa1, 0x6d, 0x8b, 0x1b, 0xfd, 0xa4, 0x05, 0x72, 0xe6, 0xe0, 0xb9, 0x98, 0x07, 0x54, 0x7a, 0xb2, + 0x55, 0x30, 0x96, 0xa3, 0x22, 0x3b, 0xe0, 0x9d, 0x61, 0xf6, 0xdc, 0x31, 0x2b, 0xc9, 0x2c, 0x12, + 0x06, 0x7f, 0x3c, 0x5d, 0x29, 0x76, 0x01, 0x62, 0xe3, 0x41, 0x41, 0x4f, 0xa6, 0x07, 0xfa, 0x2d, + 0x0c, 0x64, 0x88, 0xd1, 0x56, 0x18, 0x4b, 0x2b, 0xc2, 0x19, 0x7e, 0xd0, 0x1a, 0x8c, 0x2d, 0x8d, + 0x06, 0xdf, 0x4d, 0xaf, 0xd9, 0xe3, 0x31, 0x59, 0xbc, 0xc3, 0x36, 0x22, 0xe7, 0x15, 0xf9, 0xb2, + 0x44, 0x8a, 0x33, 0xd7, 0x6c, 0xf1, 0xcc, 0x37, 0x05, 0x69, 0x32, 0x71, 0x76, 0xd8, 0x50, 0x06, + 0xae, 0x27, 0xed, 0x3b, 0xdb, 0x1a, 0x97, 0x9b, 0xa3, 0x3e, 0x40, 0x42, 0x29, 0xaf, 0x75, 0x1c, + 0xff, 0x1d, 0xaf, 0x85, 0x02, 0xb3, 0x2e, 0x99, 0x67, 0x08, 0x13, 0xd5, 0xda, 0x6d, 0x65, 0xb2, + 0x36, 0x6f, 0x2f, 0x64, 0xe0, 0xfa, 0xd3, 0x81, 0x86, 0x6b, 0x41, 0x3e, 0x91, 0xaa, 0x0a, 0xd3, + 0xb2, 0x92, 0xd9, 0x42, 0x36, 0x8a, 0x11, 0x0b, 0x5b, 0xb0, 0xea, 0xad, 0x76, 0xd5, 0xb4, 0x81, + 0x30, 0xca, 0x5c, 0x4f, 0xd9, 0xea, 0xe7, 0x4b, 0x10, 0x0a, 0x09, 0x4b, 0x73, 0x66, 0xed, 0x8e, + 0x84, 0xa2, 0x4f, 0x93, 0x7e, 0x29, 0xdc, 0x6a, 0xbd, 0x12, 0xa1, 0x3d, 0xd2, 0xd6, 0x2a, 0x67, + 0x99, 0x4d, 0xf3, 0x43 + }; + + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *mx1 = NULL, *mx2 = NULL, *mx3 = NULL, *mx4 = NULL, *rrsig = NULL, *dnskey = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + DnssecResult result; + + mx1 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_MX, "kodapan.se"); + assert_se(mx1); + + mx1->mx.priority = 1; + mx1->mx.exchange = strdup("ASPMX.L.GOOGLE.COM"); + assert_se(mx1->mx.exchange); + + log_info("MX: %s", strna(dns_resource_record_to_string(mx1))); + + mx2 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_MX, "kodapan.se"); + assert_se(mx2); + + mx2->mx.priority = 5; + mx2->mx.exchange = strdup("ALT2.ASPMX.L.GOOGLE.COM"); + assert_se(mx2->mx.exchange); + + log_info("MX: %s", strna(dns_resource_record_to_string(mx2))); + + mx3 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_MX, "kodapan.se"); + assert_se(mx3); + + mx3->mx.priority = 10; + mx3->mx.exchange = strdup("ASPMX2.GOOGLEMAIL.COM"); + assert_se(mx3->mx.exchange); + + log_info("MX: %s", strna(dns_resource_record_to_string(mx3))); + + mx4 = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_MX, "kodapan.se"); + assert_se(mx4); + + mx4->mx.priority = 10; + mx4->mx.exchange = strdup("ASPMX3.GOOGLEMAIL.COM"); + assert_se(mx4->mx.exchange); + + log_info("MX: %s", strna(dns_resource_record_to_string(mx4))); + + rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "kodapan.se"); + assert_se(rrsig); + + rrsig->rrsig.type_covered = DNS_TYPE_MX; + rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_RSASHA256; + rrsig->rrsig.labels = 2; + rrsig->rrsig.original_ttl = 900; + rrsig->rrsig.expiration = 0x5e608a84; + rrsig->rrsig.inception = 0x5e4e1584; + rrsig->rrsig.key_tag = 44028; + rrsig->rrsig.signer = strdup("kodapan.se."); + assert_se(rrsig->rrsig.signer); + rrsig->rrsig.signature_size = sizeof(signature_blob); + rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size); + assert_se(rrsig->rrsig.signature); + + log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig))); + + dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "kodapan.se"); + assert_se(dnskey); + + dnskey->dnskey.flags = 256; + dnskey->dnskey.protocol = 3; + dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_RSASHA256; + dnskey->dnskey.key_size = sizeof(dnskey_blob); + dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob)); + assert_se(dnskey->dnskey.key); + + log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey))); + log_info("DNSKEY keytag: %u", dnssec_keytag(dnskey, false)); + + assert_se(dnssec_key_match_rrsig(mx1->key, rrsig) > 0); + assert_se(dnssec_key_match_rrsig(mx2->key, rrsig) > 0); + assert_se(dnssec_key_match_rrsig(mx3->key, rrsig) > 0); + assert_se(dnssec_key_match_rrsig(mx4->key, rrsig) > 0); + assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0); + + answer = dns_answer_new(4); + assert_se(answer); + assert_se(dns_answer_add(answer, mx1, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + assert_se(dns_answer_add(answer, mx2, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + assert_se(dns_answer_add(answer, mx3, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + assert_se(dns_answer_add(answer, mx4, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0); + + /* Validate the RR as it if was 2020-02-24 today */ + assert_se(dnssec_verify_rrset(answer, mx1->key, rrsig, dnskey, 1582534685*USEC_PER_SEC, &result) >= 0); + assert_se(result == DNSSEC_VALIDATED); +} + +TEST(dnssec_nsec3_hash) { + static const uint8_t salt[] = { 0xB0, 0x1D, 0xFA, 0xCE }; + static const uint8_t next_hashed_name[] = { 0x84, 0x10, 0x26, 0x53, 0xc9, 0xfa, 0x4d, 0x85, 0x6c, 0x97, 0x82, 0xe2, 0x8f, 0xdf, 0x2d, 0x5e, 0x87, 0x69, 0xc4, 0x52 }; + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL; + uint8_t h[DNSSEC_HASH_SIZE_MAX]; + _cleanup_free_ char *b = NULL; + int k; + + /* The NSEC3 RR for eurid.eu on 2015-12-14. */ + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_NSEC3, "PJ8S08RR45VIQDAQGE7EN3VHKNROTBMM.eurid.eu."); + assert_se(rr); + + rr->nsec3.algorithm = DNSSEC_DIGEST_SHA1; + rr->nsec3.flags = 1; + rr->nsec3.iterations = 1; + rr->nsec3.salt = memdup(salt, sizeof(salt)); + assert_se(rr->nsec3.salt); + rr->nsec3.salt_size = sizeof(salt); + rr->nsec3.next_hashed_name = memdup(next_hashed_name, sizeof(next_hashed_name)); + assert_se(rr->nsec3.next_hashed_name); + rr->nsec3.next_hashed_name_size = sizeof(next_hashed_name); + + log_info("NSEC3: %s", strna(dns_resource_record_to_string(rr))); + + k = dnssec_nsec3_hash(rr, "eurid.eu", &h); + assert_se(k >= 0); + + b = base32hexmem(h, k, false); + assert_se(b); + assert_se(strcasecmp(b, "PJ8S08RR45VIQDAQGE7EN3VHKNROTBMM") == 0); +} + +DEFINE_TEST_MAIN(LOG_INFO); diff --git a/src/resolve/test-resolve-tables.c b/src/resolve/test-resolve-tables.c new file mode 100644 index 0000000..e4f4e14 --- /dev/null +++ b/src/resolve/test-resolve-tables.c @@ -0,0 +1,54 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "dns-type.h" +#include "resolved-dns-dnssec.h" +#include "resolved-dns-packet.h" +#include "test-tables.h" + +int main(int argc, char **argv) { + uint16_t i; + + test_table(dns_protocol, DNS_PROTOCOL); + test_table(dnssec_result, DNSSEC_RESULT); + test_table(dnssec_verdict, DNSSEC_VERDICT); + + test_table_sparse(dns_rcode, DNS_RCODE); + test_table_sparse(dns_type, DNS_TYPE); + + log_info("/* DNS_TYPE */"); + for (i = 0; i < _DNS_TYPE_MAX; i++) { + const char *s; + + s = dns_type_to_string(i); + assert_se(s == NULL || strlen(s) < _DNS_TYPE_STRING_MAX); + + if (s) + log_info("%-*s %s%s%s%s%s%s%s%s%s", + (int) _DNS_TYPE_STRING_MAX - 1, s, + dns_type_is_pseudo(i) ? "pseudo " : "", + dns_type_is_valid_query(i) ? "valid_query " : "", + dns_type_is_valid_rr(i) ? "is_valid_rr " : "", + dns_type_may_redirect(i) ? "may_redirect " : "", + dns_type_is_dnssec(i) ? "dnssec " : "", + dns_type_is_obsolete(i) ? "obsolete " : "", + dns_type_may_wildcard(i) ? "wildcard " : "", + dns_type_apex_only(i) ? "apex_only " : "", + dns_type_needs_authentication(i) ? "needs_authentication" : ""); + } + + log_info("/* DNS_CLASS */"); + for (i = 0; i < _DNS_CLASS_MAX; i++) { + const char *s; + + s = dns_class_to_string(i); + assert_se(s == NULL || strlen(s) < _DNS_CLASS_STRING_MAX); + + if (s) + log_info("%-*s %s%s", + (int) _DNS_CLASS_STRING_MAX - 1, s, + dns_class_is_pseudo(i) ? "is_pseudo " : "", + dns_class_is_valid_rr(i) ? "is_valid_rr " : ""); + } + + return EXIT_SUCCESS; +} diff --git a/src/resolve/test-resolved-etc-hosts.c b/src/resolve/test-resolved-etc-hosts.c new file mode 100644 index 0000000..c209abf --- /dev/null +++ b/src/resolve/test-resolved-etc-hosts.c @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <malloc.h> +#include <netinet/in.h> +#include <sys/socket.h> + +#include "fd-util.h" +#include "fileio.h" +#include "fs-util.h" +#include "log.h" +#include "resolved-etc-hosts.h" +#include "strv.h" +#include "tests.h" +#include "tmpfile-util.h" + +TEST(parse_etc_hosts_system) { + _cleanup_fclose_ FILE *f = NULL; + + f = fopen("/etc/hosts", "re"); + if (!f) { + assert_se(errno == ENOENT); + return; + } + + _cleanup_(etc_hosts_free) EtcHosts hosts = {}; + assert_se(etc_hosts_parse(&hosts, f) == 0); +} + +#define address_equal_4(_addr, _address) \ + ((_addr)->family == AF_INET && \ + !memcmp(&(_addr)->address.in, &(struct in_addr) { .s_addr = (_address) }, 4)) + +#define address_equal_6(_addr, ...) \ + ((_addr)->family == AF_INET6 && \ + !memcmp(&(_addr)->address.in6, &(struct in6_addr) { .s6_addr = __VA_ARGS__}, 16) ) + +TEST(parse_etc_hosts) { + _cleanup_(unlink_tempfilep) char + t[] = "/tmp/test-resolved-etc-hosts.XXXXXX"; + + int fd; + _cleanup_fclose_ FILE *f = NULL; + + fd = mkostemp_safe(t); + assert_se(fd >= 0); + + f = fdopen(fd, "r+"); + assert_se(f); + fputs("1.2.3.4 some.where\n" + "1.2.3.5 some.where\n" + "1.2.3.6 dash dash-dash.where-dash\n" + "1.2.3.7 bad-dash- -bad-dash -bad-dash.bad-\n" + "1.2.3.8\n" + "1.2.3.9 before.comment # within.comment\n" + "1.2.3.10 before.comment#within.comment2\n" + "1.2.3.11 before.comment# within.comment3\n" + "1.2.3.12 before.comment#\n" + "1.2.3 short.address\n" + "1.2.3.4.5 long.address\n" + "1::2::3 multi.colon\n" + + "::0 some.where some.other\n" + "0.0.0.0 deny.listed\n" + "::5\t\t\t \tsome.where\tsome.other foobar.foo.foo\t\t\t\n" + " \n", f); + assert_se(fflush_and_check(f) >= 0); + rewind(f); + + _cleanup_(etc_hosts_free) EtcHosts hosts = {}; + assert_se(etc_hosts_parse(&hosts, f) == 0); + + EtcHostsItemByName *bn; + assert_se(bn = hashmap_get(hosts.by_name, "some.where")); + assert_se(bn->n_addresses == 3); + assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 3); + assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.4"))); + assert_se(address_equal_4(bn->addresses[1], inet_addr("1.2.3.5"))); + assert_se(address_equal_6(bn->addresses[2], {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5})); + + assert_se(bn = hashmap_get(hosts.by_name, "dash")); + assert_se(bn->n_addresses == 1); + assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1); + assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.6"))); + + assert_se(bn = hashmap_get(hosts.by_name, "dash-dash.where-dash")); + assert_se(bn->n_addresses == 1); + assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1); + assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.6"))); + + /* See https://tools.ietf.org/html/rfc1035#section-2.3.1 */ + FOREACH_STRING(s, "bad-dash-", "-bad-dash", "-bad-dash.bad-") + assert_se(!hashmap_get(hosts.by_name, s)); + + assert_se(bn = hashmap_get(hosts.by_name, "before.comment")); + assert_se(bn->n_addresses == 4); + assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 4); + assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.9"))); + assert_se(address_equal_4(bn->addresses[1], inet_addr("1.2.3.10"))); + assert_se(address_equal_4(bn->addresses[2], inet_addr("1.2.3.11"))); + assert_se(address_equal_4(bn->addresses[3], inet_addr("1.2.3.12"))); + + assert_se(!hashmap_get(hosts.by_name, "within.comment")); + assert_se(!hashmap_get(hosts.by_name, "within.comment2")); + assert_se(!hashmap_get(hosts.by_name, "within.comment3")); + assert_se(!hashmap_get(hosts.by_name, "#")); + + assert_se(!hashmap_get(hosts.by_name, "short.address")); + assert_se(!hashmap_get(hosts.by_name, "long.address")); + assert_se(!hashmap_get(hosts.by_name, "multi.colon")); + assert_se(!set_contains(hosts.no_address, "short.address")); + assert_se(!set_contains(hosts.no_address, "long.address")); + assert_se(!set_contains(hosts.no_address, "multi.colon")); + + assert_se(bn = hashmap_get(hosts.by_name, "some.other")); + assert_se(bn->n_addresses == 1); + assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1); + assert_se(address_equal_6(bn->addresses[0], {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5})); + + assert_se( set_contains(hosts.no_address, "some.where")); + assert_se( set_contains(hosts.no_address, "some.other")); + assert_se( set_contains(hosts.no_address, "deny.listed")); + assert_se(!set_contains(hosts.no_address, "foobar.foo.foo")); +} + +static void test_parse_file_one(const char *fname) { + _cleanup_(etc_hosts_free) EtcHosts hosts = {}; + _cleanup_fclose_ FILE *f = NULL; + + log_info("/* %s(\"%s\") */", __func__, fname); + + assert_se(f = fopen(fname, "re")); + assert_se(etc_hosts_parse(&hosts, f) == 0); +} + +TEST(parse_file) { + for (int i = 1; i < saved_argc; i++) + test_parse_file_one(saved_argv[i]); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/resolve/test-resolved-packet.c b/src/resolve/test-resolved-packet.c new file mode 100644 index 0000000..dd8c969 --- /dev/null +++ b/src/resolve/test-resolved-packet.c @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "log.h" +#include "resolved-dns-packet.h" +#include "tests.h" + +TEST(dns_packet_new) { + size_t i; + _cleanup_(dns_packet_unrefp) DnsPacket *p2 = NULL; + + for (i = 0; i <= DNS_PACKET_SIZE_MAX; i++) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + + assert_se(dns_packet_new(&p, DNS_PROTOCOL_DNS, i, DNS_PACKET_SIZE_MAX) == 0); + + log_debug("dns_packet_new: %zu → %zu", i, p->allocated); + assert_se(p->allocated >= MIN(DNS_PACKET_SIZE_MAX, i)); + + if (i > DNS_PACKET_SIZE_START + 10 && i < DNS_PACKET_SIZE_MAX - 10) + i = MIN(i * 2, DNS_PACKET_SIZE_MAX - 10); + } + + assert_se(dns_packet_new(&p2, DNS_PROTOCOL_DNS, DNS_PACKET_SIZE_MAX + 1, DNS_PACKET_SIZE_MAX) == -EFBIG); +} + +DEFINE_TEST_MAIN(LOG_DEBUG); diff --git a/src/resolve/test-resolved-stream.c b/src/resolve/test-resolved-stream.c new file mode 100644 index 0000000..96395ad --- /dev/null +++ b/src/resolve/test-resolved-stream.c @@ -0,0 +1,395 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <arpa/inet.h> +#include <fcntl.h> +#include <net/if.h> +#include <net/if_arp.h> +#include <pthread.h> +#include <signal.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/prctl.h> +#include <sys/socket.h> +#include <sys/wait.h> +#include <unistd.h> + +#include "fd-util.h" +#include "log.h" +#include "macro.h" +#include "path-util.h" +#include "process-util.h" +#include "resolved-dns-packet.h" +#include "resolved-dns-question.h" +#include "resolved-dns-rr.h" +#if ENABLE_DNS_OVER_TLS +#include "resolved-dnstls.h" +#endif +#include "resolved-dns-server.h" +#include "resolved-dns-stream.h" +#include "resolved-manager.h" +#include "sd-event.h" +#include "sparse-endian.h" +#include "tests.h" + +static union sockaddr_union server_address; + +/* Bytes of the questions & answers used in the test, including TCP DNS 2-byte length prefix */ +static const uint8_t QUESTION_A[] = { + 0x00, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x01, 0x00, 0x01 +}; +static const uint8_t QUESTION_AAAA[] = { + 0x00, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x1C, 0x00, 0x01 +}; +static const uint8_t ANSWER_A[] = { + 0x00, 0x2D, 0x00, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x01, 0x00, 0x01, 0xC0, + 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x52, 0x8D, 0x00, 0x04, 0x5D, 0xB8, 0xD8, 0x22, +}; +static const uint8_t ANSWER_AAAA[] = { + 0x00, 0x39, 0x00, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x07, 'e', + 'x' , 'a' , 'm' , 'p' , 'l' , 'e' , 0x03, 'c' , 'o' , 'm' , 0x00, 0x00, 0x1C, 0x00, 0x01, 0xC0, + 0x0C, 0x00, 0x1C, 0x00, 0x01, 0x00, 0x00, 0x54, 0x4B, 0x00, 0x10, 0x26, 0x06, 0x28, 0x00, 0x02, + 0x20, 0x00, 0x01, 0x02, 0x48, 0x18, 0x93, 0x25, 0xC8, 0x19, 0x46, +}; + +/** + * A mock TCP DNS server that asserts certain questions are received + * and replies with the same answer every time. + */ +static void receive_and_check_question(int fd, const uint8_t *expected_question, + size_t question_size) { + uint8_t *actual_question; + size_t n_read = 0; + + actual_question = newa(uint8_t, question_size); + while (n_read < question_size) { + ssize_t r = read(fd, actual_question + n_read, question_size - n_read); + assert_se(r >= 0); + n_read += (size_t)r; + } + assert_se(n_read == question_size); + + assert_se(memcmp(expected_question, actual_question, question_size) == 0); +} + +static void send_answer(int fd, const uint8_t *answer, size_t answer_size) { + assert_se(write(fd, answer, answer_size) == (ssize_t)answer_size); +} + +/* Sends two answers together in a single write operation, + * so they hopefully end up in a single TCP packet / TLS record */ +static void send_answers_together(int fd, + const uint8_t *answer1, size_t answer1_size, + const uint8_t *answer2, size_t answer2_size) { + uint8_t *answer; + size_t answer_size = answer1_size + answer2_size; + + answer = newa(uint8_t, answer_size); + memcpy(answer, answer1, answer1_size); + memcpy(answer + answer1_size, answer2, answer2_size); + assert_se(write(fd, answer, answer_size) == (ssize_t)answer_size); +} + +static void server_handle(int fd) { + receive_and_check_question(fd, QUESTION_A, sizeof(QUESTION_A)); + send_answer(fd, ANSWER_A, sizeof(ANSWER_A)); + + receive_and_check_question(fd, QUESTION_AAAA, sizeof(QUESTION_AAAA)); + send_answer(fd, ANSWER_AAAA, sizeof(ANSWER_AAAA)); + + receive_and_check_question(fd, QUESTION_A, sizeof(QUESTION_A)); + receive_and_check_question(fd, QUESTION_AAAA, sizeof(QUESTION_AAAA)); + send_answers_together(fd, ANSWER_A, sizeof(ANSWER_A), + ANSWER_AAAA, sizeof(ANSWER_AAAA)); +} + +static void *tcp_dns_server(void *p) { + _cleanup_close_ int bindfd = -1, acceptfd = -1; + + assert_se((bindfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0)) >= 0); + assert_se(setsockopt(bindfd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int)) >= 0); + assert_se(bind(bindfd, &server_address.sa, SOCKADDR_LEN(server_address)) >= 0); + assert_se(listen(bindfd, 1) >= 0); + assert_se((acceptfd = accept(bindfd, NULL, NULL)) >= 0); + server_handle(acceptfd); + return NULL; +} + +#if ENABLE_DNS_OVER_TLS +/* + * Spawns a DNS TLS server using the command line "openssl s_server" tool. + */ +static void *tls_dns_server(void *p) { + pid_t openssl_pid; + int r; + _cleanup_close_ int fd_server = -1, fd_tls = -1; + _cleanup_free_ char *cert_path = NULL, *key_path = NULL; + _cleanup_free_ char *bind_str = NULL; + + assert_se(get_testdata_dir("test-resolve/selfsigned.cert", &cert_path) >= 0); + assert_se(get_testdata_dir("test-resolve/selfsigned.key", &key_path) >= 0); + + assert_se(asprintf(&bind_str, "%s:%d", + IN_ADDR_TO_STRING(server_address.in.sin_family, + sockaddr_in_addr(&server_address.sa)), + be16toh(server_address.in.sin_port)) >= 0); + + /* We will hook one of the socketpair ends to OpenSSL's TLS server + * stdin/stdout, so we will be able to read and write plaintext + * from the other end's file descriptor like an usual TCP server */ + { + int fd[2]; + assert_se(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) >= 0); + fd_server = fd[0]; + fd_tls = fd[1]; + } + + r = safe_fork_full("(test-resolved-stream-tls-openssl)", (int[]) { fd_server, fd_tls }, 2, + FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG|FORK_LOG|FORK_REOPEN_LOG, &openssl_pid); + assert_se(r >= 0); + if (r == 0) { + /* Child */ + assert_se(dup2(fd_tls, STDIN_FILENO) >= 0); + assert_se(dup2(fd_tls, STDOUT_FILENO) >= 0); + close(TAKE_FD(fd_server)); + close(TAKE_FD(fd_tls)); + + execlp("openssl", "openssl", "s_server", "-accept", bind_str, + "-key", key_path, "-cert", cert_path, + "-quiet", "-naccept", "1", NULL); + log_error("exec failed, is something wrong with the 'openssl' command?"); + _exit(EXIT_FAILURE); + } else { + pthread_mutex_t *server_lock = (pthread_mutex_t *)p; + + server_handle(fd_server); + + /* Once the test is done kill the TLS server to release the port */ + assert_se(pthread_mutex_lock(server_lock) == 0); + assert_se(kill(openssl_pid, SIGTERM) >= 0); + assert_se(waitpid(openssl_pid, NULL, 0) >= 0); + assert_se(pthread_mutex_unlock(server_lock) == 0); + } + + return NULL; +} +#endif + +static const char *TEST_DOMAIN = "example.com"; +static const uint64_t EVENT_TIMEOUT_USEC = 5 * 1000 * 1000; + +static void send_simple_question(DnsStream *stream, uint16_t type) { + _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL; + + assert_se(dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0); + assert_se(question = dns_question_new(1)); + assert_se(key = dns_resource_key_new(DNS_CLASS_IN, type, TEST_DOMAIN)); + assert_se(dns_question_add(question, key, 0) >= 0); + assert_se(dns_packet_append_question(p, question) >= 0); + DNS_PACKET_HEADER(p)->qdcount = htobe16(dns_question_size(question)); + assert_se(dns_stream_write_packet(stream, p) >= 0); +} + +static const size_t MAX_RECEIVED_PACKETS = 2; +static DnsPacket *received_packets[2] = {}; +static size_t n_received_packets = 0; + +static int on_stream_packet(DnsStream *stream, DnsPacket *p) { + assert_se(n_received_packets < MAX_RECEIVED_PACKETS); + assert_se(received_packets[n_received_packets++] = dns_packet_ref(p)); + return 0; +} + +static int on_stream_complete_do_nothing(DnsStream *s, int error) { + return 0; +} + +static void test_dns_stream(bool tls) { + Manager manager = {}; + _cleanup_(dns_stream_unrefp) DnsStream *stream = NULL; + _cleanup_(sd_event_unrefp) sd_event *event = NULL; + _cleanup_close_ int clientfd = -1; + int r; + + void *(*server_entrypoint)(void *); + pthread_t server_thread; + pthread_mutex_t server_lock; + + log_info("test-resolved-stream: Started %s test", tls ? "TLS" : "TCP"); + +#if ENABLE_DNS_OVER_TLS + if (tls) + /* For TLS mode, use DNS_OVER_TLS_OPPORTUNISTIC instead of DNS_OVER_TLS_YES, just to make + * certificate validation more lenient, allowing us to use self-signed certificates. We + * never downgrade, everything we test always goes over TLS */ + manager.dns_over_tls_mode = DNS_OVER_TLS_OPPORTUNISTIC; +#endif + + assert_se(sd_event_new(&event) >= 0); + manager.event = event; + + /* Set up a mock DNS (over TCP or TLS) server */ + server_entrypoint = tcp_dns_server; +#if ENABLE_DNS_OVER_TLS + if (tls) + server_entrypoint = tls_dns_server; +#endif + assert_se(pthread_mutex_init(&server_lock, NULL) == 0); + assert_se(pthread_mutex_lock(&server_lock) == 0); + assert_se(pthread_create(&server_thread, NULL, server_entrypoint, &server_lock) == 0); + + /* Create a socket client and connect to the TCP or TLS server + * The server may not be up immediately, so try to connect a few times before failing */ + assert_se((clientfd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0)) >= 0); + + for (int i = 0; i < 100; i++) { + r = connect(clientfd, &server_address.sa, SOCKADDR_LEN(server_address)); + if (r >= 0) + break; + usleep(EVENT_TIMEOUT_USEC / 100); + } + assert_se(r >= 0); + + /* systemd-resolved uses (and requires) the socket to be in nonblocking mode */ + assert_se(fcntl(clientfd, F_SETFL, O_NONBLOCK) >= 0); + + /* Initialize DNS stream (disabling the default self-destruction + behaviour when no complete callback is set) */ + assert_se(dns_stream_new(&manager, &stream, DNS_STREAM_LOOKUP, DNS_PROTOCOL_DNS, + TAKE_FD(clientfd), NULL, on_stream_packet, on_stream_complete_do_nothing, + DNS_STREAM_DEFAULT_TIMEOUT_USEC) >= 0); +#if ENABLE_DNS_OVER_TLS + if (tls) { + DnsServer server = { + .manager = &manager, + .family = server_address.sa.sa_family, + .address = *sockaddr_in_addr(&server_address.sa), + }; + + assert_se(dnstls_manager_init(&manager) >= 0); + assert_se(dnstls_stream_connect_tls(stream, &server) >= 0); + } +#endif + + /* Test: Question of type A and associated answer */ + log_info("test-resolved-stream: A record"); + send_simple_question(stream, DNS_TYPE_A); + while (n_received_packets != 1) + assert_se(sd_event_run(event, EVENT_TIMEOUT_USEC) >= 1); + assert_se(DNS_PACKET_DATA(received_packets[0])); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[0]), + ANSWER_A + 2, sizeof(ANSWER_A) - 2) == 0); + dns_packet_unref(TAKE_PTR(received_packets[0])); + n_received_packets = 0; + + /* Test: Question of type AAAA and associated answer */ + log_info("test-resolved-stream: AAAA record"); + send_simple_question(stream, DNS_TYPE_AAAA); + while (n_received_packets != 1) + assert_se(sd_event_run(event, EVENT_TIMEOUT_USEC) >= 1); + assert_se(DNS_PACKET_DATA(received_packets[0])); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[0]), + ANSWER_AAAA + 2, sizeof(ANSWER_AAAA) - 2) == 0); + dns_packet_unref(TAKE_PTR(received_packets[0])); + n_received_packets = 0; + + /* Test: Question of type A and AAAA and associated answers + * Both answers are sent back in a single packet or TLS record + * (tests the fix of PR #22132: "Fix DoT timeout on multiple answer records") */ + log_info("test-resolved-stream: A + AAAA record"); + send_simple_question(stream, DNS_TYPE_A); + send_simple_question(stream, DNS_TYPE_AAAA); + + while (n_received_packets != 2) + assert_se(sd_event_run(event, EVENT_TIMEOUT_USEC) >= 1); + assert_se(DNS_PACKET_DATA(received_packets[0])); + assert_se(DNS_PACKET_DATA(received_packets[1])); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[0]), + ANSWER_A + 2, sizeof(ANSWER_A) - 2) == 0); + assert_se(memcmp(DNS_PACKET_DATA(received_packets[1]), + ANSWER_AAAA + 2, sizeof(ANSWER_AAAA) - 2) == 0); + dns_packet_unref(TAKE_PTR(received_packets[0])); + dns_packet_unref(TAKE_PTR(received_packets[1])); + n_received_packets = 0; + +#if ENABLE_DNS_OVER_TLS + if (tls) + dnstls_manager_free(&manager); +#endif + + /* Stop the DNS server */ + assert_se(pthread_mutex_unlock(&server_lock) == 0); + assert_se(pthread_join(server_thread, NULL) == 0); + assert_se(pthread_mutex_destroy(&server_lock) == 0); + + log_info("test-resolved-stream: Finished %s test", tls ? "TLS" : "TCP"); +} + +static void try_isolate_network(void) { + _cleanup_close_ int socket_fd = -1; + int r; + + /* First test if CLONE_NEWUSER/CLONE_NEWNET can actually work for us, i.e. we can open the namespaces + * and then still access the build dir we are run from. We do that in a child process since it's + * nasty if we have to go back from the namespace once we entered it and realized it cannot work. */ + r = safe_fork("(usernstest)", FORK_DEATHSIG|FORK_LOG|FORK_WAIT, NULL); + if (r == 0) { /* child */ + _cleanup_free_ char *rt = NULL, *d = NULL; + + if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { + log_warning_errno(errno, "test-resolved-stream: Can't create user and network ns, running on host: %m"); + _exit(EXIT_FAILURE); + } + + assert_se(get_process_exe(0, &rt) >= 0); + assert_se(path_extract_directory(rt, &d) >= 0); + + if (access(d, F_OK) < 0) { + log_warning_errno(errno, "test-resolved-stream: Can't access /proc/self/exe from user/network ns, running on host: %m"); + _exit(EXIT_FAILURE); + } + + _exit(EXIT_SUCCESS); + } + if (r == -EPROTO) /* EPROTO means nonzero exit code of child, i.e. the tests in the child failed */ + return; + assert_se(r > 0); + + /* Now that we know that the unshare() is safe, let's actually do it */ + assert_se(unshare(CLONE_NEWUSER | CLONE_NEWNET) >= 0); + + /* Bring up the loopback interfaceon the newly created network namespace */ + struct ifreq req = { .ifr_ifindex = 1 }; + assert_se((socket_fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0)) >= 0); + assert_se(ioctl(socket_fd, SIOCGIFNAME, &req) >= 0); + assert_se(ioctl(socket_fd, SIOCGIFFLAGS, &req) >= 0); + assert_se(FLAGS_SET(req.ifr_flags, IFF_LOOPBACK)); + req.ifr_flags |= IFF_UP; + assert_se(ioctl(socket_fd, SIOCSIFFLAGS, &req) >= 0); +} + +int main(int argc, char **argv) { + server_address = (union sockaddr_union) { + .in.sin_family = AF_INET, + .in.sin_port = htobe16(12345), + .in.sin_addr.s_addr = htobe32(INADDR_LOOPBACK) + }; + + test_setup_logging(LOG_DEBUG); + + try_isolate_network(); + + test_dns_stream(false); +#if ENABLE_DNS_OVER_TLS + if (system("openssl version >/dev/null 2>&1") != 0) + return log_tests_skipped("Skipping TLS test since the 'openssl' command does not seem to be available"); + test_dns_stream(true); +#endif + + return 0; +} |