summaryrefslogtreecommitdiffstats
path: root/src/resolve
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:00:47 +0000
commit2cb7e0aaedad73b076ea18c6900b0e86c5760d79 (patch)
treeda68ca54bb79f4080079bf0828acda937593a4e1 /src/resolve
parentInitial commit. (diff)
downloadsystemd-2cb7e0aaedad73b076ea18c6900b0e86c5760d79.tar.xz
systemd-2cb7e0aaedad73b076ea18c6900b0e86c5760d79.zip
Adding upstream version 247.3.upstream/247.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/resolve/RFCs60
-rw-r--r--src/resolve/dns-type.c316
-rw-r--r--src/resolve/dns-type.h144
-rw-r--r--src/resolve/dns_type-to-name.awk11
-rwxr-xr-xsrc/resolve/generate-dns_type-gperf.py24
-rw-r--r--src/resolve/generate-dns_type-list.sed1
-rw-r--r--src/resolve/meson.build235
-rw-r--r--src/resolve/org.freedesktop.resolve1.conf27
-rw-r--r--src/resolve/org.freedesktop.resolve1.policy142
-rw-r--r--src/resolve/org.freedesktop.resolve1.service14
-rw-r--r--src/resolve/resolv.conf19
-rw-r--r--src/resolve/resolvconf-compat.c275
-rw-r--r--src/resolve/resolvconf-compat.h4
-rw-r--r--src/resolve/resolvectl.c3322
-rw-r--r--src/resolve/resolvectl.h29
-rw-r--r--src/resolve/resolved-bus.c2236
-rw-r--r--src/resolve/resolved-bus.h15
-rw-r--r--src/resolve/resolved-conf.c516
-rw-r--r--src/resolve/resolved-conf.h22
-rw-r--r--src/resolve/resolved-def.h34
-rw-r--r--src/resolve/resolved-dns-answer.c771
-rw-r--r--src/resolve/resolved-dns-answer.h129
-rw-r--r--src/resolve/resolved-dns-cache.c1119
-rw-r--r--src/resolve/resolved-dns-cache.h35
-rw-r--r--src/resolve/resolved-dns-dnssec.c2263
-rw-r--r--src/resolve/resolved-dns-dnssec.h81
-rw-r--r--src/resolve/resolved-dns-packet.c2410
-rw-r--r--src/resolve/resolved-dns-packet.h302
-rw-r--r--src/resolve/resolved-dns-query.c1041
-rw-r--r--src/resolve/resolved-dns-query.h130
-rw-r--r--src/resolve/resolved-dns-question.c447
-rw-r--r--src/resolve/resolved-dns-question.h56
-rw-r--r--src/resolve/resolved-dns-rr.c1824
-rw-r--r--src/resolve/resolved-dns-rr.h341
-rw-r--r--src/resolve/resolved-dns-scope.c1436
-rw-r--r--src/resolve/resolved-dns-scope.h113
-rw-r--r--src/resolve/resolved-dns-search-domain.c196
-rw-r--r--src/resolve/resolved-dns-search-domain.h56
-rw-r--r--src/resolve/resolved-dns-server.c953
-rw-r--r--src/resolve/resolved-dns-server.h164
-rw-r--r--src/resolve/resolved-dns-stream.c590
-rw-r--r--src/resolve/resolved-dns-stream.h107
-rw-r--r--src/resolve/resolved-dns-stub.c776
-rw-r--r--src/resolve/resolved-dns-stub.h41
-rw-r--r--src/resolve/resolved-dns-synthesize.c450
-rw-r--r--src/resolve/resolved-dns-synthesize.h12
-rw-r--r--src/resolve/resolved-dns-transaction.c3258
-rw-r--r--src/resolve/resolved-dns-transaction.h181
-rw-r--r--src/resolve/resolved-dns-trust-anchor.c770
-rw-r--r--src/resolve/resolved-dns-trust-anchor.h25
-rw-r--r--src/resolve/resolved-dns-zone.c696
-rw-r--r--src/resolve/resolved-dns-zone.h69
-rw-r--r--src/resolve/resolved-dnssd-bus.c135
-rw-r--r--src/resolve/resolved-dnssd-bus.h11
-rw-r--r--src/resolve/resolved-dnssd-gperf.gperf24
-rw-r--r--src/resolve/resolved-dnssd.c368
-rw-r--r--src/resolve/resolved-dnssd.h61
-rw-r--r--src/resolve/resolved-dnstls-gnutls.c241
-rw-r--r--src/resolve/resolved-dnstls-gnutls.h24
-rw-r--r--src/resolve/resolved-dnstls-openssl.c411
-rw-r--r--src/resolve/resolved-dnstls-openssl.h25
-rw-r--r--src/resolve/resolved-dnstls.h37
-rw-r--r--src/resolve/resolved-etc-hosts.c384
-rw-r--r--src/resolve/resolved-etc-hosts.h25
-rw-r--r--src/resolve/resolved-gperf.gperf32
-rw-r--r--src/resolve/resolved-link-bus.c851
-rw-r--r--src/resolve/resolved-link-bus.h22
-rw-r--r--src/resolve/resolved-link.c1432
-rw-r--r--src/resolve/resolved-link.h113
-rw-r--r--src/resolve/resolved-llmnr.c450
-rw-r--r--src/resolve/resolved-llmnr.h14
-rw-r--r--src/resolve/resolved-manager.c1551
-rw-r--r--src/resolve/resolved-manager.h195
-rw-r--r--src/resolve/resolved-mdns.c482
-rw-r--r--src/resolve/resolved-mdns.h13
-rw-r--r--src/resolve/resolved-resolv-conf.c429
-rw-r--r--src/resolve/resolved-resolv-conf.h23
-rw-r--r--src/resolve/resolved-varlink.c544
-rw-r--r--src/resolve/resolved-varlink.h7
-rw-r--r--src/resolve/resolved.c99
-rw-r--r--src/resolve/resolved.conf.in30
-rw-r--r--src/resolve/test-dns-packet.c120
-rw-r--r--src/resolve/test-dnssec-complex.c223
-rw-r--r--src/resolve/test-dnssec.c623
-rw-r--r--src/resolve/test-resolve-tables.c54
-rw-r--r--src/resolve/test-resolved-etc-hosts.c150
-rw-r--r--src/resolve/test-resolved-packet.c32
87 files changed, 37523 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..1f73347
--- /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_TYPE_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..4370db9
--- /dev/null
+++ b/src/resolve/dns-type.h
@@ -0,0 +1,144 @@
+/* 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 {
+ /* Normal records */
+ 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_HIP = 0x37,
+ DNS_TYPE_NINFO,
+ DNS_TYPE_RKEY,
+ DNS_TYPE_TALINK,
+ DNS_TYPE_CDS,
+ DNS_TYPE_CDNSKEY,
+ DNS_TYPE_OPENPGPKEY,
+
+ DNS_TYPE_SPF = 0x63,
+ DNS_TYPE_NID,
+ DNS_TYPE_L32,
+ DNS_TYPE_L64,
+ DNS_TYPE_LP,
+ DNS_TYPE_EUI48,
+ DNS_TYPE_EUI64,
+
+ 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_TA = 0x8000,
+ DNS_TYPE_DLV,
+
+ _DNS_TYPE_MAX,
+ _DNS_TYPE_INVALID = -1
+};
+
+assert_cc(DNS_TYPE_SSHFP == 44);
+assert_cc(DNS_TYPE_TLSA == 52);
+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 = -1
+};
+
+#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..badb182
--- /dev/null
+++ b/src/resolve/dns_type-to-name.awk
@@ -0,0 +1,11 @@
+BEGIN{
+ print "const char *dns_type_to_string(int type) {\n\tswitch(type) {"
+}
+{
+ printf " case DNS_TYPE_%s: return ", $1;
+ sub(/_/, "-");
+ printf "\"%s\";\n", $1
+}
+END{
+ print " default: return NULL;\n\t}\n}\n"
+}
diff --git a/src/resolve/generate-dns_type-gperf.py b/src/resolve/generate-dns_type-gperf.py
new file mode 100755
index 0000000..d4f7b94
--- /dev/null
+++ b/src/resolve/generate-dns_type-gperf.py
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+
+"""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..b7bc30f
--- /dev/null
+++ b/src/resolve/generate-dns_type-list.sed
@@ -0,0 +1 @@
+s/.* DNS_TYPE_(\w+).*/\1/p
diff --git a/src/resolve/meson.build b/src/resolve/meson.build
new file mode 100644
index 0000000..8e7bad0
--- /dev/null
+++ b/src/resolve/meson.build
@@ -0,0 +1,235 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+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
+ dns-type.c
+'''.split())
+
+dns_type_h = files('dns-type.h')[0]
+
+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-varlink.c
+ resolved-varlink.h
+ resolved.c
+'''.split())
+
+resolvectl_sources = files('''
+ resolvconf-compat.c
+ resolvconf-compat.h
+ resolvectl.c
+ resolvectl.h
+'''.split())
+
+############################################################
+
+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')
+
+dns_type_headers = [dns_type_h]
+foreach item : [['dns_type', dns_type_list_txt, 'dns_type', 'DNS_TYPE_']]
+
+ fname = '@0@-from-name.gperf'.format(item[0])
+ gperf_file = custom_target(
+ fname,
+ input : item[1],
+ output : fname,
+ command : [generate_dns_type_gperf, item[2], item[3], '@INPUT@'],
+ capture : true)
+
+ fname = '@0@-from-name.h'.format(item[0])
+ target1 = custom_target(
+ fname,
+ input : gperf_file,
+ output : fname,
+ command : [gperf,
+ '-L', 'ANSI-C', '-t', '--ignore-case',
+ '-N', 'lookup_@0@'.format(item[2]),
+ '-H', 'hash_@0@_name'.format(item[2]),
+ '-p', '-C',
+ '@INPUT@'],
+ capture : true)
+
+ fname = '@0@-to-name.h'.format(item[0])
+ awkscript = '@0@-to-name.awk'.format(item[0])
+ target2 = custom_target(
+ fname,
+ input : [awkscript, item[1]],
+ output : fname,
+ command : [awk, '-f', '@INPUT0@', '@INPUT1@'],
+ capture : true)
+
+ dns_type_headers += [target1, target2]
+endforeach
+
+resolved_gperf_c = custom_target(
+ 'resolved_gperf.c',
+ input : 'resolved-gperf.gperf',
+ output : 'resolved-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+resolved_dnssd_gperf_c = custom_target(
+ 'resolved_dnssd_gperf.c',
+ input : 'resolved-dnssd-gperf.gperf',
+ output : 'resolved-dnssd-gperf.c',
+ command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+libsystemd_resolve_core = static_library(
+ 'systemd-resolve-core',
+ basic_dns_sources,
+ dns_type_headers,
+ include_directories : includes)
+
+systemd_resolved_sources += [resolved_gperf_c, resolved_dnssd_gperf_c]
+
+systemd_resolved_dependencies = [threads, libgpg_error, libm]
+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)
+
+ resolved_conf = configure_file(
+ input : 'resolved.conf.in',
+ output : 'resolved.conf',
+ configuration : substs)
+ if install_sysconfdir
+ install_data(resolved_conf,
+ install_dir : pkgsysconfdir)
+ endif
+
+ install_data('resolv.conf',
+ install_dir : rootlibexecdir)
+endif
+
+tests += [
+ [['src/resolve/test-resolve-tables.c',
+ dns_type_headers,
+ 'src/shared/test-tables.h'],
+ [libsystemd_resolve_core,
+ libshared],
+ [libgcrypt,
+ libgpg_error,
+ libm],
+ 'ENABLE_RESOLVE'],
+
+ [['src/resolve/test-dns-packet.c',
+ dns_type_headers],
+ [libsystemd_resolve_core,
+ libshared],
+ [libgcrypt,
+ libgpg_error,
+ libm],
+ 'ENABLE_RESOLVE'],
+
+ [['src/resolve/test-resolved-etc-hosts.c',
+ 'src/resolve/resolved-etc-hosts.c',
+ 'src/resolve/resolved-etc-hosts.h'],
+ [libsystemd_resolve_core,
+ libshared],
+ [libgcrypt,
+ libgpg_error,
+ libm],
+ 'ENABLE_RESOLVE'],
+
+ [['src/resolve/test-resolved-packet.c',
+ dns_type_headers],
+ [libsystemd_resolve_core,
+ libshared],
+ [libgcrypt,
+ libgpg_error,
+ libm],
+ 'ENABLE_RESOLVE'],
+
+ [['src/resolve/test-dnssec.c',
+ dns_type_headers],
+ [libsystemd_resolve_core,
+ libshared],
+ [libgcrypt,
+ libgpg_error,
+ libm],
+ 'ENABLE_RESOLVE'],
+
+ [['src/resolve/test-dnssec-complex.c',
+ 'src/resolve/dns-type.c',
+ dns_type_headers],
+ [],
+ [],
+ 'ENABLE_RESOLVE', 'manual'],
+]
diff --git a/src/resolve/org.freedesktop.resolve1.conf b/src/resolve/org.freedesktop.resolve1.conf
new file mode 100644
index 0000000..25b0977
--- /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"
+ "http://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..08615ec
--- /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"
+ "http://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>http://www.freedesktop.org/wiki/Software/systemd</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..5bc936f
--- /dev/null
+++ b/src/resolve/resolvconf-compat.c
@@ -0,0 +1,275 @@
+/* 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. Various options supported by other\n"
+ "implementations are not supported and will cause the invocation to fail: -u,\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;
+
+ /* Everybody else can agree on the existence of -u but we don't support it. */
+ case 'u':
+
+ /* 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("Unhandled option");
+ }
+
+ 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_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..b479335
--- /dev/null
+++ b/src/resolve/resolvectl.c
@@ -0,0 +1,3322 @@
+/* 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 "escape.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "gcrypt-util.h"
+#include "main-func.h"
+#include "missing_network.h"
+#include "netlink-util.h"
+#include "pager.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "resolvconf-compat.h"
+#include "resolvectl.h"
+#include "resolved-def.h"
+#include "resolved-dns-packet.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 "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 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(const char *s) {
+ _cleanup_free_ char *iface = NULL;
+ const char *dot;
+ int ifi;
+
+ assert(s);
+
+ dot = strchr(s, '.');
+ if (dot) {
+ log_debug("Ignoring protocol specifier '%s'.", dot + 1);
+ iface = strndup(s, dot - s);
+
+ } else
+ iface = strdup(s);
+ if (!iface)
+ return log_oom();
+
+ ifi = resolve_interface(NULL, iface);
+ if (ifi < 0) {
+ if (ifi == -ENODEV && arg_ifindex_permissive) {
+ log_debug("Interface '%s' not found, but -f specified, ignoring.", iface);
+ return 0; /* done */
+ }
+
+ return log_error_errno(ifi, "Failed to resolve interface \"%s\": %m", iface);
+ }
+
+ if (arg_ifindex > 0 && arg_ifindex != ifi)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified multiple different interfaces. Refusing.");
+
+ arg_ifindex = ifi;
+ free_and_replace(arg_ifname, iface);
+
+ return 1;
+}
+
+static void print_source(uint64_t flags, usec_t rtt) {
+ char rtt_str[FORMAT_TIMESTAMP_MAX];
+
+ 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" : "");
+
+ assert_se(format_timespan(rtt_str, sizeof(rtt_str), rtt, 100));
+
+ printf(" in %s.%s\n"
+ "%s-- Data is authenticated: %s%s\n",
+ rtt_str, ansi_normal(),
+ ansi_grey(), yes_no(flags & SD_RESOLVED_AUTHENTICATED), ansi_normal());
+}
+
+static void print_ifindex_comment(int printed_so_far, int ifindex) {
+ char ifname[IF_NAMESIZE + 1];
+
+ if (ifindex <= 0)
+ return;
+
+ if (!format_ifname(ifindex, ifname))
+ log_warning_errno(errno, "Failed to resolve interface name for index %i, ignoring: %m", ifindex);
+ else
+ 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(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 log_error_errno(r, "%s: resolve call failed: %s", name, bus_error_message(&error, r));
+
+ 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;
+ _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 log_oom();
+
+ p->refuse_compression = true;
+
+ r = dns_packet_append_blob(p, d, l, NULL);
+ if (r < 0)
+ return log_oom();
+
+ r = dns_packet_read_rr(p, &rr, NULL, NULL);
+ 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 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;
+ unsigned n = 0;
+ uint64_t flags;
+ int r;
+ usec_t ts;
+ bool needs_authentication = false;
+
+ 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);
+
+ 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(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(SYNTHETIC_ERRNO(EINVAL),
+ "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(SYNTHETIC_ERRNO(EINVAL),
+ "Unknown DNS type %s.", 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;
+ char **p;
+ 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;
+ char **p;
+ 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(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 **p, **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 = 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;
+
+ assert(bus);
+
+ 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, bool extended, char **ret) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ _cleanup_free_ char *pretty = NULL;
+ int ifindex, family, r, k;
+ union in_addr_union a;
+ const char *name = NULL;
+ 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 (with_ifindex && ifindex != 0) {
+ /* only show the global ones here */
+ *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 = userdata;
+ int r;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+ assert(l);
+
+ 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, false, extended, &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, false, 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, false, true, 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 = userdata;
+ int r;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+ assert(l);
+
+ 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;
+
+ strv_sort(*l);
+
+ 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;
+ char **i;
+
+ 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", 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 + 1] = "";
+ int r;
+
+ assert(bus);
+ assert(ifindex > 0);
+
+ if (!name) {
+ if (!format_ifname(ifindex, ifname))
+ return log_error_errno(errno, "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));
+
+ (void) 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 = userdata;
+ int r;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+ assert(l);
+
+ 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, true, extended, &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) {
+ assert(m);
+ assert(userdata);
+
+ return read_dns_server_one(m, true, false, 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) {
+ assert(m);
+ assert(userdata);
+
+ return read_dns_server_one(m, true, 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 = userdata;
+ int r;
+
+ assert(bus);
+ assert(member);
+ assert(m);
+ assert(l);
+
+ 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));
+
+ (void) 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_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_allocated = 0, 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_allocated, 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) {
+ char **ifname;
+ bool empty_line = false;
+
+ STRV_FOREACH(ifname, argv + 1) {
+ int ifindex, q;
+
+ ifindex = 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;
+ char **p;
+ 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 = userdata;
+ int r;
+
+ assert(bus);
+
+ 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;
+ char **p;
+ 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 = userdata;
+ int r;
+
+ assert(bus);
+
+ 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 = userdata;
+ int r, b;
+
+ assert(bus);
+
+ 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 = userdata;
+ int r;
+
+ assert(bus);
+
+ 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 = userdata;
+ int r;
+
+ assert(bus);
+
+ 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 = userdata;
+ int r;
+
+ assert(bus);
+
+ 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 = userdata;
+ int r;
+
+ assert(bus);
+
+ 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 = userdata;
+ char **p;
+ int r;
+ bool clear;
+
+ assert(bus);
+
+ 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 = userdata;
+ int r;
+
+ assert(bus);
+
+ 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) {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+ sd_bus *bus = userdata;
+ int r;
+
+ assert(bus);
+
+ if (argc == 1) {
+ _cleanup_free_ char *level = NULL;
+
+ r = sd_bus_get_property_string(
+ bus,
+ "org.freedesktop.resolve1",
+ "/org/freedesktop/LogControl1",
+ "org.freedesktop.LogControl1",
+ "LogLevel",
+ &error,
+ &level);
+ if (r < 0)
+ return log_error_errno(r, "Failed to get log level: %s", bus_error_message(&error, r));
+
+ puts(level);
+
+ } else {
+ assert(argc == 2);
+
+ r = sd_bus_set_property(
+ bus,
+ "org.freedesktop.resolve1",
+ "/org/freedesktop/LogControl1",
+ "org.freedesktop.LogControl1",
+ "LogLevel",
+ &error,
+ "s",
+ argv[1]);
+ if (r < 0)
+ return log_error_errno(r, "Failed to set log level: %s", bus_error_message(&error, r));
+ }
+
+ return 0;
+}
+
+static void help_protocol_types(void) {
+ if (arg_legend)
+ puts("Known protocol types:");
+ puts("dns\nllmnr\nllmnr-ipv4\nllmnr-ipv6\nmdns\nmdns-ipv4\nmdns-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"
+ " 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"
+ " --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"
+ "\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) {
+ log_error("Failed to parse RR record type %s", optarg);
+ return r;
+ }
+ 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) {
+ log_error("Failed to parse RR record class %s", optarg);
+ return r;
+ }
+ arg_class = (uint16_t) r;
+ assert((int) arg_class == r);
+
+ break;
+
+ case ARG_LEGEND:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --legend= argument");
+
+ arg_legend = 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(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --cname= argument.");
+ SET_FLAG(arg_flags, SD_RESOLVED_NO_CNAME, r == 0);
+ break;
+
+ case ARG_SERVICE_ADDRESS:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --service-address= argument.");
+ SET_FLAG(arg_flags, SD_RESOLVED_NO_ADDRESS, r == 0);
+ break;
+
+ case ARG_SERVICE_TXT:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --service-txt= argument.");
+ SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0);
+ break;
+
+ case ARG_SEARCH:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --search argument.");
+ 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("Unhandled option");
+ }
+
+ 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_SERVICE_ADDRESS,
+ ARG_SERVICE_TXT,
+ ARG_RAW,
+ ARG_SEARCH,
+ ARG_NO_PAGER,
+ };
+
+ 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-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 },
+ {}
+ };
+
+ 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 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) {
+ log_error("Failed to parse RR record type %s", optarg);
+ return r;
+ }
+ 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) {
+ log_error("Failed to parse RR record class %s", optarg);
+ return r;
+ }
+ arg_class = (uint16_t) r;
+ assert((int) arg_class == r);
+
+ break;
+
+ case ARG_LEGEND:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --legend= argument");
+
+ arg_legend = 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(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --cname= argument.");
+ SET_FLAG(arg_flags, SD_RESOLVED_NO_CNAME, r == 0);
+ break;
+
+ case ARG_SERVICE_ADDRESS:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --service-address= argument.");
+ SET_FLAG(arg_flags, SD_RESOLVED_NO_ADDRESS, r == 0);
+ break;
+
+ case ARG_SERVICE_TXT:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --service-txt= argument.");
+ SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0);
+ break;
+
+ case ARG_SEARCH:
+ r = parse_boolean(optarg);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse --search argument.");
+ SET_FLAG(arg_flags, SD_RESOLVED_NO_SEARCH, r == 0);
+ break;
+
+ case ARG_NO_PAGER:
+ arg_pager_flags |= PAGER_DISABLE;
+ break;
+
+ case '?':
+ return -EINVAL;
+
+ default:
+ assert_not_reached("Unhandled option");
+ }
+
+ 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 },
+ {}
+ };
+
+ 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("invalid mode");
+ }
+
+ return 0;
+}
+
+static int run(int argc, char **argv) {
+ _cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
+ int r;
+
+ setlocale(LC_ALL, "");
+ log_setup_cli();
+
+ if (streq(program_invocation_short_name, "resolvconf"))
+ r = resolvconf_parse_argv(argc, argv);
+ else if (streq(program_invocation_short_name, "systemd-resolve"))
+ 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 (STR_IN_SET(program_invocation_short_name, "systemd-resolve", "resolvconf"))
+ 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..830c81d
--- /dev/null
+++ b/src/resolve/resolvectl.h
@@ -0,0 +1,29 @@
+/* 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 = -1,
+} ExecutionMode;
+
+extern ExecutionMode arg_mode;
+extern char **arg_set_dns;
+extern char **arg_set_domain;
+extern bool arg_ifindex_permissive;
+
+int ifname_mangle(const char *s);
diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c
new file mode 100644
index 0000000..dca9b88
--- /dev/null
+++ b/src/resolve/resolved-bus.c
@@ -0,0 +1,2236 @@
+/* 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 "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 = userdata;
+
+ assert(t);
+ assert(q);
+
+ 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 int reply_query_state(DnsQuery *q) {
+
+ assert(q);
+ assert(q->bus_request);
+
+ switch (q->state) {
+
+ case DNS_TRANSACTION_NO_SERVERS:
+ return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_NO_NAME_SERVERS, "No appropriate name servers or networks for name found");
+
+ case DNS_TRANSACTION_TIMEOUT:
+ return sd_bus_reply_method_errorf(q->bus_request, SD_BUS_ERROR_TIMEOUT, "Query timed out");
+
+ case DNS_TRANSACTION_ATTEMPTS_MAX_REACHED:
+ return sd_bus_reply_method_errorf(q->bus_request, SD_BUS_ERROR_TIMEOUT, "All attempts to contact name servers or networks failed");
+
+ case DNS_TRANSACTION_INVALID_REPLY:
+ return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_INVALID_REPLY, "Received invalid reply");
+
+ case DNS_TRANSACTION_ERRNO:
+ return sd_bus_reply_method_errnof(q->bus_request, q->answer_errno, "Lookup failed due to system error: %m");
+
+ case DNS_TRANSACTION_ABORTED:
+ return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_ABORTED, "Query aborted");
+
+ case DNS_TRANSACTION_DNSSEC_FAILED:
+ return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_DNSSEC_FAILED, "DNSSEC validation failed: %s",
+ dnssec_result_to_string(q->answer_dnssec_result));
+
+ case DNS_TRANSACTION_NO_TRUST_ANCHOR:
+ return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_NO_TRUST_ANCHOR, "No suitable trust anchor known");
+
+ case DNS_TRANSACTION_RR_TYPE_UNSUPPORTED:
+ return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_RR_TYPE_UNSUPPORTED, "Server does not support requested resource record type");
+
+ case DNS_TRANSACTION_NETWORK_DOWN:
+ return sd_bus_reply_method_errorf(q->bus_request, 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 sd_bus_reply_method_errorf(q->bus_request, _BUS_ERROR_DNS "NXDOMAIN", "'%s' not found", dns_query_string(q));
+
+ case DNS_TRANSACTION_RCODE_FAILURE: {
+ _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+ if (q->answer_rcode == DNS_RCODE_NXDOMAIN)
+ sd_bus_error_setf(&error, _BUS_ERROR_DNS "NXDOMAIN", "'%s' not found", dns_query_string(q));
+ else {
+ const char *rc, *n;
+ char p[DECIMAL_STR_MAX(q->answer_rcode)];
+
+ rc = dns_rcode_to_string(q->answer_rcode);
+ if (!rc) {
+ xsprintf(p, "%i", q->answer_rcode);
+ rc = p;
+ }
+
+ 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(q->bus_request, &error);
+ }
+
+ case DNS_TRANSACTION_NULL:
+ case DNS_TRANSACTION_PENDING:
+ case DNS_TRANSACTION_VALIDATING:
+ case DNS_TRANSACTION_SUCCESS:
+ default:
+ assert_not_reached("Impossible state");
+ }
+}
+
+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 *q) {
+ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ _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(q);
+ if (r == -ELOOP) {
+ r = sd_bus_reply_method_errorf(q->bus_request, 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_RESTARTED) /* This was a cname, and the query was restarted. */
+ 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 = sd_bus_reply_method_errorf(q->bus_request, 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,
+ SD_RESOLVED_FLAGS_MAKE(q->answer_protocol, q->answer_family, dns_query_fully_authenticated(q)));
+ if (r < 0)
+ goto finish;
+
+ r = sd_bus_send(q->manager->bus, reply, NULL);
+
+finish:
+ if (r < 0) {
+ log_error_errno(r, "Failed to send hostname reply: %m");
+ sd_bus_reply_method_errno(q->bus_request, r, NULL);
+ }
+
+ dns_query_free(q);
+}
+
+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 the NO_CNAME flag 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|ok))
+ return sd_bus_error_setf(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));
+ if (r < 0)
+ return r;
+
+ return sd_bus_send(sd_bus_message_get_bus(m), reply, NULL);
+}
+
+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;
+ Manager *m = userdata;
+ const char *hostname;
+ int family, ifindex;
+ uint64_t flags;
+ DnsQuery *q;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ 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_setf(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;
+
+ r = dns_query_new(m, &q, question_utf8, question_idna ?: question_utf8, 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)
+ goto fail;
+
+ r = dns_query_go(q);
+ if (r < 0)
+ goto fail;
+
+ return 1;
+
+fail:
+ dns_query_free(q);
+ return r;
+}
+
+static void bus_method_resolve_address_complete(DnsQuery *q) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = 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(q);
+ if (r == -ELOOP) {
+ r = sd_bus_reply_method_errorf(q->bus_request, 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_RESTARTED) /* This was a cname, and the query was restarted. */
+ 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) {
+ _cleanup_free_ char *ip = NULL;
+
+ (void) in_addr_to_string(q->request_family, &q->request_address, &ip);
+ r = sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_NO_SUCH_RR,
+ "Address '%s' does not have any RR of requested type", strnull(ip));
+ goto finish;
+ }
+
+ r = sd_bus_message_close_container(reply);
+ if (r < 0)
+ goto finish;
+
+ r = sd_bus_message_append(reply, "t", SD_RESOLVED_FLAGS_MAKE(q->answer_protocol, q->answer_family, dns_query_fully_authenticated(q)));
+ if (r < 0)
+ goto finish;
+
+ r = sd_bus_send(q->manager->bus, reply, NULL);
+
+finish:
+ if (r < 0) {
+ log_error_errno(r, "Failed to send address reply: %m");
+ sd_bus_reply_method_errno(q->bus_request, r, NULL);
+ }
+
+ dns_query_free(q);
+}
+
+static int bus_method_resolve_address(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+ _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
+ Manager *m = userdata;
+ union in_addr_union a;
+ int family, ifindex;
+ uint64_t flags;
+ DnsQuery *q;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ 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_setf(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;
+
+ r = dns_query_new(m, &q, question, question, 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)
+ goto fail;
+
+ r = dns_query_go(q);
+ if (r < 0)
+ goto fail;
+
+ return 1;
+
+fail:
+ dns_query_free(q);
+ return r;
+}
+
+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 *q) {
+ _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+ 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(q);
+ if (r == -ELOOP) {
+ r = sd_bus_reply_method_errorf(q->bus_request, 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_RESTARTED) /* This was a cname, and the query was restarted. */
+ 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 = sd_bus_reply_method_errorf(q->bus_request, 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", SD_RESOLVED_FLAGS_MAKE(q->answer_protocol, q->answer_family, dns_query_fully_authenticated(q)));
+ if (r < 0)
+ goto finish;
+
+ r = sd_bus_send(q->manager->bus, reply, NULL);
+
+finish:
+ if (r < 0) {
+ log_error_errno(r, "Failed to send record reply: %m");
+ sd_bus_reply_method_errno(q->bus_request, r, NULL);
+ }
+
+ dns_query_free(q);
+}
+
+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;
+ Manager *m = userdata;
+ uint16_t class, type;
+ const char *name;
+ int r, ifindex;
+ uint64_t flags;
+ DnsQuery *q;
+
+ assert(message);
+ assert(m);
+
+ 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_setf(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_setf(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);
+ if (r < 0)
+ return r;
+
+ r = dns_query_new(m, &q, question, question, ifindex, flags|SD_RESOLVED_NO_SEARCH);
+ if (r < 0)
+ return r;
+
+ /* Let's request that the TTL is fixed up for locally cached entries, after all we return it in the wire format
+ * blob */
+ q->clamp_ttl = true;
+
+ 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)
+ goto fail;
+
+ r = dns_query_go(q);
+ if (r < 0)
+ goto fail;
+
+ return 1;
+
+fail:
+ dns_query_free(q);
+ return r;
+}
+
+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;
+ DnsQuery *aux;
+ 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) {
+ DnsTxtItem *i;
+ 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 *q) {
+ _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;
+ DnsQuestion *question;
+ DnsResourceRecord *rr;
+ unsigned added = 0;
+ DnsQuery *aux;
+ int r;
+
+ assert(q);
+
+ if (q->block_all_complete > 0)
+ 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 */
+ 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 = sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_CNAME_LOOP, "CNAME loop detected, or CNAME resolving disabled on '%s'", dns_query_string(bad));
+ goto finish;
+ }
+
+ 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 = sd_bus_reply_method_errorf(q->bus_request, 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,
+ SD_RESOLVED_FLAGS_MAKE(q->answer_protocol, q->answer_family, dns_query_fully_authenticated(q)));
+ if (r < 0)
+ goto finish;
+
+ r = sd_bus_send(q->manager->bus, reply, NULL);
+
+finish:
+ if (r < 0) {
+ log_error_errno(r, "Failed to send service reply: %m");
+ sd_bus_reply_method_errno(q->bus_request, r, NULL);
+ }
+
+ dns_query_free(q);
+}
+
+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(q);
+ if (r == DNS_QUERY_RESTARTED) /* 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;
+ 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;
+ DnsQuery *aux;
+ 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, 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 */
+
+ dns_query_free(aux);
+ return 0;
+ }
+ if (r < 0)
+ goto fail;
+
+ /* 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)
+ goto fail;
+
+ return 1;
+
+fail:
+ dns_query_free(aux);
+ return r;
+}
+
+static void bus_method_resolve_service_complete(DnsQuery *q) {
+ 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(q);
+ if (r == -ELOOP) {
+ r = sd_bus_reply_method_errorf(q->bus_request, 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_RESTARTED) /* This was a cname, and the query was restarted. */
+ 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 = sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_NO_SUCH_SERVICE, "'%s' does not provide the requested service", dns_query_string(q));
+ goto finish;
+ }
+
+ if (found <= 0) {
+ r = sd_bus_reply_method_errorf(q->bus_request, 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(q);
+ return;
+
+finish:
+ if (r < 0) {
+ log_error_errno(r, "Failed to send service reply: %m");
+ sd_bus_reply_method_errno(q->bus_request, r, NULL);
+ }
+
+ dns_query_free(q);
+}
+
+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;
+ const char *name, *type, *domain;
+ Manager *m = userdata;
+ int family, ifindex;
+ uint64_t flags;
+ DnsQuery *q;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ 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_setf(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_setf(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;
+
+ r = dns_query_new(m, &q, question_utf8, question_idna, 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)
+ goto fail;
+
+ r = dns_query_go(q);
+ if (r < 0)
+ goto fail;
+
+ return 1;
+
+fail:
+ dns_query_free(q);
+ return r;
+}
+
+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 = userdata;
+ DnsServer *s;
+ Link *l;
+ int r;
+
+ assert(reply);
+ assert(m);
+
+ 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 *s, **f = userdata;
+ int r;
+
+ assert(reply);
+ assert(f);
+
+ 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 = userdata;
+ DnsSearchDomain *d;
+ Link *l;
+ int r;
+
+ assert(reply);
+ assert(m);
+
+ 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 = userdata;
+
+ assert(reply);
+ assert(m);
+
+ 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 = userdata;
+ DnsScope *s;
+
+ assert(reply);
+ assert(m);
+
+ 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 = userdata;
+
+ assert(reply);
+ assert(m);
+
+ 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 = userdata;
+ const char *domain;
+ int r;
+
+ assert(reply);
+ assert(m);
+
+ 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 = userdata;
+ DnsScope *s;
+
+ assert(message);
+ assert(m);
+
+ 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 = userdata;
+ int r, ifindex;
+ Link *l;
+
+ assert(message);
+ assert(m);
+
+ 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 = userdata;
+
+ assert(message);
+ assert(m);
+
+ manager_flush_caches(m);
+
+ 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 = userdata;
+
+ assert(message);
+ assert(m);
+
+ 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 = userdata;
+
+ assert(t);
+ assert(s);
+
+ 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;
+ _cleanup_free_ char *path = NULL;
+ _cleanup_free_ char *instance_name = NULL;
+ Manager *m = userdata;
+ DnssdService *s = NULL;
+ const char *name;
+ const char *name_template;
+ const char *type;
+ uid_t euid;
+ int r;
+
+ assert(message);
+ assert(m);
+
+ if (m->mdns_support != RESOLVE_SUPPORT_YES)
+ return sd_bus_error_setf(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(service, &instance_name);
+ 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_setf(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->txt, 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->txt) {
+ 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->txt);
+ 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_allocated(&m->dnssd_services, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(m->dnssd_services, 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 = userdata;
+
+ assert(message);
+ assert(m);
+
+ 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 = userdata;
+ int b, r;
+
+ assert(message);
+ assert(m);
+
+ 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);
+
+ 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..8628d8b
--- /dev/null
+++ b/src/resolve/resolved-bus.h
@@ -0,0 +1,15 @@
+/* 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);
diff --git a/src/resolve/resolved-conf.c b/src/resolve/resolved-conf.c
new file mode 100644
index 0000000..f2a3316
--- /dev/null
+++ b/src/resolve/resolved-conf.c
@@ -0,0 +1,516 @@
+/* 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 and 127.0.0.53 (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;
+ if (r == 0)
+ break;
+
+ 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);
+ }
+
+ return 0;
+}
+
+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;
+ if (r == 0)
+ break;
+
+ 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);
+ }
+
+ return 0;
+}
+
+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 = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(m);
+
+ 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 = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(m);
+
+ 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_host_name, NULL }, /* We will use specifier_dnssd_host_name(). */
+ { '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 = userdata;
+ _cleanup_free_ char *name = NULL;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(s);
+
+ if (isempty(rvalue)) {
+ s->name_template = mfree(s->name_template);
+ return 0;
+ }
+
+ r = specifier_printf(rvalue, specifier_table, 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 = userdata;
+ int r;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(s);
+
+ 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 = userdata;
+ DnsTxtItem *last = NULL;
+
+ assert(filename);
+ assert(lvalue);
+ assert(rvalue);
+ assert(s);
+
+ 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_CUNESCAPE_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("Unknown type of Txt config");
+ }
+
+ LIST_INSERT_AFTER(items, txt_data->txt, last, i);
+ last = i;
+ }
+
+ if (!LIST_IS_EMPTY(txt_data->txt)) {
+ 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_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 gcrypt support. 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..21eb699
--- /dev/null
+++ b/src/resolve/resolved-def.h
@@ -0,0 +1,34 @@
+/* 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)
+
+#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_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..5b762a8
--- /dev/null
+++ b/src/resolve/resolved-dns-answer.c
@@ -0,0 +1,771 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "resolved-dns-answer.h"
+#include "resolved-dns-dnssec.h"
+#include "string-util.h"
+
+DnsAnswer *dns_answer_new(size_t n) {
+ DnsAnswer *a;
+
+ if (n > UINT16_MAX) /* We can only place 64K RRs in an answer at max */
+ n = UINT16_MAX;
+
+ a = malloc0(offsetof(DnsAnswer, items) + sizeof(DnsAnswerItem) * n);
+ if (!a)
+ return NULL;
+
+ a->n_ref = 1;
+ a->n_allocated = n;
+
+ return a;
+}
+
+static void dns_answer_flush(DnsAnswer *a) {
+ DnsResourceRecord *rr;
+
+ if (!a)
+ return;
+
+ DNS_ANSWER_FOREACH(rr, a)
+ dns_resource_record_unref(rr);
+
+ a->n_rrs = 0;
+}
+
+static DnsAnswer *dns_answer_free(DnsAnswer *a) {
+ assert(a);
+
+ dns_answer_flush(a);
+ 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) {
+ assert(rr);
+
+ if (!a)
+ return -ENOSPC;
+
+ if (a->n_rrs >= a->n_allocated)
+ return -ENOSPC;
+
+ a->items[a->n_rrs++] = (DnsAnswerItem) {
+ .rr = dns_resource_record_ref(rr),
+ .ifindex = ifindex,
+ .flags = flags,
+ };
+
+ return 1;
+}
+
+static int dns_answer_add_raw_all(DnsAnswer *a, DnsAnswer *source) {
+ DnsResourceRecord *rr;
+ DnsAnswerFlags flags;
+ int ifindex, r;
+
+ DNS_ANSWER_FOREACH_FULL(rr, ifindex, flags, source) {
+ r = dns_answer_add_raw(a, rr, ifindex, flags);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int dns_answer_add(DnsAnswer *a, DnsResourceRecord *rr, int ifindex, DnsAnswerFlags flags) {
+ size_t i;
+ int r;
+
+ assert(rr);
+
+ if (!a)
+ return -ENOSPC;
+ if (a->n_ref > 1)
+ return -EBUSY;
+
+ for (i = 0; i < a->n_rrs; i++) {
+ if (a->items[i].ifindex != ifindex)
+ continue;
+
+ r = dns_resource_key_equal(a->items[i].rr->key, rr->key);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ /* 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) != (a->items[i].rr->ttl == 0))
+ return -EINVAL;
+
+ r = dns_resource_record_payload_equal(a->items[i].rr, rr);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ /* Entry already exists, keep the entry with the higher RR. */
+ if (rr->ttl > a->items[i].rr->ttl) {
+ dns_resource_record_ref(rr);
+ dns_resource_record_unref(a->items[i].rr);
+ a->items[i].rr = rr;
+ }
+
+ a->items[i].flags |= flags;
+ return 0;
+ }
+
+ return dns_answer_add_raw(a, rr, ifindex, flags);
+}
+
+static int dns_answer_add_all(DnsAnswer *a, DnsAnswer *b) {
+ DnsResourceRecord *rr;
+ DnsAnswerFlags flags;
+ int ifindex, r;
+
+ DNS_ANSWER_FOREACH_FULL(rr, ifindex, flags, b) {
+ r = dns_answer_add(a, rr, ifindex, flags);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int dns_answer_add_extend(DnsAnswer **a, DnsResourceRecord *rr, int ifindex, DnsAnswerFlags flags) {
+ 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);
+}
+
+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);
+}
+
+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;
+}
+
+int 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;
+}
+
+int dns_answer_find_soa(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *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)
+ return 0;
+
+ 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)
+ return 0;
+
+ if (ret)
+ *ret = soa;
+ if (flags)
+ *flags = soa_flags;
+
+ return 1;
+}
+
+int dns_answer_find_cname_or_dname(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *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 (flags)
+ *flags = rr_flags;
+ return 1;
+ }
+ }
+
+ 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(a->n_rrs + b->n_rrs);
+ 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_unref(*a);
+ *a = merged;
+
+ return 0;
+}
+
+int dns_answer_remove_by_key(DnsAnswer **a, const DnsResourceKey *key) {
+ bool found = false, other = false;
+ DnsResourceRecord *rr;
+ size_t i;
+ int r;
+
+ assert(a);
+ assert(key);
+
+ /* Remove all entries matching the specified key from *a */
+
+ DNS_ANSWER_FOREACH(rr, *a) {
+ r = dns_resource_key_equal(rr->key, key);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ found = true;
+ else
+ other = true;
+
+ if (found && other)
+ break;
+ }
+
+ if (!found)
+ return 0;
+
+ if (!other) {
+ *a = dns_answer_unref(*a); /* Return NULL for the empty answer */
+ return 1;
+ }
+
+ if ((*a)->n_ref > 1) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *copy = NULL;
+ DnsAnswerFlags flags;
+ int ifindex;
+
+ copy = dns_answer_new((*a)->n_rrs);
+ if (!copy)
+ return -ENOMEM;
+
+ DNS_ANSWER_FOREACH_FULL(rr, ifindex, flags, *a) {
+ r = dns_resource_key_equal(rr->key, key);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ continue;
+
+ r = dns_answer_add_raw(copy, rr, ifindex, flags);
+ if (r < 0)
+ return r;
+ }
+
+ dns_answer_unref(*a);
+ *a = TAKE_PTR(copy);
+
+ return 1;
+ }
+
+ /* Only a single reference, edit in-place */
+
+ i = 0;
+ for (;;) {
+ if (i >= (*a)->n_rrs)
+ break;
+
+ r = dns_resource_key_equal((*a)->items[i].rr->key, key);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ /* Kill this entry */
+
+ dns_resource_record_unref((*a)->items[i].rr);
+ memmove((*a)->items + i, (*a)->items + i + 1, sizeof(DnsAnswerItem) * ((*a)->n_rrs - i - 1));
+ (*a)->n_rrs--;
+ continue;
+
+ } else
+ /* Keep this entry */
+ i++;
+ }
+
+ return 1;
+}
+
+int dns_answer_remove_by_rr(DnsAnswer **a, DnsResourceRecord *rm) {
+ bool found = false, other = false;
+ DnsResourceRecord *rr;
+ size_t i;
+ int r;
+
+ assert(a);
+ assert(rm);
+
+ /* Remove all entries matching the specified RR from *a */
+
+ DNS_ANSWER_FOREACH(rr, *a) {
+ r = dns_resource_record_equal(rr, rm);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ found = true;
+ else
+ other = true;
+
+ if (found && other)
+ break;
+ }
+
+ if (!found)
+ return 0;
+
+ if (!other) {
+ *a = dns_answer_unref(*a); /* Return NULL for the empty answer */
+ return 1;
+ }
+
+ if ((*a)->n_ref > 1) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *copy = NULL;
+ DnsAnswerFlags flags;
+ int ifindex;
+
+ copy = dns_answer_new((*a)->n_rrs);
+ if (!copy)
+ return -ENOMEM;
+
+ DNS_ANSWER_FOREACH_FULL(rr, ifindex, flags, *a) {
+ r = dns_resource_record_equal(rr, rm);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ continue;
+
+ r = dns_answer_add_raw(copy, rr, ifindex, flags);
+ if (r < 0)
+ return r;
+ }
+
+ dns_answer_unref(*a);
+ *a = TAKE_PTR(copy);
+
+ return 1;
+ }
+
+ /* Only a single reference, edit in-place */
+
+ i = 0;
+ for (;;) {
+ if (i >= (*a)->n_rrs)
+ break;
+
+ r = dns_resource_record_equal((*a)->items[i].rr, rm);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ /* Kill this entry */
+
+ dns_resource_record_unref((*a)->items[i].rr);
+ memmove((*a)->items + i, (*a)->items + i + 1, sizeof(DnsAnswerItem) * ((*a)->n_rrs - i - 1));
+ (*a)->n_rrs--;
+ continue;
+
+ } else
+ /* Keep this entry */
+ i++;
+ }
+
+ return 1;
+}
+
+int dns_answer_copy_by_key(DnsAnswer **a, DnsAnswer *source, const DnsResourceKey *key, DnsAnswerFlags or_flags) {
+ DnsResourceRecord *rr_source;
+ int ifindex_source, r;
+ DnsAnswerFlags flags_source;
+
+ assert(a);
+ assert(key);
+
+ /* Copy all RRs matching the specified key from source into *a */
+
+ DNS_ANSWER_FOREACH_FULL(rr_source, ifindex_source, flags_source, source) {
+
+ r = dns_resource_key_equal(rr_source->key, key);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ /* Make space for at least one entry */
+ r = dns_answer_reserve_or_clone(a, 1);
+ if (r < 0)
+ return r;
+
+ r = dns_answer_add(*a, rr_source, ifindex_source, flags_source|or_flags);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int dns_answer_move_by_key(DnsAnswer **to, DnsAnswer **from, const DnsResourceKey *key, DnsAnswerFlags or_flags) {
+ int r;
+
+ assert(to);
+ assert(from);
+ assert(key);
+
+ r = dns_answer_copy_by_key(to, *from, key, or_flags);
+ 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) {
+ DnsAnswerItem *items;
+ size_t i, start, end;
+
+ if (!a)
+ return;
+
+ if (a->n_rrs <= 1)
+ return;
+
+ start = 0;
+ end = a->n_rrs-1;
+
+ /* RFC 4795, Section 2.6 suggests we should order entries
+ * depending on whether the sender is a link-local address. */
+
+ items = newa(DnsAnswerItem, a->n_rrs);
+ for (i = 0; i < a->n_rrs; i++) {
+
+ if (a->items[i].rr->key->class == DNS_CLASS_IN &&
+ ((a->items[i].rr->key->type == DNS_TYPE_A && in_addr_is_link_local(AF_INET, (union in_addr_union*) &a->items[i].rr->a.in_addr) != prefer_link_local) ||
+ (a->items[i].rr->key->type == DNS_TYPE_AAAA && in_addr_is_link_local(AF_INET6, (union in_addr_union*) &a->items[i].rr->aaaa.in6_addr) != prefer_link_local)))
+ /* Order address records that are not preferred to the end of the array */
+ items[end--] = a->items[i];
+ else
+ /* Order all other records to the beginning of the array */
+ items[start++] = a->items[i];
+ }
+
+ assert(start == end+1);
+ memcpy(a->items, items, sizeof(DnsAnswerItem) * a->n_rrs);
+}
+
+int dns_answer_reserve(DnsAnswer **a, size_t n_free) {
+ DnsAnswer *n;
+
+ assert(a);
+
+ if (n_free <= 0)
+ return 0;
+
+ if (*a) {
+ size_t ns;
+
+ if ((*a)->n_ref > 1)
+ return -EBUSY;
+
+ ns = (*a)->n_rrs + n_free;
+ if (ns > UINT16_MAX) /* Maximum number of RRs we can stick into a DNS packet section */
+ ns = UINT16_MAX;
+
+ if ((*a)->n_allocated >= ns)
+ return 0;
+
+ /* Allocate more than we need */
+ ns *= 2;
+ if (ns > UINT16_MAX)
+ ns = UINT16_MAX;
+
+ n = realloc(*a, offsetof(DnsAnswer, items) + sizeof(DnsAnswerItem) * ns);
+ if (!n)
+ return -ENOMEM;
+
+ n->n_allocated = ns;
+ } else {
+ n = dns_answer_new(n_free);
+ if (!n)
+ return -ENOMEM;
+ }
+
+ *a = n;
+ return 0;
+}
+
+int dns_answer_reserve_or_clone(DnsAnswer **a, size_t n_free) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *n = NULL;
+ int r;
+
+ assert(a);
+
+ /* Tries to extend the DnsAnswer object. And if that's not
+ * possible, since we are not the sole owner, then allocate a
+ * new, appropriately sized one. Either way, after this call
+ * the object will only have a single reference, and has room
+ * for at least the specified number of RRs. */
+
+ r = dns_answer_reserve(a, n_free);
+ if (r != -EBUSY)
+ return r;
+
+ assert(*a);
+
+ n = dns_answer_new(((*a)->n_rrs + n_free) * 2);
+ if (!n)
+ return -ENOMEM;
+
+ r = dns_answer_add_raw_all(n, *a);
+ if (r < 0)
+ return r;
+
+ dns_answer_unref(*a);
+ *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) {
+ DnsResourceRecord *rr;
+ DnsAnswerFlags flags;
+ int ifindex;
+
+ if (!f)
+ f = stdout;
+
+ DNS_ANSWER_FOREACH_FULL(rr, ifindex, flags, answer) {
+ const char *t;
+
+ fputc('\t', f);
+
+ t = dns_resource_record_to_string(rr);
+ if (!t) {
+ log_oom();
+ continue;
+ }
+
+ fputs(t, f);
+
+ if (ifindex != 0 || flags != 0)
+ fputs("\t;", f);
+
+ if (ifindex != 0)
+ fprintf(f, " ifindex=%i", ifindex);
+ if (flags & DNS_ANSWER_AUTHENTICATED)
+ fputs(" authenticated", f);
+ if (flags & DNS_ANSWER_CACHEABLE)
+ fputs(" cacheable", f);
+ if (flags & DNS_ANSWER_SHARED_OWNER)
+ fputs(" shared-owner", f);
+ if (flags & DNS_ANSWER_CACHE_FLUSH)
+ fputs(" cache-flush", f);
+ if (flags & DNS_ANSWER_GOODBYE)
+ fputs(" goodbye", 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;
+}
diff --git a/src/resolve/resolved-dns-answer.h b/src/resolve/resolved-dns-answer.h
new file mode 100644
index 0000000..fd94c51
--- /dev/null
+++ b/src/resolve/resolved-dns-answer.h
@@ -0,0 +1,129 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct DnsAnswer DnsAnswer;
+typedef struct DnsAnswerItem DnsAnswerItem;
+
+#include "macro.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 */
+} DnsAnswerFlags;
+
+struct DnsAnswerItem {
+ DnsResourceRecord *rr;
+ int ifindex;
+ DnsAnswerFlags flags;
+};
+
+struct DnsAnswer {
+ unsigned n_ref;
+ size_t n_rrs, n_allocated;
+ DnsAnswerItem items[0];
+};
+
+DnsAnswer *dns_answer_new(size_t n);
+DnsAnswer *dns_answer_ref(DnsAnswer *a);
+DnsAnswer *dns_answer_unref(DnsAnswer *a);
+
+int dns_answer_add(DnsAnswer *a, DnsResourceRecord *rr, int ifindex, DnsAnswerFlags flags);
+int dns_answer_add_extend(DnsAnswer **a, DnsResourceRecord *rr, int ifindex, DnsAnswerFlags flags);
+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 *combined_flags);
+int dns_answer_contains_nsec_or_nsec3(DnsAnswer *a);
+int dns_answer_contains_zone_nsec3(DnsAnswer *answer, const char *zone);
+
+int dns_answer_find_soa(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *flags);
+int dns_answer_find_cname_or_dname(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *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_copy_by_key(DnsAnswer **a, DnsAnswer *source, const DnsResourceKey *key, DnsAnswerFlags or_flags);
+int dns_answer_move_by_key(DnsAnswer **to, DnsAnswer **from, const DnsResourceKey *key, DnsAnswerFlags or_flags);
+
+int dns_answer_has_dname_for_cname(DnsAnswer *a, DnsResourceRecord *cname);
+
+static inline size_t dns_answer_size(DnsAnswer *a) {
+ return a ? a->n_rrs : 0;
+}
+
+static inline bool dns_answer_isempty(DnsAnswer *a) {
+ return dns_answer_size(a) <= 0;
+}
+
+void dns_answer_dump(DnsAnswer *answer, FILE *f);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsAnswer*, dns_answer_unref);
+
+#define _DNS_ANSWER_FOREACH(q, kk, a) \
+ for (size_t UNIQ_T(i, q) = ({ \
+ (kk) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].rr : NULL; \
+ 0; \
+ }); \
+ (a) && (UNIQ_T(i, q) < (a)->n_rrs); \
+ UNIQ_T(i, q)++, (kk) = (UNIQ_T(i, q) < (a)->n_rrs ? (a)->items[UNIQ_T(i, q)].rr : NULL))
+
+#define DNS_ANSWER_FOREACH(kk, a) _DNS_ANSWER_FOREACH(UNIQ, kk, a)
+
+#define _DNS_ANSWER_FOREACH_IFINDEX(q, kk, ifi, a) \
+ for (size_t UNIQ_T(i, q) = ({ \
+ (kk) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].rr : NULL; \
+ (ifi) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].ifindex : 0; \
+ 0; \
+ }); \
+ (a) && (UNIQ_T(i, q) < (a)->n_rrs); \
+ UNIQ_T(i, q)++, \
+ (kk) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].rr : NULL), \
+ (ifi) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].ifindex : 0))
+
+#define DNS_ANSWER_FOREACH_IFINDEX(kk, ifindex, a) _DNS_ANSWER_FOREACH_IFINDEX(UNIQ, kk, ifindex, a)
+
+#define _DNS_ANSWER_FOREACH_FLAGS(q, kk, fl, a) \
+ for (size_t UNIQ_T(i, q) = ({ \
+ (kk) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].rr : NULL; \
+ (fl) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].flags : 0; \
+ 0; \
+ }); \
+ (a) && (UNIQ_T(i, q) < (a)->n_rrs); \
+ UNIQ_T(i, q)++, \
+ (kk) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].rr : NULL), \
+ (fl) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].flags : 0))
+
+#define DNS_ANSWER_FOREACH_FLAGS(kk, flags, a) _DNS_ANSWER_FOREACH_FLAGS(UNIQ, kk, flags, a)
+
+#define _DNS_ANSWER_FOREACH_FULL(q, kk, ifi, fl, a) \
+ for (size_t UNIQ_T(i, q) = ({ \
+ (kk) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].rr : NULL; \
+ (ifi) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].ifindex : 0; \
+ (fl) = ((a) && (a)->n_rrs > 0) ? (a)->items[0].flags : 0; \
+ 0; \
+ }); \
+ (a) && (UNIQ_T(i, q) < (a)->n_rrs); \
+ UNIQ_T(i, q)++, \
+ (kk) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].rr : NULL), \
+ (ifi) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].ifindex : 0), \
+ (fl) = ((UNIQ_T(i, q) < (a)->n_rrs) ? (a)->items[UNIQ_T(i, q)].flags : 0))
+
+#define DNS_ANSWER_FOREACH_FULL(kk, ifindex, flags, a) _DNS_ANSWER_FOREACH_FULL(UNIQ, kk, ifindex, flags, a)
diff --git a/src/resolve/resolved-dns-cache.c b/src/resolve/resolved-dns-cache.c
new file mode 100644
index 0000000..75f1ccb
--- /dev/null
+++ b/src/resolve/resolved-dns-cache.c
@@ -0,0 +1,1119 @@
+/* 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)
+
+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;
+ DnsResourceKey *key;
+ DnsResourceRecord *rr;
+ int rcode;
+
+ usec_t until;
+ bool authenticated:1;
+ bool shared_owner:1;
+
+ int ifindex;
+ int owner_family;
+ union in_addr_union owner_address;
+
+ unsigned prioq_idx;
+ LIST_FIELDS(DnsCacheItem, by_key);
+};
+
+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 void dns_cache_item_free(DnsCacheItem *i) {
+ if (!i)
+ return;
+
+ dns_resource_record_unref(i->rr);
+ dns_resource_key_unref(i->key);
+ free(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, *i;
+ 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, *i, *n;
+
+ assert(c);
+ assert(key);
+
+ first = hashmap_remove(c->by_key, key);
+ if (!first)
+ return false;
+
+ LIST_FOREACH_SAFE(by_key, i, n, 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_or_monotonic());
+
+ 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) {
+ _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) {
+ DnsCacheItem *i;
+
+ assert(c);
+ assert(rr);
+
+ LIST_FOREACH(by_key, i, 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 nsec_ttl, usec_t timestamp, bool use_soa_minimum) {
+ uint32_t ttl;
+ usec_t u;
+
+ assert(rr);
+
+ ttl = MIN(rr->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,
+ bool authenticated,
+ bool shared_owner,
+ 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_resource_record_ref(rr);
+ dns_resource_record_unref(i->rr);
+ i->rr = rr;
+
+ dns_resource_key_unref(i->key);
+ i->key = dns_resource_key_ref(rr->key);
+
+ i->until = calculate_until(rr, (uint32_t) -1, timestamp, false);
+ i->authenticated = authenticated;
+ i->shared_owner = shared_owner;
+
+ 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,
+ DnsResourceRecord *rr,
+ bool authenticated,
+ bool shared_owner,
+ usec_t timestamp,
+ int ifindex,
+ int owner_family,
+ const union in_addr_union *owner_address) {
+
+ _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
+ DnsCacheItem *existing;
+ char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+ int r, k;
+
+ 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;
+
+ /* New TTL is 0? Delete this specific entry... */
+ if (rr->ttl <= 0) {
+ k = dns_cache_remove_by_rr(c, rr);
+ log_debug("%s: %s",
+ k > 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,
+ authenticated,
+ shared_owner,
+ timestamp,
+ ifindex,
+ owner_family,
+ owner_address);
+ return 0;
+ }
+
+ /* Otherwise, add the new RR */
+ 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 = DNS_CACHE_POSITIVE,
+ .key = dns_resource_key_ref(rr->key),
+ .rr = dns_resource_record_ref(rr),
+ .until = calculate_until(rr, (uint32_t) -1, timestamp, false),
+ .authenticated = authenticated,
+ .shared_owner = shared_owner,
+ .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;
+
+ if (DEBUG_LOGGING) {
+ _cleanup_free_ char *t = NULL;
+ char ifname[IF_NAMESIZE + 1];
+
+ (void) in_addr_to_string(i->owner_family, &i->owner_address, &t);
+
+ log_debug("Added positive %s%s cache entry for %s "USEC_FMT"s on %s/%s/%s",
+ i->authenticated ? "authenticated" : "unauthenticated",
+ 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 ? "*" : strna(format_ifname(i->ifindex, ifname)),
+ af_to_name_short(i->owner_family),
+ strna(t));
+ }
+
+ i = NULL;
+ return 0;
+}
+
+static int dns_cache_put_negative(
+ DnsCache *c,
+ DnsResourceKey *key,
+ int rcode,
+ bool authenticated,
+ 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,
+ .authenticated = authenticated,
+ .owner_family = owner_family,
+ .owner_address = *owner_address,
+ .prioq_idx = PRIOQ_IDX_NULL,
+ .rcode = rcode,
+ };
+
+ i->until =
+ i->type == DNS_CACHE_RCODE ? timestamp + CACHE_TTL_STRANGE_RCODE_USEC :
+ calculate_until(soa, 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,
+ DnsResourceKey *key,
+ int rcode,
+ DnsAnswer *answer,
+ bool authenticated,
+ uint32_t nsec_ttl,
+ usec_t timestamp,
+ int owner_family,
+ const union in_addr_union *owner_address) {
+
+ DnsResourceRecord *soa = NULL, *rr;
+ bool weird_rcode = false;
+ DnsAnswerFlags flags;
+ unsigned cache_keys;
+ int r, ifindex;
+
+ 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_size(answer) <= 0) {
+ 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);
+
+ if (timestamp <= 0)
+ timestamp = now(clock_boottime_or_monotonic());
+
+ /* Second, add in positive entries for all contained RRs */
+ DNS_ANSWER_FOREACH_FULL(rr, ifindex, flags, answer) {
+ if ((flags & DNS_ANSWER_CACHEABLE) == 0 ||
+ !rr_eligible(rr))
+ continue;
+
+ r = dns_cache_put_positive(
+ c,
+ rr,
+ flags & DNS_ANSWER_AUTHENTICATED,
+ flags & DNS_ANSWER_SHARED_OWNER,
+ timestamp,
+ 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 (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,
+ authenticated,
+ 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_FLAGS(rr, flags, answer) {
+ if ((flags & DNS_ANSWER_CACHEABLE) == 0)
+ continue;
+
+ dns_cache_remove_by_key(c, 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;
+}
+
+int dns_cache_lookup(DnsCache *c, DnsResourceKey *key, bool clamp_ttl, int *rcode, DnsAnswer **ret, bool *authenticated) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+ char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+ unsigned n = 0;
+ int r;
+ bool nxdomain = false;
+ DnsCacheItem *j, *first, *nsec = NULL;
+ bool have_authenticated = false, have_non_authenticated = false;
+ usec_t current;
+ int found_rcode = -1;
+
+ assert(c);
+ assert(key);
+ assert(rcode);
+ assert(ret);
+ assert(authenticated);
+
+ 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));
+
+ c->n_miss++;
+
+ *ret = NULL;
+ *rcode = DNS_RCODE_SUCCESS;
+ *authenticated = false;
+
+ return 0;
+ }
+
+ 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));
+
+ c->n_miss++;
+
+ *ret = NULL;
+ *rcode = DNS_RCODE_SUCCESS;
+ *authenticated = false;
+
+ return 0;
+ }
+
+ LIST_FOREACH(by_key, j, first) {
+ if (j->rr) {
+ if (j->rr->key->type == DNS_TYPE_NSEC)
+ nsec = j;
+
+ n++;
+ } else if (j->type == DNS_CACHE_NXDOMAIN)
+ nxdomain = true;
+ else if (j->type == DNS_CACHE_RCODE)
+ found_rcode = j->rcode;
+
+ if (j->authenticated)
+ have_authenticated = true;
+ else
+ have_non_authenticated = true;
+ }
+
+ if (found_rcode >= 0) {
+ log_debug("RCODE %s cache hit for %s",
+ dns_rcode_to_string(found_rcode),
+ dns_resource_key_to_string(key, key_str, sizeof(key_str)));
+
+ *ret = NULL;
+ *rcode = found_rcode;
+ *authenticated = false;
+
+ 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. */
+
+ *ret = NULL;
+ *rcode = DNS_RCODE_SUCCESS;
+ *authenticated = nsec->authenticated;
+
+ 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++;
+
+ *ret = NULL;
+ *rcode = nxdomain ? DNS_RCODE_NXDOMAIN : DNS_RCODE_SUCCESS;
+ *authenticated = have_authenticated && !have_non_authenticated;
+ return 1;
+ }
+
+ answer = dns_answer_new(n);
+ if (!answer)
+ return -ENOMEM;
+
+ if (clamp_ttl)
+ current = now(clock_boottime_or_monotonic());
+
+ LIST_FOREACH(by_key, j, first) {
+ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+
+ if (!j->rr)
+ continue;
+
+ if (clamp_ttl) {
+ rr = dns_resource_record_ref(j->rr);
+
+ r = dns_resource_record_clamp_ttl(&rr, LESS_BY(j->until, current) / USEC_PER_SEC);
+ if (r < 0)
+ return r;
+ }
+
+ r = dns_answer_add(answer, rr ?: j->rr, j->ifindex, j->authenticated ? DNS_ANSWER_AUTHENTICATED : 0);
+ if (r < 0)
+ return r;
+ }
+
+ c->n_hit++;
+
+ *ret = answer;
+ *rcode = DNS_RCODE_SUCCESS;
+ *authenticated = have_authenticated && !have_non_authenticated;
+ answer = NULL;
+
+ return n;
+}
+
+int dns_cache_check_conflicts(DnsCache *cache, DnsResourceRecord *rr, int owner_family, const union in_addr_union *owner_address) {
+ DnsCacheItem *i, *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) {
+ unsigned ancount = 0;
+ DnsCacheItem *i;
+ int r;
+
+ assert(cache);
+ assert(p);
+
+ HASHMAP_FOREACH(i, cache->by_key) {
+ DnsCacheItem *j;
+
+ LIST_FOREACH(by_key, j, i) {
+ if (!j->rr)
+ continue;
+
+ if (!j->shared_owner)
+ continue;
+
+ r = dns_packet_append_rr(p, j->rr, 0, NULL, NULL);
+ if (r == -EMSGSIZE && p->protocol == DNS_PROTOCOL_MDNS) {
+ /* For mDNS, 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++;
+ }
+ }
+
+ 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) {
+ DnsCacheItem *j;
+
+ 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..4ab213d
--- /dev/null
+++ b/src/resolve/resolved-dns-cache.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "hashmap.h"
+#include "list.h"
+#include "prioq.h"
+#include "resolve-util.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, DnsResourceKey *key, int rcode, DnsAnswer *answer, bool authenticated, uint32_t nsec_ttl, usec_t timestamp, int owner_family, const union in_addr_union *owner_address);
+int dns_cache_lookup(DnsCache *c, DnsResourceKey *key, bool clamp_ttl, int *rcode, DnsAnswer **answer, bool *authenticated);
+
+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);
diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c
new file mode 100644
index 0000000..2f5776b
--- /dev/null
+++ b/src/resolve/resolved-dns-dnssec.c
@@ -0,0 +1,2263 @@
+/* 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 "resolved-dns-dnssec.h"
+#include "resolved-dns-packet.h"
+#include "sort-util.h"
+#include "string-table.h"
+
+#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;
+ size_t i;
+
+ /* 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 (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_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(
+ const char *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) {
+
+ 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;
+ int r;
+
+ 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) {
+ log_debug("RSA signature check failed: %s", gpg_strerror(ge));
+ r = -EIO;
+ } 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);
+
+ return r;
+}
+
+static int dnssec_rsa_verify(
+ const char *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(
+ const char *hash_algorithm,
+ const char *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) {
+
+ 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;
+ int k;
+
+ 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);
+
+ return k;
+}
+
+static int dnssec_ecdsa_verify(
+ const char *hash_algorithm,
+ int algorithm,
+ const void *hash, size_t hash_size,
+ DnsResourceRecord *rrsig,
+ DnsResourceRecord *dnskey) {
+
+ const char *curve;
+ size_t key_size;
+ uint8_t *q;
+
+ assert(hash);
+ assert(hash_size);
+ assert(rrsig);
+ assert(dnskey);
+
+ if (algorithm == DNSSEC_ALGORITHM_ECDSAP256SHA256) {
+ key_size = 32;
+ curve = "NIST P-256";
+ } else if (algorithm == DNSSEC_ALGORITHM_ECDSAP384SHA384) {
+ key_size = 48;
+ curve = "NIST P-384";
+ } 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);
+}
+
+#if GCRYPT_VERSION_NUMBER >= 0x010600
+static int dnssec_eddsa_verify_raw(
+ const char *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) {
+
+ gcry_sexp_t public_key_sexp = NULL, data_sexp = NULL, signature_sexp = NULL;
+ gcry_error_t ge;
+ int k;
+
+ ge = gcry_sexp_build(&signature_sexp,
+ NULL,
+ "(sig-val (eddsa (r %b) (s %b)))",
+ (int) signature_r_size,
+ signature_r,
+ (int) signature_s_size,
+ signature_s);
+ 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) {
+ log_debug("EdDSA signature check failed: %s", gpg_strerror(ge));
+ k = -EIO;
+ } 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;
+}
+
+static int dnssec_eddsa_verify(
+ int algorithm,
+ const void *data, size_t data_size,
+ DnsResourceRecord *rrsig,
+ DnsResourceRecord *dnskey) {
+ const char *curve;
+ size_t key_size;
+
+ if (algorithm == DNSSEC_ALGORITHM_ED25519) {
+ curve = "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, key_size,
+ (uint8_t*) rrsig->rrsig.signature + key_size, key_size,
+ data, data_size,
+ dnskey->dnskey.key, key_size);
+}
+#endif
+
+static void md_add_uint8(gcry_md_hd_t md, uint8_t v) {
+ gcry_md_write(md, &v, sizeof(v));
+}
+
+static void md_add_uint16(gcry_md_hd_t md, uint16_t v) {
+ v = htobe16(v);
+ gcry_md_write(md, &v, sizeof(v));
+}
+
+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 != (unsigned) -1)
+ 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;
+
+ 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 int algorithm_to_gcrypt_md(uint8_t algorithm) {
+
+ /* Translates a DNSSEC signature algorithm into a 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 GCRY_MD_SHA1;
+
+ case DNSSEC_ALGORITHM_RSASHA256:
+ case DNSSEC_ALGORITHM_ECDSAP256SHA256:
+ return GCRY_MD_SHA256;
+
+ case DNSSEC_ALGORITHM_ECDSAP384SHA384:
+ return GCRY_MD_SHA384;
+
+ case DNSSEC_ALGORITHM_RSASHA512:
+ return GCRY_MD_SHA512;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static void dnssec_fix_rrset_ttl(
+ DnsResourceRecord *list[],
+ unsigned n,
+ DnsResourceRecord *rrsig,
+ usec_t realtime) {
+
+ unsigned k;
+
+ assert(list);
+ assert(n > 0);
+ assert(rrsig);
+
+ for (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;
+}
+
+int dnssec_verify_rrset(
+ DnsAnswer *a,
+ const DnsResourceKey *key,
+ DnsResourceRecord *rrsig,
+ DnsResourceRecord *dnskey,
+ usec_t realtime,
+ DnssecResult *result) {
+
+ uint8_t wire_format_name[DNS_WIRE_FORMAT_HOSTNAME_MAX];
+ DnsResourceRecord **list, *rr;
+ const char *source, *name;
+ _cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL;
+ int r, md_algorithm;
+ size_t k, n = 0;
+ size_t sig_size = 0;
+ _cleanup_free_ char *sig_data = NULL;
+ _cleanup_fclose_ FILE *f = NULL;
+ size_t hash_size;
+ void *hash;
+ bool wildcard;
+
+ 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);
+
+ 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 (k = 0; k < n; 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);
+ if (r < 0)
+ return r;
+
+ initialize_libgcrypt(false);
+
+ switch (rrsig->rrsig.algorithm) {
+#if GCRYPT_VERSION_NUMBER >= 0x010600
+ case DNSSEC_ALGORITHM_ED25519:
+ break;
+#else
+ case DNSSEC_ALGORITHM_ED25519:
+#endif
+ case DNSSEC_ALGORITHM_ED448:
+ *result = DNSSEC_UNSUPPORTED_ALGORITHM;
+ return 0;
+ default:
+ /* OK, the RRs are now in canonical order. Let's calculate the digest */
+ md_algorithm = algorithm_to_gcrypt_md(rrsig->rrsig.algorithm);
+ if (md_algorithm == -EOPNOTSUPP) {
+ *result = DNSSEC_UNSUPPORTED_ALGORITHM;
+ return 0;
+ }
+ if (md_algorithm < 0)
+ return md_algorithm;
+
+ gcry_md_open(&md, md_algorithm, 0);
+ if (!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;
+ }
+
+ switch (rrsig->rrsig.algorithm) {
+
+ case DNSSEC_ALGORITHM_RSASHA1:
+ case DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1:
+ case DNSSEC_ALGORITHM_RSASHA256:
+ case DNSSEC_ALGORITHM_RSASHA512:
+ r = dnssec_rsa_verify(
+ gcry_md_algo_name(md_algorithm),
+ hash, hash_size,
+ rrsig,
+ dnskey);
+ break;
+
+ case DNSSEC_ALGORITHM_ECDSAP256SHA256:
+ case DNSSEC_ALGORITHM_ECDSAP384SHA384:
+ r = dnssec_ecdsa_verify(
+ gcry_md_algo_name(md_algorithm),
+ rrsig->rrsig.algorithm,
+ hash, hash_size,
+ rrsig,
+ dnskey);
+ break;
+#if GCRYPT_VERSION_NUMBER >= 0x010600
+ case DNSSEC_ALGORITHM_ED25519:
+ r = dnssec_eddsa_verify(
+ rrsig->rrsig.algorithm,
+ sig_data, sig_size,
+ rrsig,
+ dnskey);
+ break;
+#endif
+ }
+ 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, realtime);
+
+ 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 (!a || a->n_rrs <= 0)
+ 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("Unexpected DNSSEC validation result");
+ }
+ }
+ }
+
+ 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 int digest_to_gcrypt_md(uint8_t algorithm) {
+
+ /* Translates a DNSSEC digest algorithm into a gcrypt digest identifier */
+
+ switch (algorithm) {
+
+ case DNSSEC_DIGEST_SHA1:
+ return GCRY_MD_SHA1;
+
+ case DNSSEC_DIGEST_SHA256:
+ return GCRY_MD_SHA256;
+
+ case DNSSEC_DIGEST_SHA384:
+ return GCRY_MD_SHA384;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke) {
+ uint8_t wire_format[DNS_WIRE_FORMAT_HOSTNAME_MAX];
+ _cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL;
+ size_t hash_size;
+ int md_algorithm, r;
+ void *result;
+
+ 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;
+
+ initialize_libgcrypt(false);
+
+ md_algorithm = digest_to_gcrypt_md(ds->ds.digest_type);
+ if (md_algorithm < 0)
+ return md_algorithm;
+
+ hash_size = gcry_md_get_algo_dlen(md_algorithm);
+ assert(hash_size > 0);
+
+ if (ds->ds.digest_size != hash_size)
+ 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;
+
+ gcry_md_open(&md, md_algorithm, 0);
+ if (!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);
+
+ result = gcry_md_read(md, 0);
+ if (!result)
+ return -EIO;
+
+ 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 int nsec3_hash_to_gcrypt_md(uint8_t algorithm) {
+
+ /* Translates a DNSSEC NSEC3 hash algorithm into a gcrypt digest identifier */
+
+ switch (algorithm) {
+
+ case NSEC3_ALGORITHM_SHA1:
+ return GCRY_MD_SHA1;
+
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret) {
+ uint8_t wire_format[DNS_WIRE_FORMAT_HOSTNAME_MAX];
+ gcry_md_hd_t md = NULL;
+ size_t hash_size;
+ int algorithm;
+ void *result;
+ unsigned k;
+ 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));
+
+ algorithm = nsec3_hash_to_gcrypt_md(nsec3->nsec3.algorithm);
+ if (algorithm < 0)
+ return algorithm;
+
+ initialize_libgcrypt(false);
+
+ 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;
+
+ gcry_md_open(&md, algorithm, 0);
+ if (!md)
+ return -EIO;
+
+ gcry_md_write(md, wire_format, r);
+ gcry_md_write(md, nsec3->nsec3.salt, nsec3->nsec3.salt_size);
+
+ result = gcry_md_read(md, 0);
+ if (!result) {
+ r = -EIO;
+ goto finish;
+ }
+
+ for (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) {
+ r = -EIO;
+ goto finish;
+ }
+ }
+
+ memcpy(ret, result, hash_size);
+ r = (int) hash_size;
+
+finish:
+ gcry_md_close(md);
+ return r;
+}
+
+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 (nsec3_hash_to_gcrypt_md(rr->nsec3.algorithm) < 0)
+ return 0;
+ /* 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, (unsigned) -1))
+ 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, (unsigned) -1))
+ 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..9c3c0dc
--- /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 = -1
+};
+
+enum DnssecVerdict {
+ DNSSEC_SECURE,
+ DNSSEC_INSECURE,
+ DNSSEC_BOGUS,
+ DNSSEC_INDETERMINATE,
+
+ _DNSSEC_VERDICT_MAX,
+ _DNSSEC_VERDICT_INVALID = -1
+};
+
+#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..b4eb5ef
--- /dev/null
+++ b/src/resolve/resolved-dns-packet.c
@@ -0,0 +1,2410 @@
+/* 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 "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 INIT_REWINDER(rewinder, p) do { rewinder.packet = p; rewinder.saved_rindex = p->rindex; } while (0)
+#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_t) -1,
+ .opt_size = (size_t) -1,
+ };
+
+ *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;
+}
+
+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;
+
+ if (DNS_PACKET_TC(p))
+ return -EBADMSG;
+
+ switch (p->protocol) {
+
+ case DNS_PROTOCOL_LLMNR:
+ case DNS_PROTOCOL_DNS:
+ /* 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:
+ /* RFC 6762, Section 18 */
+ if (DNS_PACKET_AA(p) != 0 ||
+ DNS_PACKET_RD(p) != 0 ||
+ DNS_PACKET_RA(p) != 0 ||
+ DNS_PACKET_AD(p) != 0 ||
+ DNS_PACKET_CD(p) != 0 ||
+ 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) {
+ size_t i;
+
+ /* Generate in canonical form, as defined by DNSSEC
+ * RFC 4034, Section 6.2, i.e. all lower-case. */
+
+ for (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_allocated(&p->names, &dns_name_hash_ops);
+ if (r < 0)
+ goto fail;
+
+ r = hashmap_put(p->names, s, SIZE_TO_PTR(n));
+ if (r < 0)
+ goto fail;
+
+ s = NULL;
+ }
+ }
+
+ 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 : 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,
+ int rcode,
+ size_t *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_t) -1)
+ return -EBUSY;
+
+ assert(p->opt_size == (size_t) -1);
+
+ 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;
+
+ /* RDLENGTH */
+ 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 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 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);
+ if (r < 0)
+ goto fail;
+
+ r = dns_packet_append_blob(p, rfc6975, sizeof(rfc6975), 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 (start)
+ *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_t) -1) {
+ assert(p->opt_size == (size_t) -1);
+ return 0;
+ }
+
+ assert(p->opt_size != (size_t) -1);
+ 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_t) -1;
+
+ 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 {
+ DnsTxtItem *i;
+
+ 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) {
+ 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;
+ }
+
+ return 0;
+}
+
+int dns_packet_read(DnsPacket *p, size_t sz, const void **ret, size_t *start) {
+ assert(p);
+
+ if (p->rindex + sz > p->size)
+ 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;
+
+ *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) {
+ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+ const void *d;
+ char *t;
+ uint8_t c;
+ int r;
+
+ assert(p);
+ INIT_REWINDER(rewinder, p);
+
+ 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 = strndup(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) {
+ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+ uint8_t c;
+ int r;
+
+ assert(p);
+ INIT_REWINDER(rewinder, p);
+
+ 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 *start) {
+
+ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+ size_t after_rindex = 0, jump_barrier;
+ _cleanup_free_ char *ret = NULL;
+ size_t n = 0, allocated = 0;
+ bool first = true;
+ int r;
+
+ assert(p);
+ assert(_ret);
+ INIT_REWINDER(rewinder, p);
+ jump_barrier = p->rindex;
+
+ 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(ret, allocated, n + !first + DNS_LABEL_ESCAPED_MAX))
+ return -ENOMEM;
+
+ if (first)
+ first = false;
+ else
+ ret[n++] = '.';
+
+ r = dns_label_escape(label, c, ret + 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(ret, allocated, n + 1))
+ return -ENOMEM;
+
+ ret[n] = 0;
+
+ if (after_rindex != 0)
+ p->rindex= after_rindex;
+
+ *_ret = TAKE_PTR(ret);
+
+ if (start)
+ *start = rewinder.saved_rindex;
+ CANCEL_REWINDER(rewinder);
+
+ return 0;
+}
+
+static int dns_packet_read_type_window(DnsPacket *p, Bitmap **types, size_t *start) {
+ uint8_t window;
+ uint8_t length;
+ const uint8_t *bitmap;
+ uint8_t bit = 0;
+ unsigned i;
+ bool found = false;
+ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+ int r;
+
+ assert(p);
+ assert(types);
+ INIT_REWINDER(rewinder, p);
+
+ 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 (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;
+ int r;
+
+ INIT_REWINDER(rewinder, p);
+
+ while (p->rindex < rewinder.saved_rindex + size) {
+ r = dns_packet_read_type_window(p, types, NULL);
+ if (r < 0)
+ return r;
+
+ /* 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, size_t *start) {
+ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+ _cleanup_free_ char *name = NULL;
+ bool cache_flush = false;
+ uint16_t class, type;
+ DnsResourceKey *key;
+ int r;
+
+ assert(p);
+ assert(ret);
+ INIT_REWINDER(rewinder, p);
+
+ 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, Section 10.2 */
+
+ if (type != DNS_TYPE_OPT && (class & MDNS_RR_CACHE_FLUSH)) {
+ class &= ~MDNS_RR_CACHE_FLUSH;
+ cache_flush = true;
+ }
+ }
+
+ key = dns_resource_key_new_consume(class, type, name);
+ if (!key)
+ return -ENOMEM;
+
+ name = NULL;
+ *ret = key;
+
+ if (ret_cache_flush)
+ *ret_cache_flush = cache_flush;
+ if (start)
+ *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 *start) {
+ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+ size_t offset;
+ uint16_t rdlength;
+ bool cache_flush;
+ int r;
+
+ assert(p);
+ assert(ret);
+
+ INIT_REWINDER(rewinder, p);
+
+ 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 (p->rindex + rdlength > p->size)
+ 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 + offset < p->rindex)
+ 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;
+
+ 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;
+
+ 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 + offset < p->rindex)
+ 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;
+
+ *ret = TAKE_PTR(rr);
+
+ if (ret_cache_flush)
+ *ret_cache_flush = cache_flush;
+ if (start)
+ *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, i;
+ 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 of after 2. */
+ if (r < 0)
+ return r;
+
+ for (i = 0; i < n; i++) {
+ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+ bool cache_flush;
+
+ r = dns_packet_read_key(p, &key, &cache_flush, NULL);
+ if (r < 0)
+ return r;
+
+ if (cache_flush)
+ return -EBADMSG;
+
+ 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);
+ 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, i;
+ _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 (i = 0; i < n; i++) {
+ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+ bool cache_flush = false;
+
+ r = dns_packet_read_rr(p, &rr, &cache_flush, NULL);
+ 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);
+ } else {
+ /* 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. */
+ DnsAnswerFlags flags =
+ (i < DNS_PACKET_ANCOUNT(p) ? DNS_ANSWER_CACHEABLE : 0) |
+ (p->protocol == DNS_PROTOCOL_MDNS && !cache_flush ? DNS_ANSWER_SHARED_OWNER : 0);
+
+ r = dns_answer_add(answer, rr, p->ifindex, flags);
+ if (r < 0)
+ return r;
+ }
+
+ /* Remember this RR, so that we potentically can merge it's ->key object with the
+ * next RR. Note that we only do this if we actually decided to keep the RR around.
+ */
+ dns_resource_record_unref(previous);
+ previous = dns_resource_record_ref(rr);
+ }
+
+ if (bad_opt)
+ p->opt = dns_resource_record_unref(p->opt);
+
+ *ret_answer = TAKE_PTR(answer);
+
+ return 0;
+}
+
+int dns_packet_extract(DnsPacket *p) {
+ _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
+ _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+ _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = {};
+ int r;
+
+ if (p->extracted)
+ return 0;
+
+ INIT_REWINDER(rewinder, p);
+ 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;
+
+ 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(p->question->keys[0], key);
+}
+
+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);
+
+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);
+
+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..7d6ee2b
--- /dev/null
+++ b/src/resolve/resolved-dns-packet.h
@@ -0,0 +1,302 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/ip.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 = -1
+} DnsProtocol;
+
+struct DnsPacketHeader {
+ uint16_t id;
+ be16_t flags;
+ be16_t qdcount;
+ be16_t ancount;
+ be16_t nscount;
+ be16_t arcount;
+};
+
+#define DNS_PACKET_HEADER_SIZE sizeof(DnsPacketHeader)
+#define UDP_PACKET_HEADER_SIZE (sizeof(struct iphdr) + sizeof(struct udphdr))
+
+/* 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 4096, which is what is commonly used */
+#define DNS_PACKET_UNICAST_SIZE_LARGE_MAX 4096u
+
+struct DnsPacket {
+ unsigned n_ref;
+ DnsProtocol protocol;
+ size_t size, allocated, rindex, max_size;
+ 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;
+
+ /* Packet reception metadata */
+ int ifindex;
+ int family, ipproto;
+ union in_addr_union sender, destination;
+ uint16_t sender_port, destination_port;
+ uint32_t ttl;
+
+ /* For support of truncated packets */
+ DnsPacket *more;
+
+ bool on_stack:1;
+ bool extracted:1;
+ bool refuse_compression:1;
+ bool canonical_form:1;
+};
+
+static inline uint8_t* DNS_PACKET_DATA(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);
+}
+
+/* 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);
+
+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);
+
+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, int rcode, size_t *start);
+int dns_packet_append_question(DnsPacket *p, DnsQuestion *q);
+int dns_packet_append_answer(DnsPacket *p, DnsAnswer *a);
+
+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, 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);
+
+static inline bool DNS_PACKET_SHALL_CACHE(DnsPacket *p) {
+ /* Never cache data originating from localhost, under the
+ * assumption, that it's coming from a locally DNS forwarder
+ * or server, that is caching on its own. */
+
+ return in_addr_is_localhost(p->family, &p->sender) == 0;
+}
+
+/* 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* 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) {
+ uint64_t f;
+
+ /* Converts a protocol + family into a flags field as used in queries and responses */
+
+ f = authenticated ? SD_RESOLVED_AUTHENTICATED : 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;
+}
diff --git a/src/resolve/resolved-dns-query.c b/src/resolve/resolved-dns-query.c
new file mode 100644
index 0000000..8ee4fd8
--- /dev/null
+++ b/src/resolve/resolved-dns-query.c
@@ -0,0 +1,1041 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "dns-type.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 CNAME_MAX 8
+#define QUERIES_MAX 2048
+#define AUXILIARY_QUERIES_MAX 64
+
+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);
+
+ 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_free(DnsQueryCandidate *c) {
+ if (!c)
+ return NULL;
+
+ dns_query_candidate_stop(c);
+
+ set_free(c->transactions);
+ dns_search_domain_unref(c->search_domain);
+
+ if (c->query)
+ LIST_REMOVE(candidates_by_query, c->query->candidates, c);
+
+ if (c->scope)
+ LIST_REMOVE(candidates_by_scope, c->scope->query_candidates, c);
+
+ 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) {
+ _cleanup_(dns_transaction_gcp) DnsTransaction *t = NULL;
+ int r;
+
+ assert(c);
+ assert(key);
+
+ t = dns_scope_find_transaction(c->scope, key, true);
+ if (!t) {
+ r = dns_transaction_new(&t, c->scope, key);
+ if (r < 0)
+ return r;
+ } else if (set_contains(c->transactions, t))
+ return 0;
+
+ 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;
+ }
+
+ t->clamp_ttl = c->query->clamp_ttl;
+ TAKE_PTR(t);
+ return 1;
+}
+
+static int dns_query_candidate_go(DnsQueryCandidate *c) {
+ _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);
+
+ dns_query_candidate_stop(c);
+
+ 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);
+ 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);
+
+ 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) {
+ DnsQueryCandidate *c;
+
+ assert(q);
+
+ q->timeout_event_source = sd_event_source_unref(q->timeout_event_source);
+
+ LIST_FOREACH(candidates_by_query, c, q->candidates)
+ dns_query_candidate_stop(c);
+}
+
+static void dns_query_unref_candidates(DnsQuery *q) {
+ assert(q);
+
+ while (q->candidates)
+ dns_query_candidate_unref(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_authenticated = false;
+ q->answer_protocol = _DNS_PROTOCOL_INVALID;
+ q->answer_family = AF_UNSPEC;
+ q->answer_search_domain = dns_search_domain_unref(q->answer_search_domain);
+}
+
+DnsQuery *dns_query_free(DnsQuery *q) {
+ if (!q)
+ return NULL;
+
+ 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_unref_candidates(q);
+
+ dns_question_unref(q->question_idna);
+ dns_question_unref(q->question_utf8);
+
+ 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);
+ }
+
+ dns_packet_unref(q->request_dns_packet);
+ dns_packet_unref(q->reply_dns_packet);
+
+ if (q->request_dns_stream) {
+ /* Detach the stream from our query, in case something else keeps a reference to it. */
+ (void) set_remove(q->request_dns_stream->queries, q);
+ q->request_dns_stream = dns_stream_unref(q->request_dns_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,
+ int ifindex,
+ uint64_t flags) {
+
+ _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+ DnsResourceKey *key;
+ bool good = false;
+ int r;
+ char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+
+ assert(m);
+
+ 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),
+ .ifindex = ifindex,
+ .flags = flags,
+ .answer_dnssec_result = _DNSSEC_RESULT_INVALID,
+ .answer_protocol = _DNS_PROTOCOL_INVALID,
+ .answer_family = AF_UNSPEC,
+ };
+
+ /* 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(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;
+ q = NULL;
+
+ 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;
+
+ 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 = userdata;
+
+ assert(s);
+ assert(q);
+
+ 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;
+
+ r = dns_synthesize_answer(
+ q->manager,
+ 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_authenticated = true;
+ *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_authenticated = true;
+
+ *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. */
+
+ r = manager_etc_hosts_lookup(
+ q->manager,
+ 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_authenticated = true;
+
+ return 1;
+}
+
+int dns_query_go(DnsQuery *q) {
+ DnsScopeMatch found = DNS_SCOPE_NO;
+ DnsScope *s, *first = NULL;
+ DnsQueryCandidate *c;
+ 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;
+ const char *name;
+
+ name = dns_question_first_name(dns_query_question_for_protocol(q, s->protocol));
+ if (!name)
+ continue;
+
+ match = dns_scope_good_domain(s, q->ifindex, q->flags, name);
+ if (match < 0) {
+ log_debug("Couldn't check if '%s' matches against scope, ignoring.", name);
+ continue;
+ }
+
+ 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;
+ const char *name;
+
+ name = dns_question_first_name(dns_query_question_for_protocol(q, s->protocol));
+ if (!name)
+ continue;
+
+ match = dns_scope_good_domain(s, q->ifindex, q->flags, name);
+ if (match < 0) {
+ log_debug("Couldn't check if '%s' matches against scope, ignoring.", name);
+ continue;
+ }
+
+ if (match < found)
+ continue;
+
+ r = dns_query_add_candidate(q, s);
+ if (r < 0)
+ goto fail;
+ }
+
+ dns_query_reset_answer(q);
+
+ r = sd_event_add_time_relative(
+ q->manager->event,
+ &q->timeout_event_source,
+ clock_boottime_or_monotonic(),
+ SD_RESOLVED_QUERY_TIMEOUT_USEC,
+ 0, on_query_timeout, q);
+ if (r < 0)
+ goto fail;
+
+ (void) sd_event_source_set_description(q->timeout_event_source, "query-timeout");
+
+ 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;
+ 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_authenticated = false;
+ q->answer_errno = c->error_code;
+ }
+
+ SET_FOREACH(t, c->transactions) {
+
+ switch (t->state) {
+
+ case DNS_TRANSACTION_SUCCESS: {
+ /* We found a successfully reply, merge it into the answer */
+ r = dns_answer_extend(&q->answer, t->answer);
+ if (r < 0)
+ goto fail;
+
+ q->answer_rcode = t->answer_rcode;
+ q->answer_errno = 0;
+
+ if (t->answer_authenticated) {
+ has_authenticated = true;
+ dnssec_result_authenticated = t->answer_dnssec_result;
+ } else {
+ has_non_authenticated = true;
+ dnssec_result_non_authenticated = t->answer_dnssec_result;
+ }
+
+ 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 (q->answer_authenticated && !t->answer_authenticated)
+ continue;
+
+ q->answer = dns_answer_unref(q->answer);
+ q->answer_rcode = t->answer_rcode;
+ q->answer_dnssec_result = t->answer_dnssec_result;
+ q->answer_authenticated = t->answer_authenticated;
+ q->answer_errno = t->answer_errno;
+
+ state = t->state;
+ break;
+ }
+ }
+
+ if (state == DNS_TRANSACTION_SUCCESS) {
+ q->answer_authenticated = has_authenticated && !has_non_authenticated;
+ q->answer_dnssec_result = q->answer_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, *c;
+ 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_cname_redirect(DnsQuery *q, const DnsResourceRecord *cname) {
+ _cleanup_(dns_question_unrefp) DnsQuestion *nq_idna = NULL, *nq_utf8 = NULL;
+ int r, k;
+
+ assert(q);
+
+ q->n_cname_redirects++;
+ if (q->n_cname_redirects > CNAME_MAX)
+ return -ELOOP;
+
+ r = dns_question_cname_redirect(q->question_idna, cname, &nq_idna);
+ if (r < 0)
+ return r;
+ else if (r > 0)
+ log_debug("Following CNAME/DNAME %s → %s.", dns_question_first_name(q->question_idna), dns_question_first_name(nq_idna));
+
+ k = dns_question_is_equal(q->question_idna, q->question_utf8);
+ if (k < 0)
+ return r;
+ 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;
+ else if (k > 0)
+ log_debug("Following UTF8 CNAME/DNAME %s → %s.", dns_question_first_name(q->question_utf8), 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;
+
+ 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_unref_candidates(q);
+ dns_query_reset_answer(q);
+
+ q->state = DNS_TRANSACTION_NULL;
+
+ return 0;
+}
+
+int dns_query_process_cname(DnsQuery *q) {
+ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *cname = NULL;
+ DnsQuestion *question;
+ DnsResourceRecord *rr;
+ int r;
+
+ assert(q);
+
+ 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);
+
+ DNS_ANSWER_FOREACH(rr, q->answer) {
+ r = dns_question_matches_rr(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return DNS_QUERY_MATCH; /* The answer matches directly, no need to follow cnames */
+
+ 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 to follow */
+
+ if (q->flags & SD_RESOLVED_NO_CNAME)
+ return -ELOOP;
+
+ if (!q->answer_authenticated)
+ q->previous_redirect_unauthenticated = true;
+
+ /* OK, let's actually follow the CNAME */
+ r = dns_query_cname_redirect(q, cname);
+ if (r < 0)
+ return r;
+
+ /* Let's see if the answer can already answer the new
+ * redirected question */
+ r = dns_query_process_cname(q);
+ if (r != DNS_QUERY_NOMATCH)
+ return r;
+
+ /* OK, it cannot, let's begin with the new query */
+ r = dns_query_go(q);
+ if (r < 0)
+ return r;
+
+ return DNS_QUERY_RESTARTED; /* We restarted the query for a new cname */
+}
+
+DnsQuestion* dns_query_question_for_protocol(DnsQuery *q, DnsProtocol protocol) {
+ assert(q);
+
+ 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->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 q->answer_authenticated && !q->previous_redirect_unauthenticated;
+}
diff --git a/src/resolve/resolved-dns-query.h b/src/resolve/resolved-dns-query.h
new file mode 100644
index 0000000..133076d
--- /dev/null
+++ b/src/resolve/resolved-dns-query.h
@@ -0,0 +1,130 @@
+/* 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;
+
+ /* 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. */
+ DnsQuery *auxiliary_for;
+ LIST_HEAD(DnsQuery, auxiliary_queries);
+ unsigned n_auxiliary_queries;
+ int auxiliary_result;
+
+ /* 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. */
+ DnsQuestion *question_idna;
+ DnsQuestion *question_utf8;
+
+ uint64_t flags;
+ int ifindex;
+
+ /* If true, the RR TTLs of the answer will be clamped by their current left validity in the cache */
+ bool clamp_ttl;
+
+ DnsTransactionState state;
+ unsigned n_cname_redirects;
+
+ LIST_HEAD(DnsQueryCandidate, candidates);
+ sd_event_source *timeout_event_source;
+
+ /* Discovered data */
+ DnsAnswer *answer;
+ int answer_rcode;
+ DnssecResult answer_dnssec_result;
+ bool answer_authenticated;
+ DnsProtocol answer_protocol;
+ int answer_family;
+ DnsSearchDomain *answer_search_domain;
+ int answer_errno; /* if state is DNS_TRANSACTION_ERRNO */
+ bool previous_redirect_unauthenticated;
+
+ /* Bus + Varlink client information */
+ sd_bus_message *bus_request;
+ Varlink *varlink_request;
+ int request_family;
+ bool request_address_valid;
+ union in_addr_union request_address;
+ unsigned block_all_complete;
+ char *request_address_string;
+
+ /* DNS stub information */
+ DnsPacket *request_dns_packet;
+ DnsStream *request_dns_stream;
+ DnsPacket *reply_dns_packet;
+ DnsStubListenerExtra *stub_listener_extra;
+
+ /* Completion callback */
+ void (*complete)(DnsQuery* q);
+ unsigned block_ready;
+
+ sd_bus_track *bus_track;
+
+ LIST_FIELDS(DnsQuery, queries);
+ LIST_FIELDS(DnsQuery, auxiliary_queries);
+};
+
+enum {
+ DNS_QUERY_MATCH,
+ DNS_QUERY_NOMATCH,
+ DNS_QUERY_RESTARTED,
+};
+
+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, 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(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);
diff --git a/src/resolve/resolved-dns-question.c b/src/resolve/resolved-dns-question.c
new file mode 100644
index 0000000..0471708
--- /dev/null
+++ b/src/resolve/resolved-dns-question.c
@@ -0,0 +1,447 @@
+/* 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"
+
+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, keys) + sizeof(DnsResourceKey*) * n);
+ if (!q)
+ return NULL;
+
+ q->n_ref = 1;
+ q->n_allocated = n;
+
+ return q;
+}
+
+static DnsQuestion *dns_question_free(DnsQuestion *q) {
+ size_t i;
+
+ assert(q);
+
+ for (i = 0; i < q->n_keys; i++)
+ dns_resource_key_unref(q->keys[i]);
+ return mfree(q);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsQuestion, dns_question, dns_question_free);
+
+int dns_question_add_raw(DnsQuestion *q, DnsResourceKey *key) {
+ /* Insert without checking for duplicates. */
+
+ assert(key);
+ assert(q);
+
+ if (q->n_keys >= q->n_allocated)
+ return -ENOSPC;
+
+ q->keys[q->n_keys++] = dns_resource_key_ref(key);
+ return 0;
+}
+
+int dns_question_add(DnsQuestion *q, DnsResourceKey *key) {
+ int r;
+
+ assert(key);
+
+ if (!q)
+ return -ENOSPC;
+
+ for (size_t i = 0; i < q->n_keys; i++) {
+ r = dns_resource_key_equal(q->keys[i], key);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return 0;
+ }
+
+ return dns_question_add_raw(q, key);
+}
+
+int dns_question_matches_rr(DnsQuestion *q, DnsResourceRecord *rr, const char *search_domain) {
+ size_t i;
+ int r;
+
+ assert(rr);
+
+ if (!q)
+ return 0;
+
+ for (i = 0; i < q->n_keys; i++) {
+ r = dns_resource_key_match_rr(q->keys[i], 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) {
+ size_t i;
+ int r;
+
+ assert(rr);
+
+ if (!q)
+ return 0;
+
+ if (!IN_SET(rr->key->type, DNS_TYPE_CNAME, DNS_TYPE_DNAME))
+ return 0;
+
+ for (i = 0; i < q->n_keys; i++) {
+ /* For a {C,D}NAME record we can never find a matching {C,D}NAME record */
+ if (!dns_type_may_redirect(q->keys[i]->type))
+ return 0;
+
+ r = dns_resource_key_match_cname_or_dname(q->keys[i], 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->keys[0]);
+ 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->keys[i]);
+
+ if (i > 0) {
+ r = dns_name_equal(dns_resource_key_name(q->keys[i]), name);
+ if (r <= 0)
+ return r;
+ }
+
+ if (!dns_type_is_valid_query(q->keys[i]->type))
+ return 0;
+ }
+
+ return 1;
+}
+
+int dns_question_contains(DnsQuestion *a, const DnsResourceKey *k) {
+ size_t j;
+ int r;
+
+ assert(k);
+
+ if (!a)
+ return 0;
+
+ for (j = 0; j < a->n_keys; j++) {
+ r = dns_resource_key_equal(a->keys[j], k);
+ if (r != 0)
+ return r;
+ }
+
+ return 0;
+}
+
+int dns_question_is_equal(DnsQuestion *a, DnsQuestion *b) {
+ size_t j;
+ 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 keys in a are also contained b, and vice versa */
+
+ for (j = 0; j < a->n_keys; j++) {
+ r = dns_question_contains(b, a->keys[j]);
+ if (r <= 0)
+ return r;
+ }
+
+ for (j = 0; j < b->n_keys; j++) {
+ r = dns_question_contains(a, b->keys[j]);
+ 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);
+ 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->keys[0]);
+}
+
+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 (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);
+ 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);
+ 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);
+ 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);
+ 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);
+ if (r < 0)
+ return r;
+ }
+
+ *ret = TAKE_PTR(q);
+
+ return 0;
+}
diff --git a/src/resolve/resolved-dns-question.h b/src/resolve/resolved-dns-question.h
new file mode 100644
index 0000000..a6444b0
--- /dev/null
+++ b/src/resolve/resolved-dns-question.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct DnsQuestion DnsQuestion;
+
+#include "macro.h"
+#include "resolved-dns-rr.h"
+
+/* A simple array of resource keys */
+
+struct DnsQuestion {
+ unsigned n_ref;
+ size_t n_keys, n_allocated;
+ DnsResourceKey* keys[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);
+int dns_question_add(DnsQuestion *q, DnsResourceKey *key);
+
+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(DnsQuestion *a, const DnsResourceKey *k);
+int dns_question_is_equal(DnsQuestion *a, DnsQuestion *b);
+
+int dns_question_cname_redirect(DnsQuestion *q, const DnsResourceRecord *cname, DnsQuestion **ret);
+
+const char *dns_question_first_name(DnsQuestion *q);
+
+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;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuestion*, dns_question_unref);
+
+#define _DNS_QUESTION_FOREACH(u, key, q) \
+ for (size_t UNIQ_T(i, u) = ({ \
+ (key) = ((q) && (q)->n_keys > 0) ? (q)->keys[0] : NULL; \
+ 0; \
+ }); \
+ (q) && (UNIQ_T(i, u) < (q)->n_keys); \
+ UNIQ_T(i, u)++, (key) = (UNIQ_T(i, u) < (q)->n_keys ? (q)->keys[UNIQ_T(i, u)] : NULL))
+
+#define DNS_QUESTION_FOREACH(key, q) _DNS_QUESTION_FOREACH(UNIQ, key, q)
diff --git a/src/resolve/resolved-dns-rr.c b/src/resolve/resolved-dns-rr.c
new file mode 100644
index 0000000..52c76ee
--- /dev/null
+++ b/src/resolve/resolved-dns-rr.c
@@ -0,0 +1,1824 @@
+/* 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 {
+ DnsResourceKey *k;
+ char *destination = NULL;
+
+ 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 mfree(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 != (unsigned) -1);
+
+ 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 != (unsigned) -1);
+ 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 (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 ret;
+
+ ret = dns_name_compare_func(dns_resource_key_name(x), dns_resource_key_name(y));
+ if (ret != 0)
+ return ret;
+
+ ret = CMP(x->type, y->type);
+ if (ret != 0)
+ return ret;
+
+ ret = CMP(x->class, y->class);
+ if (ret != 0)
+ return ret;
+
+ return 0;
+}
+
+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);
+
+ snprintf(buf, buf_size, "%s %s%s%.0u %s%s%.0u",
+ dns_resource_key_name(key),
+ strempty(c), c ? "" : "CLASS", c ? 0 : key->class,
+ strempty(t), t ? "" : "TYPE", t ? 0 : 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 == (unsigned) -1)
+ return false;
+ if ((*b)->n_ref == (unsigned) -1)
+ 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_unref(*b);
+ *b = dns_resource_key_ref(*a);
+ } else {
+ dns_resource_key_unref(*a);
+ *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 = (unsigned) -1,
+ .n_skip_labels_source = (unsigned) -1,
+ };
+
+ 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) {
+ DnsTxtItem *i;
+ 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) {
+ size_t j;
+
+ if (i != first)
+ *(p++) = ' ';
+
+ *(p++) = '"';
+
+ for (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 *t = NULL;
+ char *s, 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: {
+ _cleanup_free_ char *x = NULL;
+
+ r = in_addr_to_string(AF_INET, (const union in_addr_union*) &rr->a.in_addr, &x);
+ if (r < 0)
+ return NULL;
+
+ s = strjoin(k, " ", x);
+ 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;
+ char *ss;
+ 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(&s, "%s %u %u %s",
+ k,
+ rr->dnskey.flags,
+ rr->dnskey.protocol,
+ alg);
+ if (r < 0)
+ return NULL;
+
+ r = base64_append(&s, r,
+ rr->dnskey.key, rr->dnskey.key_size,
+ 8, columns());
+ if (r < 0)
+ return NULL;
+
+ r = asprintf(&ss, "%s\n"
+ " -- Flags:%s%s%s\n"
+ " -- Key tag: %u",
+ s,
+ 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;
+ free(s);
+ s = ss;
+
+ 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: {
+ const char *cert_usage, *selector, *matching_type;
+
+ cert_usage = tlsa_cert_usage_to_string(rr->tlsa.cert_usage);
+ selector = tlsa_selector_to_string(rr->tlsa.selector);
+ matching_type = tlsa_matching_type_to_string(rr->tlsa.matching_type);
+
+ t = hexmem(rr->sshfp.fingerprint, rr->sshfp.fingerprint_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,
+ cert_usage,
+ selector,
+ matching_type);
+ if (r < 0)
+ return NULL;
+
+ break;
+ }
+
+ case DNS_TYPE_CAA: {
+ _cleanup_free_ char *value;
+
+ value = octescape(rr->caa.value, rr->caa.value_size);
+ if (!value)
+ return NULL;
+
+ r = asprintf(&s, "%s %u %s \"%s\"%s%s%s%.0u",
+ k,
+ rr->caa.flags,
+ rr->caa.tag,
+ value,
+ 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:
+ t = hexmem(rr->generic.data, rr->generic.data_size);
+ if (!t)
+ return NULL;
+
+ /* Format as documented in RFC 3597, Section 5 */
+ r = asprintf(&s, "%s \\# %zu %s", k, rr->generic.data_size, t);
+ if (r < 0)
+ return NULL;
+ break;
+ }
+
+ rr->to_string = s;
+ return 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)
+ 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 == (unsigned) -1)
+ 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 == (unsigned) -1)
+ 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 == (unsigned) -1)
+ 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: {
+ DnsTxtItem *j;
+
+ 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;
+ }
+}
+
+static 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_equal(x, y))
+ 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;
+ 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;
+ 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_resource_record_unref(*rr);
+ *rr = new_rr;
+
+ return 1;
+}
+
+DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *i) {
+ DnsTxtItem *n;
+
+ if (!i)
+ return NULL;
+
+ n = i->items_next;
+
+ free(i);
+ return dns_txt_item_free_all(n);
+}
+
+bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b) {
+
+ if (a == b)
+ return true;
+
+ if (!a != !b)
+ return false;
+
+ if (!a)
+ return true;
+
+ if (a->length != b->length)
+ return false;
+
+ if (memcmp(a->data, b->data, a->length) != 0)
+ return false;
+
+ return dns_txt_item_equal(a->items_next, b->items_next);
+}
+
+DnsTxtItem *dns_txt_item_copy(DnsTxtItem *first) {
+ DnsTxtItem *i, *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;
+}
+
+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..59b3a70
--- /dev/null
+++ b/src/resolve/resolved-dns-rr.h
@@ -0,0 +1,341 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+
+#include "bitmap.h"
+#include "dns-type.h"
+#include "hashmap.h"
+#include "in-addr-util.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 (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 = (unsigned) -1, \
+ .class = c, \
+ .type = t, \
+ ._name = (char*) n, \
+ })
+
+struct DnsTxtItem {
+ size_t length;
+ LIST_FIELDS(DnsTxtItem, items);
+ uint8_t data[];
+};
+
+struct DnsResourceRecord {
+ unsigned n_ref;
+ DnsResourceKey *key;
+
+ char *to_string;
+
+ uint32_t ttl;
+ usec_t expiry; /* RRSIG signature expiry */
+
+ /* How many labels to strip to determine "signer" of the RRSIG (aka, the zone). -1 if not signed. */
+ unsigned 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. */
+ unsigned n_skip_labels_source;
+
+ bool unparsable:1;
+
+ bool wire_format_canonical:1;
+ 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 {
+ uint16_t priority;
+ uint16_t weight;
+ uint16_t port;
+ char *name;
+ } 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 {
+ uint16_t priority;
+ char *exchange;
+ } 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 {
+ uint8_t algorithm;
+ uint8_t fptype;
+ void *fingerprint;
+ size_t fingerprint_size;
+ } sshfp;
+
+ /* http://tools.ietf.org/html/rfc4034#section-2.1 */
+ struct {
+ uint16_t flags;
+ uint8_t protocol;
+ uint8_t algorithm;
+ void* key;
+ size_t key_size;
+ } dnskey;
+
+ /* http://tools.ietf.org/html/rfc4034#section-3.1 */
+ struct {
+ uint16_t type_covered;
+ uint8_t algorithm;
+ uint8_t labels;
+ uint32_t original_ttl;
+ uint32_t expiration;
+ uint32_t inception;
+ uint16_t key_tag;
+ char *signer;
+ void *signature;
+ size_t signature_size;
+ } 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 {
+ uint16_t key_tag;
+ uint8_t algorithm;
+ uint8_t digest_type;
+ void *digest;
+ size_t digest_size;
+ } ds;
+
+ struct {
+ uint8_t algorithm;
+ uint8_t flags;
+ uint16_t iterations;
+ void *salt;
+ size_t salt_size;
+ void *next_hashed_name;
+ size_t next_hashed_name_size;
+ Bitmap *types;
+ } nsec3;
+
+ /* https://tools.ietf.org/html/draft-ietf-dane-protocol-23 */
+ struct {
+ uint8_t cert_usage;
+ uint8_t selector;
+ uint8_t matching_type;
+ void *data;
+ size_t data_size;
+ } tlsa;
+
+ /* https://tools.ietf.org/html/rfc6844 */
+ struct {
+ uint8_t flags;
+ char *tag;
+ void *value;
+ size_t value_size;
+ } caa;
+ };
+};
+
+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);
+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);
+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);
+
+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);
+
+void dns_resource_record_hash_func(const DnsResourceRecord *i, struct siphash *state);
+
+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..509a206
--- /dev/null
+++ b/src/resolve/resolved-dns-scope.c
@@ -0,0 +1,1436 @@
+/* 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 "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_unref(s->conflict_event_source);
+
+ sd_event_source_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) {
+ assert(s);
+
+ if (s->protocol != DNS_PROTOCOL_DNS)
+ return;
+
+ if (s->link)
+ link_next_dns_server(s->link);
+ else
+ manager_next_dns_server(s->manager);
+}
+
+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, DnsPacket *p) {
+ union in_addr_union addr;
+ int ifindex = 0, r;
+ int family;
+ uint32_t mtu;
+
+ assert(s);
+ assert(p);
+ assert(p->protocol == s->protocol);
+
+ if (s->link) {
+ mtu = s->link->mtu;
+ ifindex = s->link->ifindex;
+ } else
+ mtu = manager_find_mtu(s->manager);
+
+ switch (s->protocol) {
+
+ case DNS_PROTOCOL_DNS:
+ assert(fd >= 0);
+
+ if (DNS_PACKET_QDCOUNT(p) > 1)
+ return -EOPNOTSUPP;
+
+ if (p->size > DNS_PACKET_UNICAST_SIZE_MAX)
+ return -EMSGSIZE;
+
+ if (p->size + UDP_PACKET_HEADER_SIZE > mtu)
+ return -EMSGSIZE;
+
+ r = manager_write(s->manager, fd, p);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case DNS_PROTOCOL_LLMNR:
+ assert(fd < 0);
+
+ if (DNS_PACKET_QDCOUNT(p) > 1)
+ return -EOPNOTSUPP;
+
+ if (!ratelimit_below(&s->ratelimit))
+ return -EBUSY;
+
+ family = s->family;
+
+ 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, ifindex, family, &addr, LLMNR_PORT, NULL, p);
+ if (r < 0)
+ return r;
+
+ break;
+
+ case DNS_PROTOCOL_MDNS:
+ assert(fd < 0);
+
+ if (!ratelimit_below(&s->ratelimit))
+ return -EBUSY;
+
+ family = s->family;
+
+ if (family == AF_INET) {
+ addr.in = MDNS_MULTICAST_IPV4_ADDRESS;
+ fd = manager_mdns_ipv4_fd(s->manager);
+ } else if (family == AF_INET6) {
+ addr.in6 = MDNS_MULTICAST_IPV6_ADDRESS;
+ fd = manager_mdns_ipv6_fd(s->manager);
+ } else
+ return -EAFNOSUPPORT;
+ if (fd < 0)
+ return fd;
+
+ r = manager_send(s->manager, fd, ifindex, family, &addr, MDNS_PORT, NULL, p);
+ if (r < 0)
+ return r;
+
+ break;
+
+ default:
+ return -EAFNOSUPPORT;
+ }
+
+ return 1;
+}
+
+int dns_scope_emit_udp(DnsScope *s, int fd, 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, 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 (s->link) {
+ 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;
+ }
+
+ if (ret_socket_address)
+ *ret_socket_address = sa;
+ else {
+ r = connect(fd, &sa.sa, salen);
+ if (r < 0 && errno != EINPROGRESS)
+ return -errno;
+ }
+
+ 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 accept_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;
+}
+
+DnsScopeMatch dns_scope_good_domain(
+ DnsScope *s,
+ int ifindex,
+ uint64_t flags,
+ const char *domain) {
+
+ DnsSearchDomain *d;
+
+ /* 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(domain);
+
+ /* 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, 0) & 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 */
+ if (dns_name_endswith(domain, "0.in-addr.arpa") > 0 ||
+ dns_name_equal(domain, "255.255.255.255.in-addr.arpa") > 0 ||
+ dns_name_equal(domain, "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.0.ip6.arpa") > 0)
+ return DNS_SCOPE_NO;
+
+ /* Never respond to some of the domains listed in RFC6761 */
+ if (dns_name_endswith(domain, "invalid") > 0)
+ return DNS_SCOPE_NO;
+
+ /* Never go to network for the _gateway domain, it's something special, synthesized locally. Note
+ * that we don't use is_gateway_hostname() here, since that has support for the legacy "gateway"
+ * hostname (without the prefix underscore), which we don't want to filter on all protocols. i.e. we
+ * don't want to filter "gateway" on classic DNS, since there might very well be such a host inside
+ * some search domain, and we shouldn't block that. We do filter it in LLMNR however (and on mDNS by
+ * side-effect, since it's a single-label name which mDNS doesn't accept anyway). */
+ if (dns_name_equal(domain, "_gateway") > 0)
+ return DNS_SCOPE_NO;
+
+ switch (s->protocol) {
+
+ case DNS_PROTOCOL_DNS: {
+ bool has_search_domains = false;
+ int n_best = -1;
+
+ /* 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;
+
+ /* 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;
+ }
+
+ /* See if this scope is suitable as default route. */
+ if (!dns_scope_is_default_route(s))
+ return DNS_SCOPE_NO;
+
+ /* Exclude link-local IP ranges */
+ if (dns_name_endswith(domain, "254.169.in-addr.arpa") == 0 &&
+ 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 &&
+ /* 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_MAYBE;
+
+ return DNS_SCOPE_NO;
+ }
+
+ case DNS_PROTOCOL_MDNS: {
+ DnsScopeMatch m;
+
+ m = accept_link_local_reverse_lookups(domain);
+ 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 = accept_link_local_reverse_lookups(domain);
+ 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 */
+ 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("Unknown scope protocol");
+ }
+}
+
+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 as fully qualified
+ * name, i.e. the search suffixes already appended. */
+
+ if (key->class != DNS_CLASS_IN)
+ 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);
+ }
+
+ /* 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;
+ int r;
+
+ 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;
+
+ DNS_PACKET_HEADER(p)->id = id;
+ DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS(
+ 1 /* qr */,
+ 0 /* opcode */,
+ 0 /* c */,
+ 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);
+ if (r < 0)
+ return r;
+ DNS_PACKET_HEADER(p)->ancount = htobe16(dns_answer_size(answer));
+
+ r = dns_packet_append_answer(p, soa);
+ if (r < 0)
+ return r;
+ DNS_PACKET_HEADER(p)->arcount = htobe16(dns_answer_size(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 && !in_addr_equal(AF_INET, &p->destination, (union in_addr_union*) &LLMNR_MULTICAST_IPV4_ADDRESS))
+ return;
+
+ if (p->family == AF_INET6 && !in_addr_equal(AF_INET6, &p->destination, (union in_addr_union*) &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;
+ }
+
+ assert(dns_question_size(p->question) == 1);
+ key = p->question->keys[0];
+
+ 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, bool cache_ok) {
+ DnsTransaction *t;
+
+ assert(scope);
+ assert(key);
+
+ /* Try to find an ongoing transaction that is a equal to the
+ * specified question */
+ t = hashmap_get(scope->transactions_by_key, key);
+ if (!t)
+ return NULL;
+
+ /* Refuse reusing transactions that completed based on cached
+ * data instead of a real packet, if that's requested. */
+ if (!cache_ok &&
+ IN_SET(t->state, DNS_TRANSACTION_SUCCESS, DNS_TRANSACTION_RCODE_FAILURE) &&
+ t->answer_source != DNS_TRANSACTION_NETWORK)
+ return NULL;
+
+ return t;
+}
+
+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 = userdata;
+ int r;
+
+ assert(es);
+ assert(scope);
+
+ scope->conflict_event_source = sd_event_source_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, 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) {
+ usec_t jitter;
+ 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;
+
+ random_bytes(&jitter, sizeof(jitter));
+ jitter %= LLMNR_JITTER_INTERVAL_USEC;
+
+ r = sd_event_add_time_relative(
+ scope->manager->event,
+ &scope->conflict_event_source,
+ clock_boottime_or_monotonic(),
+ jitter,
+ LLMNR_JITTER_INTERVAL_USEC,
+ 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_our_packet(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_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;
+ DnsTransaction *t;
+ DnsZoneItem *z, *i;
+ unsigned size = 0;
+ char *service_type;
+ int r;
+
+ if (!scope)
+ return 0;
+
+ if (scope->protocol != DNS_PROTOCOL_MDNS)
+ 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);
+ 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;
+
+ rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR,
+ "_services._dns-sd._udp.local");
+ rr->ptr.name = strdup(service_type);
+ 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: %m");
+
+ r = dns_answer_add(answer, rr, 0 , 0);
+ 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, 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_or_monotonic(),
+ MDNS_ANNOUNCE_DELAY,
+ MDNS_JITTER_RANGE_USEC,
+ 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;
+ DnssdTxtData *txt_data;
+ 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;
+ DnssdTxtData *txt_data;
+ 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 *domain, *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..de05c08
--- /dev/null
+++ b/src/resolve/resolved-dns-scope.h
@@ -0,0 +1,113 @@
+/* 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-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 = -1
+} 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;
+
+ bool announced:1;
+ 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 zone probing can't reuse a transaction answered from
+ * the zone or the cache), and the hashmap only tracks the
+ * most recent entry. */
+ Hashmap *transactions_by_key;
+ LIST_HEAD(DnsTransaction, transactions);
+
+ LIST_FIELDS(DnsScope, scopes);
+};
+
+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, 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, int ifindex, uint64_t flags, const char *domain);
+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);
+
+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, bool cache_ok);
+
+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..a01f3dc
--- /dev/null
+++ b/src/resolve/resolved-dns-search-domain.c
@@ -0,0 +1,196 @@
+/* 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_SERVER_SYSTEM:
+ LIST_APPEND(domains, m->search_domains, d);
+ m->n_search_domains++;
+ break;
+
+ default:
+ assert_not_reached("Unknown search domain type");
+ }
+
+ 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("Unknown search domain type");
+ }
+}
+
+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);
+}
+
+void dns_search_domain_unlink_marked(DnsSearchDomain *first) {
+ DnsSearchDomain *next;
+
+ if (!first)
+ return;
+
+ next = first->domains_next;
+
+ if (first->marked)
+ dns_search_domain_unlink(first);
+
+ dns_search_domain_unlink_marked(next);
+}
+
+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) {
+ DnsSearchDomain *d;
+ 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..ea91a4e
--- /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);
+void 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..8112374
--- /dev/null
+++ b/src/resolve/resolved-dns-server.c
@@ -0,0 +1,953 @@
+/* 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("Unknown server type");
+ }
+
+ 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("Unknown server type");
+ }
+
+ 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("Unknown server type");
+ }
+}
+
+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_or_monotonic(), &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->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 size) {
+ 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;
+
+ /* Even if we successfully receive a reply to a request announcing support for large packets,
+ that does not mean we can necessarily receive large packets. */
+ if (level == DNS_SERVER_FEATURE_LEVEL_LARGE)
+ level = DNS_SERVER_FEATURE_LEVEL_LARGE - 1;
+
+ dns_server_verified(s, level);
+
+ /* Remember the size of the largest UDP packet 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_packet_max < size)
+ s->received_udp_packet_max = size;
+}
+
+void dns_server_packet_lost(DnsServer *s, int protocol, DnsServerFeatureLevel level) {
+ assert(s);
+ assert(s->manager);
+
+ if (s->possible_feature_level == level) {
+ 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)));
+}
+
+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_or_monotonic(), &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_LARGE :
+ 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_bad_opt &&
+ s->possible_feature_level >= DNS_SERVER_FEATURE_LEVEL_EDNS0) {
+
+ /* A reply to one of our EDNS0 queries didn't carry a valid OPT RR, then downgrade to below
+ * EDNS0 levels. After all, some records generate different responses with and without OPT RR
+ * in the request. Example:
+ * https://open.nlnetlabs.nl/pipermail/dnssec-trigger/2014-November/000376.html */
+
+ 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_rrsig_missing &&
+ s->possible_feature_level >= DNS_SERVER_FEATURE_LEVEL_DO) {
+
+ /* RRSIG data was missing on a 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. */
+
+ 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 &&
+ s->possible_feature_level >= (dns_server_get_dnssec_mode(s) == DNSSEC_YES ? DNS_SERVER_FEATURE_LEVEL_LARGE : DNS_SERVER_FEATURE_LEVEL_UDP)) {
+
+ /* We lost too many UDP packets in a row, and are on a feature level of UDP or higher. 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...");
+ s->possible_feature_level--;
+
+ } else if (s->n_failed_tcp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS &&
+ s->packet_truncated &&
+ s->possible_feature_level > (dns_server_get_dnssec_mode(s) == DNSSEC_YES ? DNS_SERVER_FEATURE_LEVEL_LARGE : DNS_SERVER_FEATURE_LEVEL_UDP)) {
+
+ /* We got too many TCP connection failures in a row, we had at least one truncated packet, and
+ * are on a 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...");
+ s->possible_feature_level--;
+ }
+
+ 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;
+ 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;
+
+ if (level >= DNS_SERVER_FEATURE_LEVEL_LARGE)
+ packet_size = DNS_PACKET_UNICAST_SIZE_LARGE_MAX;
+ else
+ packet_size = server->received_udp_packet_max;
+
+ return dns_packet_append_opt(packet, packet_size, edns_do, /* include_rfc6975 = */ true, 0, NULL);
+}
+
+int dns_server_ifindex(const DnsServer *s) {
+ assert(s);
+
+ /* 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 (server->possible_feature_level < DNS_SERVER_FEATURE_LEVEL_DO)
+ return false;
+
+ if (server->packet_bad_opt)
+ return false;
+
+ if (server->packet_rrsig_missing)
+ 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;
+}
+
+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);
+}
+
+void dns_server_unlink_marked(DnsServer *first) {
+ DnsServer *next;
+
+ if (!first)
+ return;
+
+ next = first->servers_next;
+
+ if (first->marked)
+ dns_server_unlink(first);
+
+ dns_server_unlink_marked(next);
+}
+
+void dns_server_mark_all(DnsServer *first) {
+ if (!first)
+ return;
+
+ first->marked = true;
+ dns_server_mark_all(first->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) {
+ DnsServer *s;
+
+ 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;
+
+ if (s)
+ 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)
+ manager_set_dns_server(m, m->dns_servers);
+
+ 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) {
+ assert(m);
+
+ /* 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_packet_max = DNS_PACKET_UNICAST_SIZE_MAX;
+
+ s->packet_bad_opt = false;
+ s->packet_rrsig_missing = 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) {
+ DnsServer *i;
+
+ 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 packet 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",
+ s->received_udp_packet_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));
+}
+
+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_LARGE] = "UDP+EDNS0+DO+LARGE",
+ [DNS_SERVER_FEATURE_LEVEL_TLS_DO] = "TLS+EDNS0+D0",
+};
+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..20afee7
--- /dev/null
+++ b/src/resolve/resolved-dns-server.h
@@ -0,0 +1,164 @@
+/* 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 = -1
+} 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_LARGE,
+ DNS_SERVER_FEATURE_LEVEL_TLS_DO,
+ _DNS_SERVER_FEATURE_LEVEL_MAX,
+ _DNS_SERVER_FEATURE_LEVEL_INVALID = -1
+} 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_TLS(x) IN_SET(x, DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN, DNS_SERVER_FEATURE_LEVEL_TLS_DO)
+
+const char* dns_server_feature_level_to_string(int i) _const_;
+int 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_packet_max;
+
+ unsigned n_failed_udp;
+ unsigned n_failed_tcp;
+ unsigned n_failed_tls;
+
+ bool packet_truncated:1;
+ bool packet_bad_opt:1;
+ bool packet_rrsig_missing:1;
+
+ 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 size);
+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);
+
+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);
+void 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);
+
+DnssecMode dns_server_get_dnssec_mode(DnsServer *s);
+DnsOverTlsMode dns_server_get_dns_over_tls_mode(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..1aab089
--- /dev/null
+++ b/src/resolve/resolved-dns-stream.c
@@ -0,0 +1,590 @@
+/* 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 "missing_network.h"
+#include "resolved-dns-stream.h"
+#include "resolved-manager.h"
+
+#define DNS_STREAM_TIMEOUT_USEC (10 * USEC_PER_SEC)
+#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_unref(s->io_event_source);
+ s->timeout_event_source = sd_event_source_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) {
+ int 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;
+
+#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, s->local.sa.sa_family == AF_INET ? (union in_addr_union*) &s->local.in.sin_addr : (union in_addr_union*) &s->local.in6.sin6_addr);
+
+ 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)) {
+ ssize_t ss;
+ size_t i;
+
+ m = 0;
+ for (i = 0; i < iovcnt; i++) {
+ ss = dnstls_stream_write(s, iov[i].iov_base, iov[i].iov_len);
+ if (ss < 0)
+ return ss;
+
+ m += ss;
+ if (ss != (ssize_t) iov[i].iov_len)
+ continue;
+ }
+ } else
+#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 = userdata;
+
+ assert(s);
+
+ return dns_stream_complete(s, ETIMEDOUT);
+}
+
+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[2];
+ ssize_t ss;
+
+ iov[0] = IOVEC_MAKE(&s->write_size, sizeof(s->write_size));
+ iov[1] = IOVEC_MAKE(DNS_PACKET_DATA(s->write_packet), s->write_packet->size);
+
+ IOVEC_INCREMENT(iov, 2, s->n_written);
+
+ ss = dns_stream_writev(s, iov, 2, 0);
+ if (ss < 0) {
+ if (!IN_SET(-ss, EINTR, EAGAIN))
+ 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);
+ }
+ }
+
+ if ((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 (!IN_SET(-ss, EINTR, EAGAIN))
+ return dns_stream_complete(s, -ss);
+ } 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;
+
+ 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 (!IN_SET(-ss, EINTR, EAGAIN))
+ return dns_stream_complete(s, -ss);
+ } else if (ss == 0)
+ return dns_stream_complete(s, ECONNRESET);
+ else
+ s->n_read += ss;
+ }
+
+ /* Are we done? If so, disable the event source for EPOLLIN */
+ if (s->n_read >= sizeof(s->read_size) + be16toh(s->read_size)) {
+ /* If there's a packet handler
+ * installed, call that. Note that
+ * this is optional... */
+ if (s->on_packet) {
+ r = s->on_packet(s);
+ if (r < 0)
+ return r;
+ }
+
+ r = dns_stream_update_io(s);
+ if (r < 0)
+ return dns_stream_complete(s, -r);
+ }
+ }
+ }
+
+ /* Call "complete" callback if finished reading and writing one packet, and there's nothing else left
+ * to write. */
+ if (s->type == DNS_STREAM_LLMNR_SEND &&
+ (s->write_packet && s->n_written >= sizeof(s->write_size) + s->write_packet->size) &&
+ ordered_set_isempty(s->write_queue) &&
+ (s->read_packet && s->n_read >= sizeof(s->read_size) + s->read_packet->size))
+ 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_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) {
+
+ _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);
+
+ 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_or_monotonic(),
+ DNS_STREAM_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;
+
+ 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);
+}
+
+DnsPacket *dns_stream_take_read_packet(DnsStream *s) {
+ assert(s);
+
+ 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);
+}
+
+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..dba0644
--- /dev/null
+++ b/src/resolve/resolved-dns-stream.h
@@ -0,0 +1,107 @@
+/* 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"
+
+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 = -1,
+} 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;
+
+ /* only when using TCP fast open */
+ union sockaddr_union tfo_address;
+ socklen_t tfo_salen;
+
+#if ENABLE_DNS_OVER_TLS
+ DnsTlsStreamData dnstls_data;
+ int 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);
+ 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 **s, DnsStreamType type, DnsProtocol protocol, int fd, const union sockaddr_union *tfo_address);
+#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;
+}
+
+DnsPacket *dns_stream_take_read_packet(DnsStream *s);
+
+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..6a3dc99
--- /dev/null
+++ b/src/resolve/resolved-dns-stub.c
@@ -0,0 +1,776 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.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 "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 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_unref(p->udp_event_source);
+ p->tcp_event_source = sd_event_source_unref(p->tcp_event_source);
+
+ return mfree(p);
+}
+
+static int dns_stub_make_reply_packet(
+ DnsPacket **p,
+ size_t max_size,
+ DnsQuestion *q,
+ DnsAnswer *answer,
+ bool *ret_truncated) {
+
+ bool truncated = false;
+ DnsResourceRecord *rr;
+ unsigned c = 0;
+ int r;
+
+ assert(p);
+
+ /* Note that we don't bother with any additional RRs, as this is stub is for local lookups only, and hence
+ * roundtrips aren't expensive. */
+
+ if (!*p) {
+ 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 < 0)
+ return r;
+
+ DNS_PACKET_HEADER(*p)->qdcount = htobe16(dns_question_size(q));
+ }
+
+ DNS_ANSWER_FOREACH(rr, answer) {
+
+ r = dns_question_matches_rr(q, rr, NULL);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ goto add;
+
+ r = dns_question_matches_cname_or_dname(q, rr, NULL);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ goto add;
+
+ continue;
+ add:
+ r = dns_packet_append_rr(*p, rr, 0, NULL, NULL);
+ if (r == -EMSGSIZE) {
+ truncated = true;
+ break;
+ }
+ if (r < 0)
+ return r;
+
+ c++;
+ }
+
+ if (ret_truncated)
+ *ret_truncated = truncated;
+ else if (truncated)
+ return -EMSGSIZE;
+
+ DNS_PACKET_HEADER(*p)->ancount = htobe16(be16toh(DNS_PACKET_HEADER(*p)->ancount) + c);
+
+ return 0;
+}
+
+static int dns_stub_finish_reply_packet(
+ DnsPacket *p,
+ uint16_t id,
+ int rcode,
+ bool tc, /* set the Truncated 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? */
+ uint16_t max_udp_size) { /* The maximum UDP datagram size to advertise to clients */
+
+ int r;
+
+ assert(p);
+
+ if (add_opt) {
+ r = dns_packet_append_opt(p, max_udp_size, edns0_do, /* include_rfc6975 = */ false, 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 the client didn't do EDNS, clamp the rcode to 4 bit */
+ if (rcode > 0xF)
+ rcode = DNS_RCODE_SERVFAIL;
+ }
+
+ /* Don't set the AD bit unless DO is on, too */
+ if (!edns0_do)
+ ad = false;
+
+ DNS_PACKET_HEADER(p)->id = id;
+
+ DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS(
+ 1 /* qr */,
+ 0 /* opcode */,
+ 0 /* aa */,
+ tc /* tc */,
+ 1 /* rd */,
+ 1 /* ra */,
+ ad /* ad */,
+ 0 /* cd */,
+ rcode));
+
+ return 0;
+}
+
+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
+ /* 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. */
+ r = manager_send(m,
+ manager_dns_stub_fd_extra(m, l, SOCK_DGRAM),
+ l ? p->ifindex : LOOPBACK_IFINDEX, /* force loopback iface if this is the main listener stub */
+ 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_send_failure(
+ Manager *m,
+ DnsStubListenerExtra *l,
+ DnsStream *s,
+ DnsPacket *p,
+ int rcode,
+ bool authenticated) {
+
+ _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL;
+ int r;
+
+ assert(m);
+ assert(p);
+
+ r = dns_stub_make_reply_packet(&reply, DNS_PACKET_PAYLOAD_SIZE_MAX(p), p->question, NULL, NULL);
+ 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,
+ !!p->opt,
+ DNS_PACKET_DO(p),
+ authenticated,
+ l ? ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX : ADVERTISE_DATAGRAM_SIZE_MAX);
+ if (r < 0)
+ return log_debug_errno(r, "Failed to build failure packet: %m");
+
+ return dns_stub_send(m, l, s, p, reply);
+}
+
+static void dns_stub_query_complete(DnsQuery *q) {
+ int r;
+
+ assert(q);
+ assert(q->request_dns_packet);
+
+ switch (q->state) {
+
+ case DNS_TRANSACTION_SUCCESS: {
+ bool truncated;
+
+ r = dns_stub_make_reply_packet(&q->reply_dns_packet, DNS_PACKET_PAYLOAD_SIZE_MAX(q->request_dns_packet), q->question_idna, q->answer, &truncated);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to build reply packet: %m");
+ break;
+ }
+
+ if (!truncated) {
+ r = dns_query_process_cname(q);
+ if (r == -ELOOP) {
+ (void) dns_stub_send_failure(q->manager, q->stub_listener_extra, q->request_dns_stream, q->request_dns_packet, DNS_RCODE_SERVFAIL, false);
+ break;
+ }
+ if (r < 0) {
+ log_debug_errno(r, "Failed to process CNAME: %m");
+ break;
+ }
+ if (r == DNS_QUERY_RESTARTED)
+ return;
+ }
+
+ r = dns_stub_finish_reply_packet(
+ q->reply_dns_packet,
+ DNS_PACKET_ID(q->request_dns_packet),
+ q->answer_rcode,
+ truncated,
+ !!q->request_dns_packet->opt,
+ DNS_PACKET_DO(q->request_dns_packet),
+ dns_query_fully_authenticated(q),
+ q->stub_listener_extra ? ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX : ADVERTISE_DATAGRAM_SIZE_MAX);
+ if (r < 0) {
+ log_debug_errno(r, "Failed to finish reply packet: %m");
+ break;
+ }
+
+ (void) dns_stub_send(q->manager, q->stub_listener_extra, q->request_dns_stream, q->request_dns_packet, q->reply_dns_packet);
+ break;
+ }
+
+ case DNS_TRANSACTION_RCODE_FAILURE:
+ (void) dns_stub_send_failure(q->manager, q->stub_listener_extra, q->request_dns_stream, q->request_dns_packet, q->answer_rcode, dns_query_fully_authenticated(q));
+ break;
+
+ case DNS_TRANSACTION_NOT_FOUND:
+ (void) dns_stub_send_failure(q->manager, q->stub_listener_extra, q->request_dns_stream, q->request_dns_packet, DNS_RCODE_NXDOMAIN, dns_query_fully_authenticated(q));
+ 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:
+ (void) dns_stub_send_failure(q->manager, q->stub_listener_extra, q->request_dns_stream, q->request_dns_packet, DNS_RCODE_SERVFAIL, false);
+ break;
+
+ case DNS_TRANSACTION_NULL:
+ case DNS_TRANSACTION_PENDING:
+ case DNS_TRANSACTION_VALIDATING:
+ default:
+ assert_not_reached("Impossible state");
+ }
+
+ dns_query_free(q);
+}
+
+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) {
+ _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+ int r;
+
+ assert(m);
+ assert(p);
+ assert(p->protocol == DNS_PROTOCOL_DNS);
+
+ if (!l && /* l == NULL if this is the main stub */
+ (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;
+ }
+
+ 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(p->question->keys[0]->type)) {
+ log_debug("Got message with obsolete key type, refusing.");
+ dns_stub_send_failure(m, l, s, p, DNS_RCODE_NOTIMP, false);
+ return;
+ }
+
+ if (dns_type_is_zone_transer(p->question->keys[0]->type)) {
+ log_debug("Got request for zone transfer, refusing.");
+ dns_stub_send_failure(m, l, s, p, DNS_RCODE_NOTIMP, 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;
+ }
+
+ if (DNS_PACKET_DO(p) && DNS_PACKET_CD(p)) {
+ log_debug("Got request with DNSSEC CD bit set, refusing.");
+ dns_stub_send_failure(m, l, s, p, DNS_RCODE_NOTIMP, false);
+ return;
+ }
+
+ r = dns_query_new(m, &q, p->question, p->question, 0, SD_RESOLVED_PROTOCOLS_ALL|SD_RESOLVED_NO_SEARCH);
+ 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;
+ }
+
+ /* Request that the TTL is corrected by the cached time for this lookup, so that we return vaguely useful TTLs */
+ q->clamp_ttl = true;
+
+ q->request_dns_packet = dns_packet_ref(p);
+ q->request_dns_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);
+ }
+
+ 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 = userdata;
+
+ assert(l);
+
+ return on_dns_stub_packet_internal(s, fd, revents, l->manager, l);
+}
+
+static int on_dns_stub_stream_packet(DnsStream *s) {
+ _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+
+ assert(s);
+
+ p = dns_stream_take_read_packet(s);
+ 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);
+ if (r < 0) {
+ safe_close(cfd);
+ return r;
+ }
+
+ stream->stub_listener_extra = l;
+ stream->on_packet = on_dns_stub_stream_packet;
+ stream->complete = dns_stub_stream_complete;
+
+ /* 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 = userdata;
+
+ assert(l);
+ 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 manager_dns_stub_fd(Manager *m, int type) {
+ union sockaddr_union sa = {
+ .in.sin_family = AF_INET,
+ .in.sin_addr.s_addr = htobe32(INADDR_DNS_STUB),
+ .in.sin_port = htobe16(53),
+ };
+ _cleanup_close_ int fd = -1;
+ int r;
+
+ assert(IN_SET(type, SOCK_DGRAM, SOCK_STREAM));
+
+ sd_event_source **event_source = type == SOCK_DGRAM ? &m->dns_stub_udp_event_source : &m->dns_stub_tcp_event_source;
+ if (*event_source)
+ return sd_event_source_get_io_fd(*event_source);
+
+ fd = socket(AF_INET, type | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ if (fd < 0)
+ return -errno;
+
+ r = set_dns_stub_common_socket_options(fd, AF_INET);
+ if (r < 0)
+ return r;
+
+ /* 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 = setsockopt_int(fd, IPPROTO_IP, IP_TTL, 1);
+ if (r < 0)
+ return r;
+
+ if (bind(fd, &sa.sa, sizeof(sa.in)) < 0)
+ return -errno;
+
+ if (type == SOCK_STREAM &&
+ listen(fd, SOMAXCONN) < 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(IN_SET(type, SOCK_DGRAM, SOCK_STREAM));
+
+ if (!l)
+ return manager_dns_stub_fd(m, type);
+
+ 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 (l->family == AF_INET)
+ sa = (union sockaddr_union) {
+ .in.sin_family = l->family,
+ .in.sin_port = htobe16(l->port != 0 ? l->port : 53U),
+ .in.sin_addr = l->address.in,
+ };
+ else
+ sa = (union sockaddr_union) {
+ .in6.sin6_family = l->family,
+ .in6.sin6_port = htobe16(l->port != 0 ? l->port : 53U),
+ .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;
+
+ /* 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 (bind(fd, &sa.sa, SOCKADDR_LEN(sa)) < 0) {
+ r = -errno;
+ goto fail;
+ }
+
+ if (type == SOCK_STREAM &&
+ listen(fd, SOMAXCONN) < 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) {
+ const char *t = "UDP";
+ int r = 0;
+
+ assert(m);
+
+ if (m->dns_stub_listener_mode == DNS_STUB_LISTENER_NO)
+ log_debug("Not creating stub listener.");
+ else
+ 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");
+
+ if (FLAGS_SET(m->dns_stub_listener_mode, DNS_STUB_LISTENER_UDP))
+ r = manager_dns_stub_fd(m, SOCK_DGRAM);
+
+ if (r >= 0 &&
+ FLAGS_SET(m->dns_stub_listener_mode, DNS_STUB_LISTENER_TCP)) {
+ t = "TCP";
+ r = manager_dns_stub_fd(m, SOCK_STREAM);
+ }
+
+ if (IN_SET(r, -EADDRINUSE, -EPERM)) {
+ log_warning_errno(r,
+ r == -EADDRINUSE ? "Another process is already listening on %s socket 127.0.0.53:53.\n"
+ "Turning off local DNS stub support." :
+ "Failed to listen on %s socket 127.0.0.53:53: %m.\n"
+ "Turning off local DNS stub support.",
+ t);
+ manager_dns_stub_stop(m);
+ } else if (r < 0)
+ return log_error_errno(r, "Failed to listen on %s socket 127.0.0.53:53: %m", t);
+
+ 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_unref(m->dns_stub_udp_event_source);
+ m->dns_stub_tcp_event_source = sd_event_source_unref(m->dns_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..072f213
--- /dev/null
+++ b/src/resolve/resolved-dns-stub.h
@@ -0,0 +1,41 @@
+/* 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 = -1
+} 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;
+};
+
+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);
+
+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..f08d621
--- /dev/null
+++ b/src/resolve/resolved-dns-synthesize.c
@@ -0,0 +1,450 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-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);
+ if (r < 0)
+ return r;
+ }
+
+ if (IN_SET(key->type, DNS_TYPE_AAAA, DNS_TYPE_ANY)) {
+ _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);
+ 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);
+}
+
+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);
+ 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);
+ 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))
+ 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, DnsAnswer **answer) {
+ _cleanup_free_ struct local_address *addresses = NULL;
+ int n = 0, af, r;
+
+ assert(m);
+ assert(key);
+ assert(answer);
+
+ af = dns_type_to_af(key->type);
+ if (af >= 0) {
+ n = local_gateways(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 = local_gateways(m->rtnl, ifindex, AF_INET6, NULL);
+ else {
+ assert(af == AF_INET6);
+ n = local_gateways(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 (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)) {
+
+ 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, &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 ((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;
+
+ 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;
+
+ } 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..37f0ddd
--- /dev/null
+++ b/src/resolve/resolved-dns-transaction.c
@@ -0,0 +1,3258 @@
+/* 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 "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_authenticated = false;
+ t->answer_nsec_ttl = (uint32_t) -1;
+ 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) {
+ 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_unref(t->dns_udp_event_source);
+ 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_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);
+ dns_transaction_stop_timeout(t);
+
+ dns_packet_unref(t->sent);
+ dns_transaction_reset_answer(t);
+
+ dns_server_unref(t->server);
+
+ if (t->scope) {
+ hashmap_remove_value(t->scope->transactions_by_key, t->key, t);
+ 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);
+
+ return mfree(t);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsTransaction*, dns_transaction_free);
+
+bool dns_transaction_gc(DnsTransaction *t) {
+ assert(t);
+
+ if (t->block_gc > 0)
+ return true;
+
+ 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)) {
+ dns_transaction_free(t);
+ return false;
+ }
+
+ return true;
+}
+
+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;
+}
+
+int dns_transaction_new(DnsTransaction **ret, DnsScope *s, DnsResourceKey *key) {
+ _cleanup_(dns_transaction_freep) DnsTransaction *t = NULL;
+ int r;
+
+ assert(ret);
+ assert(s);
+ assert(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;
+
+ 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;
+
+ 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_t) -1,
+ .key = dns_resource_key_ref(key),
+ .current_feature_level = _DNS_SERVER_FEATURE_LEVEL_INVALID,
+ .clamp_feature_level = _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;
+ }
+
+ r = hashmap_replace(s->transactions_by_key, t->key, t);
+ if (r < 0) {
+ hashmap_remove(s->manager->dns_transactions, UINT_TO_PTR(t->id));
+ return r;
+ }
+
+ LIST_PREPEND(transactions_by_scope, s->transactions, t);
+ t->scope = s;
+
+ s->manager->n_transactions_total++;
+
+ if (ret)
+ *ret = t;
+
+ t = NULL;
+
+ 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) {
+ _cleanup_free_ char *pretty = NULL;
+ char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+ DnsZoneItem *z;
+
+ assert(t);
+ assert(p);
+
+ if (manager_our_packet(t->scope->manager, p) != 0)
+ return;
+
+ (void) in_addr_to_string(p->family, &p->sender, &pretty);
+
+ log_debug("Transaction %" PRIu16 " for <%s> on scope %s on %s/%s got tentative packet from %s.",
+ t->id,
+ dns_resource_key_to_string(t->key, 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),
+ strnull(pretty));
+
+ /* 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(t->key, 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("Transaction %" PRIu16 " for <%s> on scope %s on %s/%s now complete with <%s> from %s (%s).",
+ t->id,
+ dns_resource_key_to_string(t->key, 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),
+ t->answer_authenticated ? "authenticated" : "unsigned");
+
+ t->state = state;
+
+ dns_transaction_close_connection(t);
+ 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 = _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 != _DNS_SERVER_FEATURE_LEVEL_INVALID &&
+ t->current_feature_level > t->clamp_feature_level)
+ t->current_feature_level = t->clamp_feature_level;
+
+ 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);
+
+ 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);
+
+ r = dns_transaction_go(t);
+ if (r < 0)
+ dns_transaction_complete_errno(t, r);
+}
+
+static int dns_transaction_maybe_restart(DnsTransaction *t) {
+ int r;
+
+ assert(t);
+
+ /* 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);
+
+ 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, DnsPacket *p) {
+ assert(t);
+ assert(p);
+
+ dns_transaction_close_connection(t);
+
+ 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);
+ 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) {
+ DnsTransaction *t, *n;
+
+ LIST_FOREACH_SAFE(transactions_by_stream, t, n, s->transactions)
+ on_transaction_stream_error(t, error);
+ }
+
+ return 0;
+}
+
+static int on_stream_packet(DnsStream *s) {
+ _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+ DnsTransaction *t;
+
+ assert(s);
+
+ /* Take ownership of packet to be able to receive new packets */
+ p = dns_stream_take_read_packet(s);
+ assert(p);
+
+ t = hashmap_get(s->manager->dns_transactions, UINT_TO_PTR(DNS_PACKET_ID(p)));
+ if (t)
+ return dns_transaction_on_stream_packet(t, 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) {
+ 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) {
+ _cleanup_(dns_stream_unrefp) DnsStream *s = NULL;
+ _cleanup_close_ int fd = -1;
+ union sockaddr_union sa;
+ DnsStreamType type;
+ int r;
+
+ assert(t);
+
+ dns_transaction_close_connection(t);
+
+ switch (t->scope->protocol) {
+
+ case DNS_PROTOCOL_DNS:
+ r = dns_transaction_pick_server(t);
+ if (r < 0)
+ return r;
+
+ if (!dns_server_dnssec_supported(t->server) && dns_type_is_dnssec(t->key->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);
+
+ 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(t->key), &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);
+ 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);
+ }
+
+ s->complete = on_stream_complete;
+ s->on_packet = on_stream_packet;
+
+ /* 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);
+ 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;
+
+ /* We never cache if this packet is from the local host, under
+ * the assumption that a locally running DNS server would
+ * cache this anyway, and probably knows better when to flush
+ * the cache then we could. */
+ if (!DNS_PACKET_SHALL_CACHE(t->received))
+ return;
+
+ dns_cache_put(&t->scope->cache,
+ t->scope->manager->enable_cache,
+ t->key,
+ t->answer_rcode,
+ t->answer,
+ t->answer_authenticated,
+ t->answer_nsec_ttl,
+ 0,
+ 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;
+
+ 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.", dns_rcode_to_string(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
+ * validationr 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:
+ 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 (t->n_picked_servers < dns_scope_get_n_dns_servers(t->scope)) {
+ /* We tried fewer servers on this transaction than we know, let's try another one then */
+ dns_transaction_retry(t, true);
+ 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, t->key, flags);
+ if (r != 0)
+ return r;
+
+ r = dns_answer_find_cname_or_dname(t->answer, t->key, 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) {
+ usec_t ts;
+ 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 on transaction %" PRIu16" (rcode=%s).",
+ t->id, dns_rcode_to_string(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("Invalid DNS protocol.");
+ }
+
+ if (t->received != p) {
+ dns_packet_unref(t->received);
+ 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;
+ }
+ }
+
+ assert_se(sd_event_now(t->scope->manager->event, clock_boottime_or_monotonic(), &ts) >= 0);
+
+ switch (t->scope->protocol) {
+
+ case DNS_PROTOCOL_DNS:
+ assert(t->server);
+
+ if (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 (t->n_picked_servers < dns_scope_get_n_dns_servers(t->scope)) {
+ /* We tried fewer servers on this transaction than we know, let's try another one then */
+ dns_transaction_retry(t, true);
+ return;
+ }
+
+ /* Give up, accept the rcode */
+ log_debug("Server returned error: %s", dns_rcode_to_string(DNS_PACKET_RCODE(p)));
+ break;
+ }
+
+ /* 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 = 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 = DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN - 1;
+ break;
+ default:
+ t->clamp_feature_level = t->current_feature_level - 1;
+ }
+
+ log_debug("Server returned error %s, retrying transaction with reduced feature level %s.",
+ dns_rcode_to_string(DNS_PACKET_RCODE(p)),
+ dns_server_feature_level_to_string(t->clamp_feature_level));
+
+ 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.");
+ dns_transaction_retry(t, true /* pick a new server */);
+ return;
+ }
+
+ 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, ts - t->start_usec);
+ break;
+
+ default:
+ assert_not_reached("Invalid DNS protocol.");
+ }
+
+ 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;
+ }
+
+ log_debug("Reply truncated, retrying via TCP.");
+
+ /* Response was truncated, let's try again with good old 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 */
+ dns_transaction_retry(t, true);
+ }
+
+ return;
+ }
+
+ /* After the superficial checks, actually parse the message. */
+ r = dns_packet_extract(p);
+ if (r < 0) {
+ dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY);
+ 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 != _DNS_SERVER_FEATURE_LEVEL_INVALID)
+ dns_server_packet_rcode_downgrade(t->server, t->clamp_feature_level);
+
+ /* Report that the OPT RR was missing */
+ if (!p->opt)
+ dns_server_packet_bad_opt(t->server, t->current_feature_level);
+
+ /* Report that we successfully received a packet */
+ dns_server_packet_received(t->server, p->ipproto, t->current_feature_level, p->size);
+ }
+
+ /* 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, t->key);
+ 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 */
+ dns_answer_unref(t->answer);
+ t->answer = dns_answer_ref(p->answer);
+ t->answer_rcode = DNS_PACKET_RCODE(p);
+ t->answer_dnssec_result = _DNSSEC_RESULT_INVALID;
+ t->answer_authenticated = false;
+
+ 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);
+ 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 = userdata;
+ int r;
+
+ assert(t);
+ assert(t->scope);
+
+ r = manager_recv(t->scope->manager, fd, DNS_PROTOCOL_DNS, &p);
+ 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_or_monotonic(), &usec) >= 0);
+ dns_server_packet_lost(t->server, IPPROTO_UDP, t->current_feature_level);
+
+ dns_transaction_retry(t, true);
+ return 0;
+ }
+ if (r < 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);
+ 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 (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 (!dns_server_dnssec_supported(t->server) && dns_type_is_dnssec(t->key->type))
+ return -EOPNOTSUPP;
+
+ if (r > 0 || t->dns_udp_fd < 0) { /* Server changed, or no connection yet. */
+ int fd;
+
+ dns_transaction_close_connection(t);
+
+ 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;
+ }
+
+ r = dns_server_adjust_opt(t->server, t->sent, t->current_feature_level);
+ if (r < 0)
+ return r;
+ } else
+ dns_transaction_close_connection(t);
+
+ r = dns_scope_emit_udp(t->scope, t->dns_udp_fd, 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 = userdata;
+
+ assert(s);
+ assert(t);
+
+ if (!t->initial_jitter_scheduled || t->initial_jitter_elapsed) {
+ /* 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("Invalid DNS protocol.");
+ }
+
+ if (t->initial_jitter_scheduled)
+ t->initial_jitter_elapsed = true;
+ }
+
+ log_debug("Timeout reached on transaction %" PRIu16 ".", t->id);
+
+ dns_transaction_retry(t, true);
+ 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:
+ assert(t->n_attempts > 0);
+ if (t->probing)
+ return MDNS_PROBING_INTERVAL_USEC;
+ else
+ return (1 << (t->n_attempts - 1)) * USEC_PER_SEC;
+
+ case DNS_PROTOCOL_LLMNR:
+ return t->scope->resend_timeout;
+
+ default:
+ assert_not_reached("Invalid DNS protocol.");
+ }
+}
+
+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) {
+ r = dns_trust_anchor_lookup_positive(&t->scope->manager->trust_anchor, t->key, &t->answer);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ t->answer_rcode = DNS_RCODE_SUCCESS;
+ t->answer_source = DNS_TRANSACTION_TRUST_ANCHOR;
+ t->answer_authenticated = true;
+ dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
+ return 0;
+ }
+
+ if (dns_name_is_root(dns_resource_key_name(t->key)) &&
+ t->key->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;
+ t->answer_authenticated = false;
+ 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, but only if this transaction is not used
+ * for probing or verifying a zone item. */
+ if (set_isempty(t->notify_zone_items)) {
+
+ r = dns_zone_lookup(&t->scope->zone, t->key, 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;
+ t->answer_authenticated = true;
+ dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
+ return 0;
+ }
+ }
+
+ /* Check the cache, but only if this transaction is not used
+ * for probing or verifying a zone item. */
+ if (set_isempty(t->notify_zone_items)) {
+
+ /* 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, t->key, t->clamp_ttl, &t->answer_rcode, &t->answer, &t->answer_authenticated);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ 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;
+ }
+ }
+
+ return 1;
+}
+
+static int dns_transaction_make_packet_mdns(DnsTransaction *t) {
+ _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+ bool add_known_answers = false;
+ DnsTransaction *other;
+ DnsResourceKey *tkey;
+ _cleanup_set_free_ Set *keys = NULL;
+ unsigned qdcount;
+ unsigned nscount = 0;
+ usec_t ts;
+ 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);
+
+ r = dns_packet_new_query(&p, t->scope->protocol, 0, false);
+ if (r < 0)
+ return r;
+
+ r = dns_packet_append_key(p, t->key, 0, NULL);
+ if (r < 0)
+ return r;
+
+ qdcount = 1;
+
+ if (dns_key_is_shared(t->key))
+ add_known_answers = true;
+
+ if (t->key->type == DNS_TYPE_ANY) {
+ r = set_ensure_put(&keys, &dns_resource_key_hash_ops, t->key);
+ if (r < 0)
+ return r;
+ }
+
+ /*
+ * For mDNS, we want to coalesce as many open queries in pending transactions into one single
+ * query packet on the wire as possible. To achieve that, we iterate through all pending transactions
+ * in our current scope, and see whether their timing constraints allow them to be sent.
+ */
+
+ assert_se(sd_event_now(t->scope->manager->event, clock_boottime_or_monotonic(), &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 (qdcount >= UINT16_MAX)
+ break;
+
+ r = dns_packet_append_key(p, other->key, 0, NULL);
+
+ /*
+ * 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_transaction_prepare(other, ts);
+ if (r <= 0)
+ continue;
+
+ ts += transaction_get_resend_timeout(other);
+
+ r = sd_event_add_time(
+ other->scope->manager->event,
+ &other->timeout_event_source,
+ clock_boottime_or_monotonic(),
+ ts, 0,
+ on_transaction_timeout, other);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(other->timeout_event_source, "dns-transaction-timeout");
+
+ other->state = DNS_TRANSACTION_PENDING;
+ other->next_attempt_after = ts;
+
+ qdcount++;
+
+ if (dns_key_is_shared(other->key))
+ add_known_answers = true;
+
+ if (other->key->type == DNS_TYPE_ANY) {
+ r = set_ensure_put(&keys, &dns_resource_key_hash_ops, other->key);
+ if (r < 0)
+ return r;
+ }
+ }
+
+ DNS_PACKET_HEADER(p)->qdcount = htobe16(qdcount);
+
+ /* Append known answer section if we're asking for any shared record */
+ if (add_known_answers) {
+ r = dns_cache_export_shared_to_packet(&t->scope->cache, p);
+ if (r < 0)
+ return r;
+ }
+
+ SET_FOREACH(tkey, keys) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+ bool tentative;
+
+ r = dns_zone_lookup(&t->scope->zone, tkey, t->scope->link->ifindex, &answer, NULL, &tentative);
+ if (r < 0)
+ return r;
+
+ r = dns_packet_append_answer(p, answer);
+ if (r < 0)
+ return r;
+
+ nscount += dns_answer_size(answer);
+ }
+ DNS_PACKET_HEADER(p)->nscount = htobe16(nscount);
+
+ 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;
+
+ r = dns_packet_new_query(&p, t->scope->protocol, 0, t->scope->dnssec_mode != DNSSEC_NO);
+ if (r < 0)
+ return r;
+
+ r = dns_packet_append_key(p, t->key, 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_or_monotonic(), &ts) >= 0);
+
+ r = dns_transaction_prepare(t, ts);
+ if (r <= 0)
+ return r;
+
+ log_debug("Transaction %" PRIu16 " for <%s> scope %s on %s/%s.",
+ t->id,
+ dns_resource_key_to_string(t->key, 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));
+
+ if (!t->initial_jitter_scheduled &&
+ IN_SET(t->scope->protocol, DNS_PROTOCOL_LLMNR, DNS_PROTOCOL_MDNS)) {
+ usec_t jitter, accuracy;
+
+ /* RFC 4795 Section 2.7 suggests all queries should be
+ * delayed by a random time from 0 to JITTER_INTERVAL. */
+
+ t->initial_jitter_scheduled = true;
+
+ random_bytes(&jitter, sizeof(jitter));
+
+ switch (t->scope->protocol) {
+
+ case DNS_PROTOCOL_LLMNR:
+ jitter %= LLMNR_JITTER_INTERVAL_USEC;
+ accuracy = LLMNR_JITTER_INTERVAL_USEC;
+ break;
+
+ case DNS_PROTOCOL_MDNS:
+ jitter %= MDNS_JITTER_RANGE_USEC;
+ jitter += MDNS_JITTER_MIN_USEC;
+ accuracy = MDNS_JITTER_RANGE_USEC;
+ break;
+ default:
+ assert_not_reached("bad protocol");
+ }
+
+ r = sd_event_add_time(
+ t->scope->manager->event,
+ &t->timeout_event_source,
+ clock_boottime_or_monotonic(),
+ ts + jitter, accuracy,
+ on_transaction_timeout, t);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(t->timeout_event_source, "dns-transaction-timeout");
+
+ t->n_attempts = 0;
+ t->next_attempt_after = ts;
+ t->state = DNS_TRANSACTION_PENDING;
+
+ log_debug("Delaying %s transaction for " USEC_FMT "us.", dns_protocol_to_string(t->scope->protocol), 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(t->key), "in-addr.arpa") > 0 ||
+ dns_name_endswith(dns_resource_key_name(t->key), "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.");
+ if (IN_SET(r, -EMSGSIZE, -EAGAIN))
+ r = dns_transaction_emit_tcp(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);
+
+ return dns_transaction_go(t);
+ }
+
+ ts += transaction_get_resend_timeout(t);
+
+ r = sd_event_add_time(
+ t->scope->manager->event,
+ &t->timeout_event_source,
+ clock_boottime_or_monotonic(),
+ ts, 0,
+ on_transaction_timeout, t);
+ if (r < 0)
+ return r;
+
+ (void) sd_event_source_set_description(t->timeout_event_source, "dns-transaction-timeout");
+
+ t->state = DNS_TRANSACTION_PENDING;
+ t->next_attempt_after = ts;
+
+ 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, true);
+ if (!aux) {
+ r = dns_transaction_new(&aux, t->scope, key);
+ 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(t->key, s, sizeof s),
+ t->id,
+ dns_resource_key_to_string(aux->key, 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_unsigned_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(t->key));
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return false;
+
+ /* The answer does not contain any RRs that match to the
+ * question. If so, let's see if there are any NSEC/NSEC3 RRs
+ * included. If not, the answer is unsigned. */
+
+ r = dns_answer_contains_nsec_or_nsec3(t->answer);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return false;
+
+ return true;
+}
+
+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(t->key, rr, NULL);
+ if (r != 0)
+ return r;
+
+ return dns_resource_key_match_cname_or_dname(t->key, 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 (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(t->key), 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(t->key), 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(t->key, 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, t->key, 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(t->key), 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_unsigned_negative_answer(t);
+ if (r < 0)
+ return r;
+ if (r > 0) {
+ const char *name;
+ uint16_t type = 0;
+
+ name = dns_resource_key_name(t->key);
+
+ /* 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 (t->key->type == DNS_TYPE_DS) {
+ r = dns_name_parent(&name);
+ if (r > 0) {
+ type = DNS_TYPE_SOA;
+ log_debug("Requesting parent SOA (→ %s) to validate transaction %" PRIu16 " (%s, unsigned empty DS response).",
+ name, t->id, dns_resource_key_name(t->key));
+ } else
+ name = NULL;
+
+ } else if (IN_SET(t->key->type, DNS_TYPE_SOA, DNS_TYPE_NS)) {
+
+ type = DNS_TYPE_DS;
+ log_debug("Requesting DS (→ %s) to validate transaction %" PRIu16 " (%s, unsigned empty SOA/NS response).",
+ name, t->id, name);
+
+ } else {
+ type = DNS_TYPE_SOA;
+ log_debug("Requesting SOA (→ %s) to validate transaction %" PRIu16 " (%s, unsigned empty non-SOA/NS/DS response).",
+ name, t->id, name);
+ }
+
+ if (name) {
+ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *soa = NULL;
+
+ soa = dns_resource_key_new(t->key->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) {
+ DnsResourceRecord *rr;
+ int ifindex, 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_IFINDEX(rr, ifindex, t->answer) {
+
+ r = dnssec_verify_dnskey_by_ds_search(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, rr, ifindex, DNS_ANSWER_AUTHENTICATED);
+ 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 (dt->key->class != rr->key->class)
+ continue;
+ if (dt->key->type != DNS_TYPE_DS)
+ continue;
+
+ r = dns_name_equal(dns_resource_key_name(dt->key), 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 (!dt->answer_authenticated)
+ return false;
+
+ return dns_answer_match_key(dt->answer, dt->key, 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 (dt->key->class != rr->key->class)
+ continue;
+ if (dt->key->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(dt->key), parent);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ return t->answer_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 (dt->key->class != rr->key->class)
+ continue;
+ if (dt->key->type != DNS_TYPE_SOA)
+ continue;
+
+ r = dns_name_equal(dns_resource_key_name(dt->key), 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 t->answer_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 (dt->key->class != key->class)
+ continue;
+
+ r = dns_name_equal(dns_resource_key_name(dt->key), 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(t->key->type))
+ return -EINVAL;
+
+ r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(t->key));
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return false;
+
+ r = dns_transaction_in_private_tld(t, t->key);
+ 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(t->key, key_str, sizeof key_str));
+ return false;
+ }
+
+ name = dns_resource_key_name(t->key);
+
+ if (t->key->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(t->key->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 (dt->key->class != t->key->class)
+ continue;
+ if (dt->key->type != type)
+ continue;
+
+ r = dns_name_equal(dns_resource_key_name(dt->key), name);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ continue;
+
+ return dt->answer_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 (dt->key->class != rr->key->class)
+ continue;
+
+ if (dt->key->type == DNS_TYPE_DNSKEY) {
+
+ r = dns_name_equal(dns_resource_key_name(dt->key), 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 (dt->answer_authenticated)
+ return true;
+
+ found = true;
+
+ } else if (dt->key->type == DNS_TYPE_DS) {
+
+ r = dns_name_equal(dns_resource_key_name(dt->key), 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 (!dt->answer_authenticated)
+ return false;
+
+ return dns_answer_match_key(dt->answer, dt->key, 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 (!dt->answer_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) {
+ 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) {
+
+ 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);
+ 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);
+ 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;
+
+ /* 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);
+ 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);
+ 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);
+ 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);
+ 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,
+ DNSSEC_UNSUPPORTED_ALGORITHM)) {
+
+ 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);
+ 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 (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;
+ t->answer_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(t->key, 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_unref(t->answer);
+ 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;
+ t->answer_authenticated = true;
+ } else {
+ /* The answer is not fully authenticated. */
+ t->answer_dnssec_result = DNSSEC_UNSIGNED;
+ t->answer_authenticated = false;
+ }
+
+ } else if (r == 0) {
+ DnssecNsecResult nr;
+ bool authenticated = false;
+
+ /* Bummer! Let's check NSEC/NSEC3 */
+ r = dnssec_nsec_test(t->answer, t->key, &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;
+ t->answer_authenticated = authenticated;
+
+ manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, t->key);
+ 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;
+ t->answer_authenticated = authenticated;
+
+ manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, t->key);
+ 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;
+ t->answer_authenticated = false;
+
+ manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, t->key);
+ 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, t->key);
+ } else {
+ t->answer_dnssec_result = DNSSEC_UNSIGNED;
+ t->answer_authenticated = false;
+ manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, t->key);
+ }
+
+ 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, t->key);
+ 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, t->key);
+ break;
+
+ default:
+ assert_not_reached("Unexpected NSEC result.");
+ }
+ }
+
+ 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",
+};
+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..88b0d8e
--- /dev/null
+++ b/src/resolve/resolved-dns-transaction.h
@@ -0,0 +1,181 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-event.h"
+
+typedef struct DnsTransaction DnsTransaction;
+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_STATE_MAX,
+ _DNS_TRANSACTION_STATE_INVALID = -1
+};
+
+#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 = -1
+};
+
+struct DnsTransaction {
+ DnsScope *scope;
+
+ DnsResourceKey *key;
+
+ DnsTransactionState state;
+
+ uint16_t id;
+
+ bool tried_stream:1;
+
+ bool initial_jitter_scheduled:1;
+ bool initial_jitter_elapsed:1;
+
+ bool clamp_ttl:1;
+
+ bool probing:1;
+
+ 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 */
+
+ /* 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. */
+ bool answer_authenticated;
+
+ /* 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;
+
+ unsigned n_picked_servers;
+
+ /* 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. */
+ DnsServerFeatureLevel clamp_feature_level;
+
+ /* 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 block_gc;
+
+ LIST_FIELDS(DnsTransaction, transactions_by_scope);
+ LIST_FIELDS(DnsTransaction, transactions_by_stream);
+};
+
+int dns_transaction_new(DnsTransaction **ret, DnsScope *s, DnsResourceKey *key);
+DnsTransaction* dns_transaction_free(DnsTransaction *t);
+
+bool 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);
+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);
+
+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 Jitter interval, see RFC 6762 Section 5.2 */
+#define MDNS_JITTER_MIN_USEC (20 * USEC_PER_MSEC)
+#define MDNS_JITTER_RANGE_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..3e5d255
--- /dev/null
+++ b/src/resolve/resolved-dns-trust-anchor.c
@@ -0,0 +1,770 @@
+/* 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);
+ 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";
+
+ 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);
+ 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;
+ char **f;
+ 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, "r");
+ 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;
+ 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;
+ }
+
+ r = hashmap_replace(d->positive_by_key, new_answer->items[0].rr->key, new_answer);
+ if (r < 0)
+ return r;
+
+ new_answer = NULL;
+ 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..00eb672
--- /dev/null
+++ b/src/resolve/resolved-dns-zone.c
@@ -0,0 +1,696 @@
+/* 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 void dns_zone_item_free(DnsZoneItem *i) {
+ if (!i)
+ return;
+
+ dns_zone_item_probe_stop(i);
+ dns_resource_record_unref(i->rr);
+
+ free(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) {
+ DnsZoneItem *i;
+
+ assert(z);
+ assert(rr);
+
+ LIST_FOREACH(by_key, i, 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);
+ assert(rr);
+
+ 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)), false);
+ 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);
+ 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) {
+ DnsZoneItem *first, *j;
+ 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_FIND_HEAD(by_name, i, first);
+ LIST_FOREACH(by_name, j, first) {
+ if (i == j)
+ continue;
+
+ 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);
+}
+
+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 *j, *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;
+
+ 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 *i, *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 *i, *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) {
+ DnsZoneItem *j;
+
+ 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) {
+ DnsZoneItem *j;
+
+ 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 *i, *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..d908cc6
--- /dev/null
+++ b/src/resolve/resolved-dnssd-bus.c
@@ -0,0 +1,135 @@
+/* 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 = userdata;
+ DnssdTxtData *txt_data;
+ Manager *m;
+ Link *l;
+ int r;
+
+ assert(message);
+ assert(s);
+
+ 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 = userdata;
+ DnssdService *service;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(found);
+ assert(m);
+
+ 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 = userdata;
+ DnssdService *service;
+ unsigned c = 0;
+ int r;
+
+ assert(bus);
+ assert(path);
+ assert(m);
+ 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..2780b85
--- /dev/null
+++ b/src/resolve/resolved-dnssd-gperf.gperf
@@ -0,0 +1,24 @@
+%{
+#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..8b40639
--- /dev/null
+++ b/src/resolve/resolved-dnssd.c
@@ -0,0 +1,368 @@
+/* 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->txt);
+
+ 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(
+ filename, DNSSD_SERVICE_DIRS, dropin_dirname,
+ "Service\0",
+ config_item_perf_lookup, resolved_dnssd_gperf_lookup,
+ CONFIG_PARSE_WARN,
+ service,
+ 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 (LIST_IS_EMPTY(service->txt_data_items)) {
+ txt_data = new0(DnssdTxtData, 1);
+ if (!txt_data)
+ return log_oom();
+
+ r = dns_txt_item_new_empty(&txt_data->txt);
+ if (r < 0)
+ return r;
+
+ LIST_PREPEND(items, service->txt_data_items, txt_data);
+ txt_data = NULL;
+ }
+
+ r = hashmap_ensure_allocated(&manager->dnssd_services, &string_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_put(manager->dnssd_services, service->name, service);
+ if (r < 0)
+ return r;
+
+ service->manager = manager;
+
+ r = dnssd_update_rrs(service);
+ if (r < 0)
+ return r;
+
+ service = NULL;
+
+ return 0;
+}
+
+static int specifier_dnssd_host_name(char specifier, const void *data, const void *userdata, char **ret) {
+ DnssdService *s = (DnssdService *) userdata;
+ char *n;
+
+ assert(s);
+ assert(s->manager);
+ assert(s->manager->llmnr_hostname);
+
+ n = strdup(s->manager->llmnr_hostname);
+ if (!n)
+ return -ENOMEM;
+
+ *ret = n;
+ return 0;
+}
+
+int dnssd_render_instance_name(DnssdService *s, char **ret_name) {
+ static const Specifier specifier_table[] = {
+ { 'a', specifier_architecture, NULL },
+ { 'b', specifier_boot_id, NULL },
+ { 'B', specifier_os_build_id, NULL },
+ { 'H', specifier_dnssd_host_name, 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(s);
+ assert(s->name_template);
+
+ r = specifier_printf(s->name_template, specifier_table, s, &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);
+
+ *ret_name = TAKE_PTR(name);
+
+ return 0;
+}
+
+int dnssd_load(Manager *manager) {
+ _cleanup_strv_free_ char **files = NULL;
+ char **f;
+ 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;
+ _cleanup_free_ char *service_name = NULL;
+ _cleanup_free_ char *full_name = NULL;
+ DnssdTxtData *txt_data;
+ 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, &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->txt);
+ 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;
+}
+
+void dnssd_signal_conflict(Manager *manager, const char *name) {
+ DnssdService *s;
+ int r;
+
+ 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) {
+ log_error_errno(r, "Can't get D-BUS object path: %m");
+ return;
+ }
+
+ r = sd_bus_emit_signal(manager->bus,
+ path,
+ "org.freedesktop.resolve1.DnssdService",
+ "Conflicted",
+ NULL);
+ if (r < 0) {
+ log_error_errno(r, "Cannot emit signal: %m");
+ return;
+ }
+
+ break;
+ }
+ }
+}
diff --git a/src/resolve/resolved-dnssd.h b/src/resolve/resolved-dnssd.h
new file mode 100644
index 0000000..8fe7556
--- /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, txt);
+
+ 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(DnssdService *s, char **ret_name);
+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);
+void 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..d3edd35
--- /dev/null
+++ b/src/resolve/resolved-dnstls-gnutls.c
@@ -0,0 +1,241 @@
+/* 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 "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(gnutls_session_t, gnutls_deinit);
+
+static ssize_t dnstls_stream_writev(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_writev);
+
+ 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_write(DnsStream *stream, const char *buf, size_t count) {
+ ssize_t ss;
+
+ assert(stream);
+ assert(stream->encrypted);
+ assert(stream->dnstls_data.session);
+ assert(buf);
+
+ ss = gnutls_record_send(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_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..defddb5
--- /dev/null
+++ b/src/resolve/resolved-dnstls-openssl.c
@@ -0,0 +1,411 @@
+/* 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 "resolved-dns-stream.h"
+#include "resolved-dnstls.h"
+#include "resolved-manager.h"
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(SSL*, SSL_free);
+DEFINE_TRIVIAL_CLEANUP_FUNC(BIO*, BIO_free);
+
+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) {
+ char errbuf[256];
+
+ error = ERR_get_error();
+ ERR_error_string_n(error, errbuf, sizeof(errbuf));
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to set server name: %s", errbuf);
+ }
+ }
+
+ 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)) {
+ char errbuf[256];
+
+ ERR_error_string_n(error, errbuf, sizeof(errbuf));
+ return log_debug_errno(SYNTHETIC_ERRNO(ECONNREFUSED),
+ "Failed to invoke SSL_do_handshake: %s", errbuf);
+ }
+ }
+
+ 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 {
+ char errbuf[256];
+
+ ERR_error_string_n(error, errbuf, sizeof(errbuf));
+ log_debug("Failed to invoke SSL_shutdown, ignoring: %s", errbuf);
+ }
+ }
+
+ 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 {
+ char errbuf[256];
+
+ ERR_error_string_n(error, errbuf, sizeof(errbuf));
+ return log_debug_errno(SYNTHETIC_ERRNO(ECONNREFUSED),
+ "Failed to invoke SSL_do_handshake: %s",
+ errbuf);
+ }
+ }
+
+ 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 {
+ char errbuf[256];
+
+ ERR_error_string_n(ssl_error, errbuf, sizeof(errbuf));
+ log_debug("Failed to invoke SSL_shutdown, ignoring: %s", errbuf);
+ }
+ }
+
+ stream->dnstls_events = 0;
+ r = dnstls_flush_write_buffer(stream);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+ssize_t dnstls_stream_write(DnsStream *stream, const char *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_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 {
+ char errbuf[256];
+
+ ERR_error_string_n(error, errbuf, sizeof(errbuf));
+ log_debug("Failed to invoke SSL_write: %s", errbuf);
+ 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_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)) {
+ 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 {
+ char errbuf[256];
+
+ ERR_error_string_n(error, errbuf, sizeof(errbuf));
+ log_debug("Failed to invoke SSL_read: %s", errbuf);
+ 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..b638d61
--- /dev/null
+++ b/src/resolve/resolved-dnstls.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#if ENABLE_DNS_OVER_TLS
+
+#include <stdint.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_write(DnsStream *stream, const char *buf, size_t count);
+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..e784213
--- /dev/null
+++ b/src/resolve/resolved-etc-hosts.c
@@ -0,0 +1,384 @@
+/* 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 "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_mtime = USEC_INFINITY;
+ m->etc_hosts_ino = 0;
+ m->etc_hosts_dev = 0;
+}
+
+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_is_null(address.family, &address.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) {
+ log_warning_errno(r, "/etc/hosts:%u: hostname \"%s\" is not valid, ignoring.", nr, name);
+ continue;
+ }
+
+ if (is_localhost(name))
+ /* Suppress the "localhost" line that is often seen */
+ 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(&item->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_allocated, 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;
+}
+
+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;
+ }
+
+ 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_or_monotonic(), &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_mtime != USEC_INFINITY) {
+ 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 (timespec_load(&st.st_mtim) == m->etc_hosts_mtime &&
+ st.st_ino == m->etc_hosts_ino && st.st_dev == m->etc_hosts_dev)
+ 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_mtime = timespec_load(&st.st_mtim);
+ m->etc_hosts_ino = st.st_ino;
+ m->etc_hosts_dev = st.st_dev;
+ 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) {
+ char **n;
+
+ r = dns_answer_reserve(answer, strv_length(item->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);
+ 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);
+ 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..459b310
--- /dev/null
+++ b/src/resolve/resolved-etc-hosts.h
@@ -0,0 +1,25 @@
+/* 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;
+} EtcHostsItem;
+
+typedef struct EtcHostsItemByName {
+ char *name;
+
+ struct in_addr_data **addresses;
+ size_t n_addresses, n_allocated;
+} 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..b54fa1b
--- /dev/null
+++ b/src/resolve/resolved-gperf.gperf
@@ -0,0 +1,32 @@
+%{
+#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)
diff --git a/src/resolve/resolved-link-bus.c b/src/resolve/resolved-link-bus.c
new file mode 100644
index 0000000..6a693ff
--- /dev/null
+++ b/src/resolve/resolved-link-bus.c
@@ -0,0 +1,851 @@
+/* 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 "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 = userdata;
+
+ assert(reply);
+ assert(l);
+
+ 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 = userdata;
+ DnsServer *s;
+ int r;
+
+ assert(reply);
+ assert(l);
+
+ 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 = userdata;
+ DnsSearchDomain *d;
+ int r;
+
+ assert(reply);
+ assert(l);
+
+ 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 = userdata;
+
+ assert(reply);
+ assert(l);
+
+ /* 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 = userdata;
+ uint64_t mask;
+
+ assert(reply);
+ assert(l);
+
+ 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 = userdata;
+ const char *name;
+ int r;
+
+ assert(reply);
+ assert(l);
+
+ 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) {
+ struct in_addr_full **dns;
+ Link *l = userdata;
+ size_t n;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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;
+ }
+
+ 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;
+ }
+ }
+
+ }
+
+ dns_server_unlink_marked(l->dns_servers);
+ link_allocate_scopes(l);
+
+ (void) link_save_user(l);
+ (void) manager_write_resolv_conf(l->manager);
+ (void) manager_send_changed(l->manager, "DNS");
+
+ 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) {
+ Link *l = userdata;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 (;;) {
+ 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_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Root domain is not suitable as search domain");
+ }
+
+ 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 */
+
+ 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;
+ }
+
+ d->route_only = route_only;
+ }
+
+ r = sd_bus_message_exit_container(message);
+ if (r < 0)
+ goto clear;
+
+ dns_search_domain_unlink_marked(l->search_domains);
+
+ (void) link_save_user(l);
+ (void) manager_write_resolv_conf(l->manager);
+
+ 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 = userdata;
+ int r, b;
+
+ assert(message);
+ assert(l);
+
+ 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 */
+
+ if (l->default_route != b) {
+ l->default_route = b;
+
+ (void) link_save_user(l);
+ (void) manager_write_resolv_conf(l->manager);
+ }
+
+ 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 = userdata;
+ ResolveSupport mode;
+ const char *llmnr;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 */
+
+ l->llmnr_support = mode;
+ link_allocate_scopes(l);
+ link_add_rrs(l, false);
+
+ (void) link_save_user(l);
+
+ 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 = userdata;
+ ResolveSupport mode;
+ const char *mdns;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 */
+
+ l->mdns_support = mode;
+ link_allocate_scopes(l);
+ link_add_rrs(l, false);
+
+ (void) link_save_user(l);
+
+ 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 = userdata;
+ const char *dns_over_tls;
+ DnsOverTlsMode mode;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 */
+
+ link_set_dns_over_tls_mode(l, mode);
+
+ (void) link_save_user(l);
+
+ 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 = userdata;
+ const char *dnssec;
+ DnssecMode mode;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 */
+
+ link_set_dnssec_mode(l, mode);
+
+ (void) link_save_user(l);
+
+ 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;
+ Link *l = userdata;
+ int r;
+ char **i;
+
+ assert(message);
+ assert(l);
+
+ 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;
+ }
+
+ 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 */
+
+ set_free_free(l->dnssec_negative_trust_anchors);
+ l->dnssec_negative_trust_anchors = TAKE_PTR(ns);
+
+ (void) link_save_user(l);
+
+ 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 = userdata;
+ int r;
+
+ assert(message);
+ assert(l);
+
+ 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 */
+
+ 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 = userdata;
+ Link *link;
+ int ifindex, r;
+
+ assert(bus);
+ assert(path);
+ assert(interface);
+ assert(found);
+ assert(m);
+
+ 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 = userdata;
+ Link *link;
+ unsigned c = 0;
+
+ assert(bus);
+ assert(path);
+ assert(m);
+ 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..4fa4451
--- /dev/null
+++ b/src/resolve/resolved-link.c
@@ -0,0 +1,1432 @@
+/* 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 "parse-util.h"
+#include "resolved-link.h"
+#include "resolved-llmnr.h"
+#include "resolved-mdns.h"
+#include "socket-netlink.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);
+
+ r = hashmap_ensure_allocated(&m->links, NULL);
+ if (r < 0)
+ return r;
+
+ 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_put(m->links, INT_TO_PTR(ifindex), l);
+ if (r < 0)
+ return r;
+
+ l->manager = m;
+
+ if (ret)
+ *ret = l;
+ l = NULL;
+
+ 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_warning_errno(r, "Failed to allocate DNS scope: %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_warning_errno(r, "Failed to allocate LLMNR IPv4 scope: %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_warning_errno(r, "Failed to allocate LLMNR IPv6 scope: %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_warning_errno(r, "Failed to allocate mDNS IPv4 scope: %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_warning_errno(r, "Failed to allocate mDNS IPv6 scope: %m");
+ }
+ } else
+ l->mdns_ipv6_scope = dns_scope_free(l->mdns_ipv6_scope);
+}
+
+void link_add_rrs(Link *l, bool force_remove) {
+ LinkAddress *a;
+ 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_warning_errno(r, "Failed to add IPv4 DNS-SD services: %m");
+ }
+
+ if (l->mdns_ipv6_scope) {
+ r = dns_scope_add_dnssd_services(l->mdns_ipv6_scope);
+ if (r < 0)
+ log_warning_errno(r, "Failed to add IPv6 DNS-SD services: %m");
+ }
+
+ } else {
+
+ if (l->mdns_ipv4_scope) {
+ r = dns_scope_remove_dnssd_services(l->mdns_ipv4_scope);
+ if (r < 0)
+ log_warning_errno(r, "Failed to remove IPv4 DNS-SD services: %m");
+ }
+
+ if (l->mdns_ipv6_scope) {
+ r = dns_scope_remove_dnssd_services(l->mdns_ipv6_scope);
+ if (r < 0)
+ log_warning_errno(r, "Failed to remove IPv6 DNS-SD services: %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) {
+ r = free_and_strdup(&l->ifname, n);
+ if (r < 0)
+ return r;
+ }
+
+ link_allocate_scopes(l);
+ link_add_rrs(l, false);
+
+ 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;
+ char **nameserver;
+ 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);
+
+ r = sd_network_link_get_llmnr(l->ifindex, &b);
+ if (r == -ENODATA) {
+ r = 0;
+ goto clear;
+ }
+ if (r < 0)
+ goto clear;
+
+ l->llmnr_support = resolve_support_from_string(b);
+ if (l->llmnr_support < 0) {
+ r = -EINVAL;
+ goto clear;
+ }
+
+ return 0;
+
+clear:
+ l->llmnr_support = RESOLVE_SUPPORT_YES;
+ return r;
+}
+
+static int link_update_mdns_support(Link *l) {
+ _cleanup_free_ char *b = NULL;
+ int r;
+
+ assert(l);
+
+ r = sd_network_link_get_mdns(l->ifindex, &b);
+ if (r == -ENODATA) {
+ r = 0;
+ goto clear;
+ }
+ if (r < 0)
+ goto clear;
+
+ l->mdns_support = resolve_support_from_string(b);
+ if (l->mdns_support < 0) {
+ r = -EINVAL;
+ goto clear;
+ }
+
+ return 0;
+
+clear:
+ l->mdns_support = RESOLVE_SUPPORT_NO;
+ return r;
+}
+
+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_warning("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;
+}
+
+static int link_update_dns_over_tls_mode(Link *l) {
+ _cleanup_free_ char *b = NULL;
+ int r;
+
+ assert(l);
+
+ r = sd_network_link_get_dns_over_tls(l->ifindex, &b);
+ if (r == -ENODATA) {
+ r = 0;
+ goto clear;
+ }
+ if (r < 0)
+ goto clear;
+
+ l->dns_over_tls_mode = dns_over_tls_mode_from_string(b);
+ if (l->dns_over_tls_mode < 0) {
+ r = -EINVAL;
+ goto clear;
+ }
+
+ return 0;
+
+clear:
+ l->dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID;
+ return r;
+}
+
+void link_set_dnssec_mode(Link *l, DnssecMode mode) {
+
+ assert(l);
+
+#if ! HAVE_GCRYPT
+ if (IN_SET(mode, DNSSEC_YES, DNSSEC_ALLOW_DOWNGRADE))
+ log_warning("DNSSEC option for the link cannot be enabled or set to allow-downgrade when systemd-resolved is built without gcrypt support. Turning off DNSSEC support.");
+ return;
+#endif
+
+ if (l->dnssec_mode == mode)
+ return;
+
+ if ((l->dnssec_mode == _DNSSEC_MODE_INVALID) ||
+ (l->dnssec_mode == DNSSEC_NO && mode != DNSSEC_NO) ||
+ (l->dnssec_mode == DNSSEC_ALLOW_DOWNGRADE && mode == DNSSEC_YES)) {
+
+ /* When switching from non-DNSSEC mode to DNSSEC mode, flush the cache. Also when switching from the
+ * allow-downgrade mode to full DNSSEC mode, flush it too. */
+ if (l->unicast_scope)
+ dns_cache_flush(&l->unicast_scope->cache);
+ }
+
+ l->dnssec_mode = mode;
+}
+
+static int link_update_dnssec_mode(Link *l) {
+ _cleanup_free_ char *m = NULL;
+ DnssecMode mode;
+ int r;
+
+ assert(l);
+
+ r = sd_network_link_get_dnssec(l->ifindex, &m);
+ if (r == -ENODATA) {
+ r = 0;
+ goto clear;
+ }
+ if (r < 0)
+ goto clear;
+
+ mode = dnssec_mode_from_string(m);
+ if (mode < 0) {
+ r = -EINVAL;
+ goto clear;
+ }
+
+ link_set_dnssec_mode(l, mode);
+
+ return 0;
+
+clear:
+ l->dnssec_mode = _DNSSEC_MODE_INVALID;
+ return r;
+}
+
+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);
+
+ r = sd_network_link_get_dnssec_negative_trust_anchors(l->ifindex, &ntas);
+ if (r == -ENODATA) {
+ r = 0;
+ goto clear;
+ }
+ if (r < 0)
+ goto clear;
+
+ ns = set_new(&dns_name_hash_ops);
+ if (!ns)
+ return -ENOMEM;
+
+ r = set_put_strdupv(&ns, ntas);
+ if (r < 0)
+ return r;
+
+ set_free_free(l->dnssec_negative_trust_anchors);
+ l->dnssec_negative_trust_anchors = TAKE_PTR(ns);
+
+ return 0;
+
+clear:
+ l->dnssec_negative_trust_anchors = set_free_free(l->dnssec_negative_trust_anchors);
+ return r;
+}
+
+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;
+ char **i;
+ 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", "unmanaged");
+}
+
+static void link_read_settings(Link *l) {
+ int r;
+
+ assert(l);
+
+ /* Read settings from networkd, except when networkd is not managing this interface. */
+
+ r = link_is_managed(l);
+ if (r < 0) {
+ log_link_warning_errno(l, r, "Failed to determine whether the interface is managed: %m");
+ return;
+ }
+ if (r == 0) {
+
+ /* 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;
+ return;
+ }
+
+ l->is_managed = true;
+
+ 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) {
+ _cleanup_free_ char *state = NULL;
+ LinkAddress *a;
+
+ 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))
+ return false;
+
+ if ((l->flags & (IFF_UP|IFF_LOWER_UP)) != (IFF_UP|IFF_LOWER_UP))
+ return false;
+
+ if (local_multicast) {
+ if ((l->flags & IFF_MULTICAST) != IFF_MULTICAST)
+ return false;
+ }
+
+ /* Check kernel operstate
+ * https://www.kernel.org/doc/Documentation/networking/operstates.txt */
+ if (!IN_SET(l->operstate, IF_OPER_UNKNOWN, IF_OPER_UP))
+ return false;
+
+ (void) sd_network_link_get_operational_state(l->ifindex, &state);
+ if (state && !STR_IN_SET(state, "unknown", "degraded", "degraded-carrier", "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) {
+ LinkAddress *a;
+
+ assert(l);
+
+ 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_debug("Switching to DNS server %s for interface %s.", strna(dns_server_string_full(s)), l->ifname);
+
+ 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) {
+ assert(l);
+
+ 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;
+ }
+
+ 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,
+ };
+
+ 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_warning_errno(r, "Failed to add A record to LLMNR zone: %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_warning_errno(r, "Failed to add IPv4 PTR record to LLMNR zone: %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_warning_errno(r, "Failed to add A record to MDNS zone: %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_warning_errno(r, "Failed to add IPv4 PTR record to MDNS zone: %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_warning_errno(r, "Failed to add AAAA record to LLMNR zone: %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_warning_errno(r, "Failed to add IPv6 PTR record to LLMNR zone: %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_warning_errno(r, "Failed to add AAAA record to MDNS zone: %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_warning_errno(r, "Failed to add IPv6 PTR record to MDNS zone: %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_debug_errno(r, "Failed to update address RRs: %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;
+
+ 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);
+
+ if (l->default_route >= 0)
+ fprintf(f, "DEFAULT_ROUTE=%s\n", yes_no(l->default_route));
+
+ if (l->dns_servers) {
+ DnsServer *server;
+
+ 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) {
+ DnsSearchDomain *domain;
+
+ 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_error_errno(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,
+ *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,
+ "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);
+
+ 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_debug_errno(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_debug_errno(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_error_errno(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..26b0d13
--- /dev/null
+++ b/src/resolve/resolved-link.h
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-netlink.h"
+
+#include "in-addr-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 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;
+
+ 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);
+
+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..2ddf088
--- /dev/null
+++ b/src/resolve/resolved-llmnr.c
@@ -0,0 +1,450 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.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_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_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_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_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_supported()) {
+ 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 = userdata;
+ DnsScope *scope;
+ int r;
+
+ assert(s);
+ assert(fd >= 0);
+ assert(m);
+
+ r = manager_recv(m, fd, DNS_PROTOCOL_LLMNR, &p);
+ if (r <= 0)
+ return r;
+
+ if (manager_our_packet(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);
+
+ } 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;
+}
+
+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");
+
+ /* RFC 4795, section 2.5 recommends setting the TTL of UDP packets to 255. */
+ r = setsockopt_int(s, IPPROTO_IP, IP_TTL, 255);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_TTL: %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");
+
+ r = setsockopt_int(s, IPPROTO_IP, IP_PKTINFO, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_PKTINFO: %m");
+
+ r = setsockopt_int(s, IPPROTO_IP, IP_RECVTTL, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv4(UDP): 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, "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 = setsockopt_int(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, 255);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_UNICAST_HOPS: %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");
+
+ r = setsockopt_int(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_RECVPKTINFO: %m");
+
+ r = setsockopt_int(s, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_RECVHOPLIMIT: %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) {
+ _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+ DnsScope *scope;
+
+ assert(s);
+
+ p = dns_stream_take_read_packet(s);
+ 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.");
+
+ dns_stream_unref(s);
+ 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;
+ }
+
+ r = dns_stream_new(m, &stream, DNS_STREAM_LLMNR_RECV, DNS_PROTOCOL_LLMNR, cfd, NULL);
+ if (r < 0) {
+ safe_close(cfd);
+ return r;
+ }
+
+ stream->on_packet = on_llmnr_stream_packet;
+ /* We don't configure a "complete" handler here, we rely on the default handler than simply drops the
+ * reference to the stream, thus freeing it */
+ 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");
+
+ /* RFC 4795, section 2.5. requires setting the TTL of TCP streams to 1 */
+ r = setsockopt_int(s, IPPROTO_IP, IP_TTL, 1);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set IP_TTL: %m");
+
+ r = setsockopt_int(s, IPPROTO_IP, IP_PKTINFO, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set IP_PKTINFO: %m");
+
+ r = setsockopt_int(s, IPPROTO_IP, IP_RECVTTL, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv4(TCP): 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, "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);
+ 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");
+
+ /* RFC 4795, section 2.5. requires setting the TTL of TCP streams to 1 */
+ r = setsockopt_int(s, IPPROTO_IPV6, IPV6_UNICAST_HOPS, 1);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set IPV6_UNICAST_HOPS: %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 = setsockopt_int(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set IPV6_RECVPKTINFO: %m");
+
+ r = setsockopt_int(s, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, true);
+ if (r < 0)
+ return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set IPV6_RECVHOPLIMIT: %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);
+ 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..7690eac
--- /dev/null
+++ b/src/resolve/resolved-manager.c
@@ -0,0 +1,1551 @@
+/* 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 "fd-util.h"
+#include "fileio.h"
+#include "hostname-util.h"
+#include "idn-util.h"
+#include "io-util.h"
+#include "missing_network.h"
+#include "netlink-util.h"
+#include "network-internal.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-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 = userdata;
+ uint16_t type;
+ Link *l;
+ int ifindex, r;
+
+ assert(rtnl);
+ assert(m);
+ 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 = userdata;
+ union in_addr_union address;
+ uint16_t type;
+ int r, ifindex, family;
+ LinkAddress *a;
+ Link *l;
+
+ assert(rtnl);
+ assert(mm);
+ assert(m);
+
+ 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;
+ sd_netlink_message *i;
+ 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_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(m->rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (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_request_dump(req, true);
+ if (r < 0)
+ return r;
+
+ r = sd_netlink_call(m->rtnl, req, 0, &reply);
+ if (r < 0)
+ return r;
+
+ for (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 = userdata;
+ Link *l;
+ int r;
+
+ assert(m);
+
+ 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 determine_hostname(char **full_hostname, char **llmnr_hostname, char **mdns_hostname) {
+ _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;
+
+ assert(full_hostname);
+ assert(llmnr_hostname);
+ assert(mdns_hostname);
+
+ /* Extract and normalize the first label of the locally configured hostname, and check it's not "localhost". */
+
+ 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_error_errno(r, "Failed to unescape hostname: %m");
+ if (r == 0)
+ return log_error_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_error_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_error_errno(k, "Failed to undo IDNA: %m");
+ if (k > 0)
+ r = k;
+
+ if (!utf8_is_valid(label))
+ return log_error_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_error_errno(r, "Failed to escape hostname: %m");
+
+ if (is_localhost(n))
+ return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+ "System hostname is 'localhost', ignoring.");
+
+ 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 const 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 compiled in hostname. In this case, let's revert to "linux" instead. */
+
+ if (is_localhost(FALLBACK_HOSTNAME))
+ return "linux";
+
+ return FALLBACK_HOSTNAME;
+}
+
+static int make_fallback_hostnames(char **full_hostname, char **llmnr_hostname, char **mdns_hostname) {
+ _cleanup_free_ char *n = NULL, *m = NULL;
+ char label[DNS_LABEL_MAX], *h;
+ const char *p;
+ int r;
+
+ assert(full_hostname);
+ assert(llmnr_hostname);
+ assert(mdns_hostname);
+
+ p = fallback_hostname();
+ 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");
+
+ h = strdup(fallback_hostname());
+ if (!h)
+ return log_oom();
+
+ *llmnr_hostname = TAKE_PTR(n);
+ *mdns_hostname = TAKE_PTR(m);
+
+ *full_hostname = 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 = userdata;
+ bool llmnr_hostname_changed;
+ int r;
+
+ assert(m);
+
+ r = determine_hostname(&full_hostname, &llmnr_hostname, &mdns_hostname);
+ if (r < 0)
+ 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_hostname(&m->full_hostname, &m->llmnr_hostname, &m->mdns_hostname);
+ if (r < 0) {
+ log_info("Defaulting to hostname '%s'.", fallback_hostname());
+
+ 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 = userdata;
+ DnsServer *server;
+ size_t size = 0;
+ DnsScope *scope;
+ Link *l;
+
+ assert(s);
+ assert(si);
+ assert(m);
+
+ 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 = userdata;
+
+ assert(s);
+ assert(si);
+ assert(m);
+
+ manager_flush_caches(m);
+
+ return 0;
+}
+
+static int manager_sigrtmin1(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+ Manager *m = userdata;
+
+ assert(s);
+ assert(si);
+ assert(m);
+
+ 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,
+ .etc_hosts_mtime = USEC_INFINITY,
+ .etc_hosts_ino = 0,
+ .etc_hosts_dev = 0,
+ .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_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);
+
+ 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);
+
+ manager_llmnr_stop(m);
+ manager_mdns_stop(m);
+ manager_dns_stub_stop(m);
+ manager_varlink_done(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);
+
+ sd_event_unref(m->event);
+
+ 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);
+
+ 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 (IN_SET(l, -EAGAIN, -EINTR))
+ return 0;
+ if (l <= 0)
+ return l;
+
+ 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;
+
+ 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;
+
+ }
+ } 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;
+ }
+ }
+ }
+
+ /* 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);
+ }
+
+ *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);
+}
+
+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);
+
+ log_debug("Sending %s%s packet with id %" PRIu16 " on interface %i/%s of size %zu.",
+ DNS_PACKET_TC(p) ? "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 */
+
+ HASHMAP_FOREACH(l, m->links) {
+ if (l->mtu <= 0)
+ continue;
+
+ if (mtu <= 0 || l->mtu < mtu)
+ mtu = l->mtu;
+ }
+
+ return mtu;
+}
+
+int manager_find_ifindex(Manager *m, int family, const union in_addr_union *in_addr) {
+ LinkAddress *a;
+
+ assert(m);
+
+ 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);
+
+ 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, true);
+ 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 (!strchr(DIGITS, 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);
+
+ HASHMAP_FOREACH(l, m->links) {
+ LinkAddress *a;
+
+ a = link_find_address(l, family, in_addr);
+ if (a)
+ return a;
+ }
+
+ return NULL;
+}
+
+bool manager_our_packet(Manager *m, DnsPacket *p) {
+ assert(m);
+ assert(p);
+
+ return !!manager_find_link_address(m, p->family, &p->sender);
+}
+
+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) {
+ DnsScope *s;
+
+ 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) {
+ DnsServer *s;
+ 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) {
+ DnsSearchDomain *d;
+ 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) {
+ DnsScope *scope;
+
+ assert(m);
+
+ LIST_FOREACH(scopes, scope, m->dns_scopes)
+ dns_cache_flush(&scope->cache);
+
+ log_info("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;
+ struct dirent *de;
+
+ 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;
+}
diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h
new file mode 100644
index 0000000..20afab0
--- /dev/null
+++ b/src/resolve/resolved-manager.h
@@ -0,0 +1,195 @@
+/* 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"
+
+#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;
+ 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;
+
+ 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;
+
+ /* DNS-SD */
+ Hashmap *dnssd_services;
+
+ sd_event_source *mdns_ipv4_event_source;
+ sd_event_source *mdns_ipv6_event_source;
+
+ /* 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, etc_hosts_mtime;
+ ino_t etc_hosts_ino;
+ dev_t etc_hosts_dev;
+ 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;
+
+ Hashmap *polkit_registry;
+
+ VarlinkServer *varlink_server;
+};
+
+/* 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_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_our_packet(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);
+void manager_reset_server_features(Manager *m);
+
+void manager_cleanup_saved_user(Manager *m);
+
+bool manager_next_dnssd_names(Manager *m);
diff --git a/src/resolve/resolved-mdns.c b/src/resolve/resolved-mdns.c
new file mode 100644
index 0000000..a093747
--- /dev/null
+++ b/src/resolve/resolved-mdns.c
@@ -0,0 +1,482 @@
+/* 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 & (x))
+
+void manager_mdns_stop(Manager *m) {
+ assert(m);
+
+ m->mdns_ipv4_event_source = sd_event_source_unref(m->mdns_ipv4_event_source);
+ m->mdns_ipv4_fd = safe_close(m->mdns_ipv4_fd);
+
+ m->mdns_ipv6_event_source = sd_event_source_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_supported()) {
+ 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;
+ unsigned n = 0, size = 0;
+ int r;
+
+ assert(p);
+ assert(key);
+ assert(ret_rrs);
+ assert_return(DNS_PACKET_NSCOUNT(p) > 0, -EINVAL);
+
+ for (size_t i = DNS_PACKET_ANCOUNT(p); i < (DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)); i++) {
+ r = dns_resource_key_match_rr(key, p->answer->items[i].rr, NULL);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ size++;
+ }
+
+ if (size == 0)
+ return 0;
+
+ list = new(DnsResourceRecord *, size);
+ if (!list)
+ return -ENOMEM;
+
+ for (size_t i = DNS_PACKET_ANCOUNT(p); i < (DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)); i++) {
+ r = dns_resource_key_match_rr(key, p->answer->items[i].rr, NULL);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ list[n++] = p->answer->items[i].rr;
+ }
+ 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;
+
+ assert(r > 0);
+
+ if (proposed_rrs_cmp(remote, r, our, size) > 0)
+ return 1;
+
+ return 0;
+}
+
+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;
+ 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");
+
+ assert_return((dns_question_size(p->question) > 0), -EINVAL);
+
+ DNS_QUESTION_FOREACH(key, p->question) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL;
+
+ 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;
+ }
+ }
+
+ r = dns_answer_extend(&full_answer, answer);
+ 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, 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;
+
+ r = dns_scope_emit_udp(s, -1, 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_our_packet(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 = dns_resource_key_name(rr->key);
+ DnsTransaction *t;
+
+ /* If the received reply packet contains ANY record that is not .local or .in-addr.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, "local") > 0))
+ return 0;
+
+ if (rr->ttl == 0) {
+ log_debug("Got a goodbye packet");
+ /* See the section 10.1 of RFC6762 */
+ rr->ttl = 1;
+ }
+
+ t = dns_scope_find_transaction(scope, rr->key, false);
+ if (t)
+ dns_transaction_process_reply(t, p);
+
+ /* Also look for the various types of ANY transactions */
+ t = dns_scope_find_transaction(scope, &DNS_RESOURCE_KEY_CONST(rr->key->class, DNS_TYPE_ANY, dns_resource_key_name(rr->key)), false);
+ if (t)
+ dns_transaction_process_reply(t, p);
+
+ t = dns_scope_find_transaction(scope, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_ANY, rr->key->type, dns_resource_key_name(rr->key)), false);
+ if (t)
+ dns_transaction_process_reply(t, p);
+
+ t = dns_scope_find_transaction(scope, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_ANY, DNS_TYPE_ANY, dns_resource_key_name(rr->key)), false);
+ if (t)
+ dns_transaction_process_reply(t, p);
+ }
+
+ dns_cache_put(&scope->cache, scope->manager->enable_cache, NULL, DNS_PACKET_RCODE(p), p->answer, false, (uint32_t) -1, 0, 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");
+
+ 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 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, "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");
+
+ 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..fce5c9b
--- /dev/null
+++ b/src/resolve/resolved-resolv-conf.c
@@ -0,0 +1,429 @@
+/* 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 &&
+ st.st_dev == own.st_dev &&
+ st.st_ino == own.st_ino)
+ 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) {
+ const char *path;
+
+ 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 &&
+ st->st_dev == own.st_dev &&
+ st->st_ino == own.st_ino)
+ 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;
+ }
+
+ /* 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 file is managed by man:systemd-resolved(8). Do not edit.\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 file is managed by man:systemd-resolved(8). Do not edit.\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_free_ 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: %m");
+
+ r = manager_compile_search_domains(m, &domains, false);
+ if (r < 0)
+ return log_warning_errno(r, "Failed to compile list of search domains: %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: %m", PRIVATE_UPLINK_RESOLV_CONF);
+
+ (void) fchmod(fileno(f_uplink), 0644);
+
+ r = write_uplink_resolv_conf_contents(f_uplink, dns, domains);
+ if (r < 0) {
+ log_error_errno(r, "Failed to write new %s: %m", PRIVATE_UPLINK_RESOLV_CONF);
+ goto fail;
+ }
+
+ 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) {
+ log_warning_errno(r, "Failed to open new %s for writing: %m", PRIVATE_STUB_RESOLV_CONF);
+ goto fail;
+ }
+
+ (void) fchmod(fileno(f_stub), 0644);
+
+ r = write_stub_resolv_conf_contents(f_stub, dns, domains);
+ if (r < 0) {
+ log_error_errno(r, "Failed to write new %s: %m", PRIVATE_STUB_RESOLV_CONF);
+ goto fail;
+ }
+
+ if (rename(temp_path_stub, PRIVATE_STUB_RESOLV_CONF) < 0)
+ r = log_error_errno(errno, "Failed to move new %s into place: %m", PRIVATE_STUB_RESOLV_CONF);
+
+ } else {
+ r = symlink_atomic_label(basename(PRIVATE_UPLINK_RESOLV_CONF), PRIVATE_STUB_RESOLV_CONF);
+ if (r < 0)
+ log_error_errno(r, "Failed to symlink %s: %m", PRIVATE_STUB_RESOLV_CONF);
+ }
+
+ if (rename(temp_path_uplink, PRIVATE_UPLINK_RESOLV_CONF) < 0)
+ r = log_error_errno(errno, "Failed to move new %s into place: %m", PRIVATE_UPLINK_RESOLV_CONF);
+
+ fail:
+ if (r < 0) {
+ /* Something went wrong, perform cleanup... */
+ (void) unlink(temp_path_uplink);
+ (void) unlink(temp_path_stub);
+ }
+
+ 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 (system_st.st_dev == our_st.st_dev &&
+ system_st.st_ino == our_st.st_ino)
+ 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..3734e28
--- /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 = -1,
+} 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-varlink.c b/src/resolve/resolved-varlink.c
new file mode 100644
index 0000000..70d6f90
--- /dev/null
+++ b/src/resolve/resolved-varlink.c
@@ -0,0 +1,544 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#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_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("Impossible state");
+ }
+}
+
+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 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 the NO_CNAME flag 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|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 *q) {
+ _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
+ _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
+ _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(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_RESTARTED) /* This was a cname, and the query was restarted. */
+ 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("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(SD_RESOLVED_FLAGS_MAKE(q->answer_protocol, q->answer_family, dns_query_fully_authenticated(q))))));
+finish:
+ if (r < 0) {
+ log_error_errno(r, "Failed to send hostname reply: %m");
+ r = varlink_error_errno(q->varlink_request, r);
+ }
+
+ dns_query_free(q);
+}
+
+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)))));
+}
+
+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,
+ };
+ Manager *m = userdata;
+ DnsQuery *q;
+ int r;
+
+ assert(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, 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)
+ goto fail;
+
+ return 1;
+
+fail:
+ dns_query_free(q);
+ return r;
+}
+
+static int json_dispatch_address(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+ LookupParameters *p = userdata;
+ union in_addr_union buf = {};
+ JsonVariant *i;
+ size_t n, k = 0;
+
+ assert(variant);
+ assert(p);
+
+ 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) {
+ intmax_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…255.", k, strna(name));
+
+ buf.bytes[k++] = (uint8_t) b;
+ }
+
+ p->address = buf;
+ p->address_size = k;
+
+ return 0;
+}
+
+static void vl_method_resolve_address_complete(DnsQuery *q) {
+ _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
+ 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(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_RESTARTED) /* This was a cname, and the query was restarted. */
+ 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("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(SD_RESOLVED_FLAGS_MAKE(q->answer_protocol, q->answer_family, dns_query_fully_authenticated(q))))));
+finish:
+ if (r < 0) {
+ log_error_errno(r, "Failed to send address reply: %m");
+ r = varlink_error_errno(q->varlink_request, r);
+ }
+
+ dns_query_free(q);
+}
+
+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,
+ };
+ Manager *m = userdata;
+ DnsQuery *q;
+ int r;
+
+ assert(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_UNSPEC, 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.UserDatabase.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, 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)
+ goto fail;
+
+ return 1;
+
+fail:
+ dns_query_free(q);
+ return r;
+}
+
+int manager_varlink_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;
+}
+
+void manager_varlink_done(Manager *m) {
+ assert(m);
+
+ m->varlink_server = varlink_server_unref(m->varlink_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..fd9be30
--- /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.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;
+ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
+ int r;
+
+ log_setup_service();
+
+ 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 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(0, 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..93279b3
--- /dev/null
+++ b/src/resolve/resolved.conf.in
@@ -0,0 +1,30 @@
+# 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.
+# You can change settings by editing this file.
+# Defaults can be restored by simply deleting this file.
+#
+# 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 1.0.0.1 2606:4700:4700::1111 2606:4700:4700::1001
+# Google: 8.8.8.8 8.8.4.4 2001:4860:4860::8888 2001:4860:4860::8844
+# Quad9: 9.9.9.9 2620:fe::fe
+#DNS=
+#FallbackDNS=@DNS_SERVERS@
+#Domains=
+#DNSSEC=@DEFAULT_DNSSEC_MODE@
+#DNSOverTLS=@DEFAULT_DNS_OVER_TLS_MODE@
+#MulticastDNS=@DEFAULT_MDNS_MODE@
+#LLMNR=@DEFAULT_LLMNR_MODE@
+#Cache=yes
+#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..47c7d67
--- /dev/null
+++ b/src/resolve/test-dns-packet.c
@@ -0,0 +1,120 @@
+/* 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);
+ }
+}
+
+int main(int argc, char **argv) {
+ int i, 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 (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("");
+ }
+
+ 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..e21eb86
--- /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..0275d0e
--- /dev/null
+++ b/src/resolve/test-dnssec.c
@@ -0,0 +1,623 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <arpa/inet.h>
+#if HAVE_GCRYPT
+#include <gcrypt.h>
+#endif
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include "alloc-util.h"
+#include "resolved-dns-dnssec.h"
+#include "resolved-dns-rr.h"
+#include "string-util.h"
+#include "hexdecoct.h"
+
+#if HAVE_GCRYPT
+
+static void test_dnssec_verify_dns_key(void) {
+
+ 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);
+}
+
+static void test_dnssec_verify_rfc8080_ed25519_example1(void) {
+ 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) >= 0);
+
+ assert_se(dnssec_verify_rrset(answer, mx->key, rrsig, dnskey,
+ rrsig->rrsig.inception * USEC_PER_SEC, &result) >= 0);
+#if GCRYPT_VERSION_NUMBER >= 0x010600
+ assert_se(result == DNSSEC_VALIDATED);
+#else
+ assert_se(result == DNSSEC_UNSUPPORTED_ALGORITHM);
+#endif
+}
+
+static void test_dnssec_verify_rfc8080_ed25519_example2(void) {
+ 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) >= 0);
+
+ assert_se(dnssec_verify_rrset(answer, mx->key, rrsig, dnskey,
+ rrsig->rrsig.inception * USEC_PER_SEC, &result) >= 0);
+#if GCRYPT_VERSION_NUMBER >= 0x010600
+ assert_se(result == DNSSEC_VALIDATED);
+#else
+ assert_se(result == DNSSEC_UNSUPPORTED_ALGORITHM);
+#endif
+}
+static void test_dnssec_verify_rrset(void) {
+
+ 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) >= 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);
+}
+
+static void test_dnssec_verify_rrset2(void) {
+
+ 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) >= 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);
+}
+
+static void test_dnssec_verify_rrset3(void) {
+
+ 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) >= 0);
+ assert_se(dns_answer_add(answer, mx2, 0, DNS_ANSWER_AUTHENTICATED) >= 0);
+ assert_se(dns_answer_add(answer, mx3, 0, DNS_ANSWER_AUTHENTICATED) >= 0);
+ assert_se(dns_answer_add(answer, mx4, 0, DNS_ANSWER_AUTHENTICATED) >= 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);
+}
+
+static void test_dnssec_nsec3_hash(void) {
+ 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);
+}
+
+#endif
+
+int main(int argc, char *argv[]) {
+
+#if HAVE_GCRYPT
+ test_dnssec_verify_dns_key();
+ test_dnssec_verify_rfc8080_ed25519_example1();
+ test_dnssec_verify_rfc8080_ed25519_example2();
+ test_dnssec_verify_rrset();
+ test_dnssec_verify_rrset2();
+ test_dnssec_verify_rrset3();
+ test_dnssec_nsec3_hash();
+#endif
+
+ return 0;
+}
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..045952e
--- /dev/null
+++ b/src/resolve/test-resolved-etc-hosts.c
@@ -0,0 +1,150 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <arpa/inet.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"
+
+static void test_parse_etc_hosts_system(void) {
+ _cleanup_fclose_ FILE *f = NULL;
+
+ log_info("/* %s */", __func__);
+
+ 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) )
+
+static void test_parse_etc_hosts(void) {
+ _cleanup_(unlink_tempfilep) char
+ t[] = "/tmp/test-resolved-etc-hosts.XXXXXX";
+
+ log_info("/* %s */", __func__);
+
+ int fd;
+ _cleanup_fclose_ FILE *f;
+ const char *s;
+
+ 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(bn->n_allocated >= 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(bn->n_allocated >= 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(bn->n_allocated >= 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(bn->n_allocated >= 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(!hashmap_get(hosts.by_name, "within.comment"));
+ assert(!hashmap_get(hosts.by_name, "within.comment2"));
+ assert(!hashmap_get(hosts.by_name, "within.comment3"));
+ assert(!hashmap_get(hosts.by_name, "#"));
+
+ assert(!hashmap_get(hosts.by_name, "short.address"));
+ assert(!hashmap_get(hosts.by_name, "long.address"));
+ assert(!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(bn->n_allocated >= 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(const char *fname) {
+ _cleanup_(etc_hosts_free) EtcHosts hosts = {};
+ _cleanup_fclose_ FILE *f;
+
+ log_info("/* %s(\"%s\") */", __func__, fname);
+
+ assert_se(f = fopen(fname, "re"));
+ assert_se(etc_hosts_parse(&hosts, f) == 0);
+}
+
+int main(int argc, char **argv) {
+ test_setup_logging(LOG_DEBUG);
+
+ if (argc == 1) {
+ test_parse_etc_hosts_system();
+ test_parse_etc_hosts();
+ } else
+ test_parse_file(argv[1]);
+
+ return 0;
+}
diff --git a/src/resolve/test-resolved-packet.c b/src/resolve/test-resolved-packet.c
new file mode 100644
index 0000000..cd93b1c
--- /dev/null
+++ b/src/resolve/test-resolved-packet.c
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "log.h"
+#include "resolved-dns-packet.h"
+#include "tests.h"
+
+static void test_dns_packet_new(void) {
+ 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);
+}
+
+int main(int argc, char **argv) {
+ test_setup_logging(LOG_DEBUG);
+
+ test_dns_packet_new();
+
+ return 0;
+}