Internal change

PiperOrigin-RevId: 470978856
Change-Id: I7c0d88a0a17ddb19a063a6f7f4494818969cf610
diff --git a/src/resolve/RFCs b/src/resolve/RFCs
new file mode 100644
index 0000000..7190c16
--- /dev/null
+++ b/src/resolve/RFCs
@@ -0,0 +1,60 @@
+Y = Comprehensively Implemented, to the point appropriate for resolved
+D = Comprehensively Implemented, by a dependency of resolved
+! = Missing and something we might want to implement
+~ = Needs no explicit support or doesn't apply
+? = Is this relevant today?
+  = We are working on this
+
+Y https://tools.ietf.org/html/rfc1034 → DOMAIN NAMES - CONCEPTS AND FACILITIES
+Y https://tools.ietf.org/html/rfc1035 → DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION
+? https://tools.ietf.org/html/rfc1101 → DNS Encoding of Network Names and Other Types
+Y https://tools.ietf.org/html/rfc1123 → Requirements for Internet Hosts — Application and Support
+~ https://tools.ietf.org/html/rfc1464 → Using the Domain Name System To Store Arbitrary String Attributes
+Y https://tools.ietf.org/html/rfc1536 → Common DNS Implementation Errors and Suggested Fixes
+Y https://tools.ietf.org/html/rfc1876 → A Means for Expressing Location Information in the Domain Name System
+Y https://tools.ietf.org/html/rfc2181 → Clarifications to the DNS Specification
+Y https://tools.ietf.org/html/rfc2308 → Negative Caching of DNS Queries (DNS NCACHE)
+Y https://tools.ietf.org/html/rfc2782 → A DNS RR for specifying the location of services (DNS SRV)
+D https://tools.ietf.org/html/rfc3492 → Punycode: A Bootstring encoding of Unicode for Internationalized Domain Names in Applications (IDNA)
+Y https://tools.ietf.org/html/rfc3596 → DNS Extensions to Support IP Version 6
+Y https://tools.ietf.org/html/rfc3597 → Handling of Unknown DNS Resource Record (RR) Types
+Y https://tools.ietf.org/html/rfc4033 → DNS Security Introduction and Requirements
+Y https://tools.ietf.org/html/rfc4034 → Resource Records for the DNS Security Extensions
+Y https://tools.ietf.org/html/rfc4035 → Protocol Modifications for the DNS Security Extensions
+! https://tools.ietf.org/html/rfc4183 → A Suggested Scheme for DNS Resolution of Networks and Gateways
+Y https://tools.ietf.org/html/rfc4255 → Using DNS to Securely Publish Secure Shell (SSH) Key Fingerprints
+Y https://tools.ietf.org/html/rfc4343 → Domain Name System (DNS) Case Insensitivity Clarification
+~ https://tools.ietf.org/html/rfc4470 → Minimally Covering NSEC Records and DNSSEC On-line Signing
+Y https://tools.ietf.org/html/rfc4501 → Domain Name System Uniform Resource Identifiers
+Y https://tools.ietf.org/html/rfc4509 → Use of SHA-256 in DNSSEC Delegation Signer (DS) Resource Records (RRs)
+~ https://tools.ietf.org/html/rfc4592 → The Role of Wildcards in the Domain Name System
+~ https://tools.ietf.org/html/rfc4697 → Observed DNS Resolution Misbehavior
+Y https://tools.ietf.org/html/rfc4795 → Link-Local Multicast Name Resolution (LLMNR)
+Y https://tools.ietf.org/html/rfc5011 → Automated Updates of DNS Security (DNSSEC) Trust Anchors
+Y https://tools.ietf.org/html/rfc5155 → DNS Security (DNSSEC) Hashed Authenticated Denial of Existence
+Y https://tools.ietf.org/html/rfc5452 → Measures for Making DNS More Resilient against Forged Answers
+Y https://tools.ietf.org/html/rfc5702 → Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource Records for DNSSEC
+Y https://tools.ietf.org/html/rfc5890 → Internationalized Domain Names for Applications (IDNA): Definitions and Document Framework
+Y https://tools.ietf.org/html/rfc5891 → Internationalized Domain Names in Applications (IDNA): Protocol
+Y https://tools.ietf.org/html/rfc5966 → DNS Transport over TCP - Implementation Requirements
+Y https://tools.ietf.org/html/rfc6303 → Locally Served DNS Zones
+Y https://tools.ietf.org/html/rfc6604 → xNAME RCODE and Status Bits Clarification
+Y https://tools.ietf.org/html/rfc6605 → Elliptic Curve Digital Signature Algorithm (DSA) for DNSSEC
+  https://tools.ietf.org/html/rfc6672 → DNAME Redirection in the DNS
+! https://tools.ietf.org/html/rfc6731 → Improved Recursive DNS Server Selection for Multi-Interfaced Nodes
+Y https://tools.ietf.org/html/rfc6761 → Special-Use Domain Names
+  https://tools.ietf.org/html/rfc6762 → Multicast DNS
+  https://tools.ietf.org/html/rfc6763 → DNS-Based Service Discovery
+~ https://tools.ietf.org/html/rfc6781 → DNSSEC Operational Practices, Version 2
+Y https://tools.ietf.org/html/rfc6840 → Clarifications and Implementation Notes for DNS Security (DNSSEC)
+Y https://tools.ietf.org/html/rfc6891 → Extension Mechanisms for DNS (EDNS(0))
+Y https://tools.ietf.org/html/rfc6944 → Applicability Statement: DNS Security (DNSSEC) DNSKEY Algorithm Implementation Status
+Y https://tools.ietf.org/html/rfc6975 → Signaling Cryptographic Algorithm Understanding in DNS Security Extensions (DNSSEC)
+Y https://tools.ietf.org/html/rfc7129 → Authenticated Denial of Existence in the DNS
+Y https://tools.ietf.org/html/rfc7646 → Definition and Use of DNSSEC Negative Trust Anchors
+~ https://tools.ietf.org/html/rfc7719 → DNS Terminology
+Y https://tools.ietf.org/html/rfc8080 → Edwards-Curve Digital Security Algorithm (EdDSA) for DNSSEC
+
+Also relevant:
+
+  https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-statement-dotless-domains-considered-harmful/
diff --git a/src/resolve/dns-type.c b/src/resolve/dns-type.c
new file mode 100644
index 0000000..da68b41
--- /dev/null
+++ b/src/resolve/dns-type.c
@@ -0,0 +1,316 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/socket.h>
+#include <errno.h>
+
+#include "dns-type.h"
+#include "parse-util.h"
+#include "string-util.h"
+
+typedef const struct {
+        uint16_t type;
+        const char *name;
+} dns_type;
+
+static const struct dns_type_name *
+lookup_dns_type (register const char *str, register GPERF_LEN_TYPE len);
+
+#include "dns_type-from-name.h"
+#include "dns_type-to-name.h"
+
+int dns_type_from_string(const char *s) {
+        const struct dns_type_name *sc;
+
+        assert(s);
+
+        sc = lookup_dns_type(s, strlen(s));
+        if (sc)
+                return sc->id;
+
+        s = startswith_no_case(s, "TYPE");
+        if (s) {
+                unsigned x;
+
+                if (safe_atou(s, &x) >= 0 &&
+                    x <= UINT16_MAX)
+                        return (int) x;
+        }
+
+        return _DNS_TYPE_INVALID;
+}
+
+bool dns_type_is_pseudo(uint16_t type) {
+
+        /* Checks whether the specified type is a "pseudo-type". What
+         * a "pseudo-type" precisely is, is defined only very weakly,
+         * but apparently entails all RR types that are not actually
+         * stored as RRs on the server and should hence also not be
+         * cached. We use this list primarily to validate NSEC type
+         * bitfields, and to verify what to cache. */
+
+        return IN_SET(type,
+                      0, /* A Pseudo RR type, according to RFC 2931 */
+                      DNS_TYPE_ANY,
+                      DNS_TYPE_AXFR,
+                      DNS_TYPE_IXFR,
+                      DNS_TYPE_OPT,
+                      DNS_TYPE_TSIG,
+                      DNS_TYPE_TKEY
+        );
+}
+
+bool dns_class_is_pseudo(uint16_t class) {
+        return class == DNS_CLASS_ANY;
+}
+
+bool dns_type_is_valid_query(uint16_t type) {
+
+        /* The types valid as questions in packets */
+
+        return !IN_SET(type,
+                       0,
+                       DNS_TYPE_OPT,
+                       DNS_TYPE_TSIG,
+                       DNS_TYPE_TKEY,
+
+                       /* RRSIG are technically valid as questions, but we refuse doing explicit queries for them, as
+                        * they aren't really payload, but signatures for payload, and cannot be validated on their
+                        * own. After all they are the signatures, and have no signatures of their own validating
+                        * them. */
+                       DNS_TYPE_RRSIG);
+}
+
+bool dns_type_is_zone_transer(uint16_t type) {
+
+        /* Zone transfers, either normal or incremental */
+
+        return IN_SET(type,
+                      DNS_TYPE_AXFR,
+                      DNS_TYPE_IXFR);
+}
+
+bool dns_type_is_valid_rr(uint16_t type) {
+
+        /* The types valid as RR in packets (but not necessarily
+         * stored on servers). */
+
+        return !IN_SET(type,
+                       DNS_TYPE_ANY,
+                       DNS_TYPE_AXFR,
+                       DNS_TYPE_IXFR);
+}
+
+bool dns_class_is_valid_rr(uint16_t class) {
+        return class != DNS_CLASS_ANY;
+}
+
+bool dns_type_may_redirect(uint16_t type) {
+        /* The following record types should never be redirected using
+         * CNAME/DNAME RRs. See
+         * <https://tools.ietf.org/html/rfc4035#section-2.5>. */
+
+        if (dns_type_is_pseudo(type))
+                return false;
+
+        return !IN_SET(type,
+                       DNS_TYPE_CNAME,
+                       DNS_TYPE_DNAME,
+                       DNS_TYPE_NSEC3,
+                       DNS_TYPE_NSEC,
+                       DNS_TYPE_RRSIG,
+                       DNS_TYPE_NXT,
+                       DNS_TYPE_SIG,
+                       DNS_TYPE_KEY);
+}
+
+bool dns_type_may_wildcard(uint16_t type) {
+
+        /* The following records may not be expanded from wildcard RRsets */
+
+        if (dns_type_is_pseudo(type))
+                return false;
+
+        return !IN_SET(type,
+                       DNS_TYPE_NSEC3,
+                       DNS_TYPE_SOA,
+
+                       /* Prohibited by https://tools.ietf.org/html/rfc4592#section-4.4 */
+                       DNS_TYPE_DNAME);
+}
+
+bool dns_type_apex_only(uint16_t type) {
+
+        /* Returns true for all RR types that may only appear signed in a zone apex */
+
+        return IN_SET(type,
+                      DNS_TYPE_SOA,
+                      DNS_TYPE_NS,            /* this one can appear elsewhere, too, but not signed */
+                      DNS_TYPE_DNSKEY,
+                      DNS_TYPE_NSEC3PARAM);
+}
+
+bool dns_type_is_dnssec(uint16_t type) {
+        return IN_SET(type,
+                      DNS_TYPE_DS,
+                      DNS_TYPE_DNSKEY,
+                      DNS_TYPE_RRSIG,
+                      DNS_TYPE_NSEC,
+                      DNS_TYPE_NSEC3,
+                      DNS_TYPE_NSEC3PARAM);
+}
+
+bool dns_type_is_obsolete(uint16_t type) {
+        return IN_SET(type,
+                      /* Obsoleted by RFC 973 */
+                      DNS_TYPE_MD,
+                      DNS_TYPE_MF,
+                      DNS_TYPE_MAILA,
+
+                      /* Kinda obsoleted by RFC 2505 */
+                      DNS_TYPE_MB,
+                      DNS_TYPE_MG,
+                      DNS_TYPE_MR,
+                      DNS_TYPE_MINFO,
+                      DNS_TYPE_MAILB,
+
+                      /* RFC1127 kinda obsoleted this by recommending against its use */
+                      DNS_TYPE_WKS,
+
+                      /* Declared historical by RFC 6563 */
+                      DNS_TYPE_A6,
+
+                      /* Obsoleted by DNSSEC-bis */
+                      DNS_TYPE_NXT,
+
+                      /* RFC 1035 removed support for concepts that needed this from RFC 883 */
+                      DNS_TYPE_NULL);
+}
+
+bool dns_type_needs_authentication(uint16_t type) {
+
+        /* Returns true for all (non-obsolete) RR types where records are not useful if they aren't
+         * authenticated. I.e. everything that contains crypto keys. */
+
+        return IN_SET(type,
+                      DNS_TYPE_CERT,
+                      DNS_TYPE_SSHFP,
+                      DNS_TYPE_IPSECKEY,
+                      DNS_TYPE_DS,
+                      DNS_TYPE_DNSKEY,
+                      DNS_TYPE_TLSA,
+                      DNS_TYPE_CDNSKEY,
+                      DNS_TYPE_OPENPGPKEY,
+                      DNS_TYPE_CAA);
+}
+
+int dns_type_to_af(uint16_t t) {
+        switch (t) {
+
+        case DNS_TYPE_A:
+                return AF_INET;
+
+        case DNS_TYPE_AAAA:
+                return AF_INET6;
+
+        case DNS_TYPE_ANY:
+                return AF_UNSPEC;
+
+        default:
+                return -EINVAL;
+        }
+}
+
+const char *dns_class_to_string(uint16_t class) {
+
+        switch (class) {
+
+        case DNS_CLASS_IN:
+                return "IN";
+
+        case DNS_CLASS_ANY:
+                return "ANY";
+        }
+
+        return NULL;
+}
+
+int dns_class_from_string(const char *s) {
+
+        if (!s)
+                return _DNS_CLASS_INVALID;
+
+        if (strcaseeq(s, "IN"))
+                return DNS_CLASS_IN;
+        else if (strcaseeq(s, "ANY"))
+                return DNS_CLASS_ANY;
+
+        return _DNS_CLASS_INVALID;
+}
+
+const char* tlsa_cert_usage_to_string(uint8_t cert_usage) {
+
+        switch (cert_usage) {
+
+        case 0:
+                return "CA constraint";
+
+        case 1:
+                return "Service certificate constraint";
+
+        case 2:
+                return "Trust anchor assertion";
+
+        case 3:
+                return "Domain-issued certificate";
+
+        case 4 ... 254:
+                return "Unassigned";
+
+        case 255:
+                return "Private use";
+        }
+
+        return NULL;  /* clang cannot count that we covered everything */
+}
+
+const char* tlsa_selector_to_string(uint8_t selector) {
+        switch (selector) {
+
+        case 0:
+                return "Full Certificate";
+
+        case 1:
+                return "SubjectPublicKeyInfo";
+
+        case 2 ... 254:
+                return "Unassigned";
+
+        case 255:
+                return "Private use";
+        }
+
+        return NULL;
+}
+
+const char* tlsa_matching_type_to_string(uint8_t selector) {
+
+        switch (selector) {
+
+        case 0:
+                return "No hash used";
+
+        case 1:
+                return "SHA-256";
+
+        case 2:
+                return "SHA-512";
+
+        case 3 ... 254:
+                return "Unassigned";
+
+        case 255:
+                return "Private use";
+        }
+
+        return NULL;
+}
diff --git a/src/resolve/dns-type.h b/src/resolve/dns-type.h
new file mode 100644
index 0000000..f0bb3be
--- /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 = -EINVAL,
+};
+
+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 = -EINVAL,
+};
+
+#define _DNS_CLASS_STRING_MAX (sizeof "CLASS" + DECIMAL_STR_MAX(uint16_t))
+#define _DNS_TYPE_STRING_MAX (sizeof "CLASS" + DECIMAL_STR_MAX(uint16_t))
+
+bool dns_type_is_pseudo(uint16_t type);
+bool dns_type_is_valid_query(uint16_t type);
+bool dns_type_is_valid_rr(uint16_t type);
+bool dns_type_may_redirect(uint16_t type);
+bool dns_type_is_dnssec(uint16_t type);
+bool dns_type_is_obsolete(uint16_t type);
+bool dns_type_may_wildcard(uint16_t type);
+bool dns_type_apex_only(uint16_t type);
+bool dns_type_needs_authentication(uint16_t type);
+bool dns_type_is_zone_transer(uint16_t type);
+int dns_type_to_af(uint16_t type);
+
+bool dns_class_is_pseudo(uint16_t class);
+bool dns_class_is_valid_rr(uint16_t class);
+
+/* TYPE?? follows http://tools.ietf.org/html/rfc3597#section-5 */
+const char *dns_type_to_string(int type);
+int dns_type_from_string(const char *s);
+
+const char *dns_class_to_string(uint16_t class);
+int dns_class_from_string(const char *name);
+
+/* https://tools.ietf.org/html/draft-ietf-dane-protocol-23#section-7.2 */
+const char *tlsa_cert_usage_to_string(uint8_t cert_usage);
+
+/* https://tools.ietf.org/html/draft-ietf-dane-protocol-23#section-7.3 */
+const char *tlsa_selector_to_string(uint8_t selector);
+
+/* https://tools.ietf.org/html/draft-ietf-dane-protocol-23#section-7.4 */
+const char *tlsa_matching_type_to_string(uint8_t selector);
+
+/* https://tools.ietf.org/html/rfc6844#section-5.1 */
+#define CAA_FLAG_CRITICAL (1u << 7)
diff --git a/src/resolve/dns_type-to-name.awk b/src/resolve/dns_type-to-name.awk
new file mode 100644
index 0000000..2d9794b
--- /dev/null
+++ b/src/resolve/dns_type-to-name.awk
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+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/fuzz-dns-packet.c b/src/resolve/fuzz-dns-packet.c
new file mode 100644
index 0000000..b9a0aa1
--- /dev/null
+++ b/src/resolve/fuzz-dns-packet.c
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "fuzz.h"
+#include "memory-util.h"
+#include "resolved-dns-packet.h"
+
+int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+
+        if (size > DNS_PACKET_SIZE_MAX)
+                return 0;
+
+        assert_se(dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0);
+        p->size = 0; /* by default append starts after the header, undo that */
+        assert_se(dns_packet_append_blob(p, data, size, NULL) >= 0);
+        if (size < DNS_PACKET_HEADER_SIZE) {
+                /* make sure we pad the packet back up to the minimum header size */
+                assert_se(p->allocated >= DNS_PACKET_HEADER_SIZE);
+                memzero(DNS_PACKET_DATA(p) + size, DNS_PACKET_HEADER_SIZE - size);
+                p->size = DNS_PACKET_HEADER_SIZE;
+        }
+        (void) dns_packet_extract(p);
+
+        return 0;
+}
diff --git a/src/resolve/fuzz-dns-packet.options b/src/resolve/fuzz-dns-packet.options
new file mode 100644
index 0000000..0824b19
--- /dev/null
+++ b/src/resolve/fuzz-dns-packet.options
@@ -0,0 +1,2 @@
+[libfuzzer]
+max_len = 65535
diff --git a/src/resolve/generate-dns_type-gperf.py b/src/resolve/generate-dns_type-gperf.py
new file mode 100755
index 0000000..0d818fb
--- /dev/null
+++ b/src/resolve/generate-dns_type-gperf.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+"""Generate %-from-name.gperf from %-list.txt
+"""
+
+import sys
+
+name, prefix, input = sys.argv[1:]
+
+print("""\
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \\"-Wimplicit-fallthrough\\"")
+#endif
+%}""")
+print("""\
+struct {}_name {{ const char* name; int id; }};
+%null-strings
+%%""".format(name))
+
+for line in open(input):
+    line = line.rstrip()
+    s = line.replace('_', '-')
+    print("{}, {}{}".format(s, prefix, line))
diff --git a/src/resolve/generate-dns_type-list.sed b/src/resolve/generate-dns_type-list.sed
new file mode 100644
index 0000000..32af08c
--- /dev/null
+++ b/src/resolve/generate-dns_type-list.sed
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+s/.* DNS_TYPE_(\w+).*/\1/p
diff --git a/src/resolve/meson.build b/src/resolve/meson.build
new file mode 100644
index 0000000..1f9b8af
--- /dev/null
+++ b/src/resolve/meson.build
@@ -0,0 +1,230 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+resolve_includes = [includes, include_directories('.')]
+
+basic_dns_sources = files('''
+        resolved-dns-dnssec.c
+        resolved-dns-dnssec.h
+        resolved-dns-packet.c
+        resolved-dns-packet.h
+        resolved-dns-rr.c
+        resolved-dns-rr.h
+        resolved-dns-answer.c
+        resolved-dns-answer.h
+        resolved-dns-question.c
+        resolved-dns-question.h
+        resolved-util.c
+        resolved-util.h
+        dns-type.c
+        dns-type.h
+'''.split())
+
+systemd_resolved_sources = files('''
+        resolved-bus.c
+        resolved-bus.h
+        resolved-conf.c
+        resolved-conf.h
+        resolved-def.h
+        resolved-dns-cache.c
+        resolved-dns-cache.h
+        resolved-dns-query.c
+        resolved-dns-query.h
+        resolved-dns-scope.c
+        resolved-dns-scope.h
+        resolved-dns-search-domain.c
+        resolved-dns-search-domain.h
+        resolved-dns-server.c
+        resolved-dns-server.h
+        resolved-dns-stream.c
+        resolved-dns-stream.h
+        resolved-dns-stub.c
+        resolved-dns-stub.h
+        resolved-dns-synthesize.c
+        resolved-dns-synthesize.h
+        resolved-dns-transaction.c
+        resolved-dns-transaction.h
+        resolved-dns-trust-anchor.c
+        resolved-dns-trust-anchor.h
+        resolved-dns-zone.c
+        resolved-dns-zone.h
+        resolved-dnssd-bus.c
+        resolved-dnssd-bus.h
+        resolved-dnssd.c
+        resolved-dnssd.h
+        resolved-dnstls.h
+        resolved-etc-hosts.c
+        resolved-etc-hosts.h
+        resolved-link-bus.c
+        resolved-link-bus.h
+        resolved-link.c
+        resolved-link.h
+        resolved-llmnr.c
+        resolved-llmnr.h
+        resolved-manager.c
+        resolved-manager.h
+        resolved-mdns.c
+        resolved-mdns.h
+        resolved-resolv-conf.c
+        resolved-resolv-conf.h
+        resolved-socket-graveyard.c
+        resolved-socket-graveyard.h
+        resolved-varlink.c
+        resolved-varlink.h
+        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')
+
+gperf_file = custom_target(
+        'dns_type-from-name.gperf',
+        input : dns_type_list_txt,
+        output : 'dns_type-from-name.gperf',
+        command : [generate_dns_type_gperf, 'dns_type', 'DNS_TYPE_', '@INPUT@'],
+        capture : true)
+
+basic_dns_sources += custom_target(
+        'dns_type-from-name.h',
+        input : gperf_file,
+        output : 'dns_type-from-name.h',
+        command : [gperf,
+                   '-L', 'ANSI-C', '-t', '--ignore-case',
+                   '-N', 'lookup_dns_type',
+                   '-H', 'hash_dns_type_name',
+                   '-p', '-C',
+                   '@INPUT@'],
+        capture : true)
+
+basic_dns_sources += custom_target(
+        'dns_type-to-name.h',
+        input : ['dns_type-to-name.awk', dns_type_list_txt],
+        output : 'dns_type-to-name.h',
+        command : [awk, '-f', '@INPUT0@', '@INPUT1@'],
+        capture : true)
+
+libsystemd_resolve_core = static_library(
+        'systemd-resolve-core',
+        basic_dns_sources,
+        include_directories : includes)
+
+systemd_resolved_sources += custom_target(
+        'resolved_gperf.c',
+        input : 'resolved-gperf.gperf',
+        output : 'resolved-gperf.c',
+        command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+systemd_resolved_sources += custom_target(
+        'resolved_dnssd_gperf.c',
+        input : 'resolved-dnssd-gperf.gperf',
+        output : 'resolved-dnssd-gperf.c',
+        command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@'])
+
+systemd_resolved_dependencies = [threads, 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)
+        install_data('resolv.conf',
+                     install_dir : rootlibexecdir)
+endif
+
+custom_target(
+        'resolved.conf',
+        input : 'resolved.conf.in',
+        output : 'resolved.conf',
+        command : [meson_render_jinja2, config_h, '@INPUT@'],
+        capture : true,
+        install : conf.get('ENABLE_RESOLVE') == 1 and install_sysconfdir_samples,
+        install_dir : pkgsysconfdir)
+
+############################################################
+
+tests += [
+        [['src/resolve/test-resolve-tables.c'],
+         [libsystemd_resolve_core,
+          libshared],
+         [libgcrypt,
+          libgpg_error,
+          libm]],
+
+        [['src/resolve/test-dns-packet.c'],
+         [libsystemd_resolve_core,
+          libshared],
+         [libgcrypt,
+          libgpg_error,
+          libm]],
+
+        [['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]],
+
+        [['src/resolve/test-resolved-packet.c'],
+         [libsystemd_resolve_core,
+          libshared],
+         [libgcrypt,
+          libgpg_error,
+          libm]],
+
+        [['src/resolve/test-dnssec.c'],
+         [libsystemd_resolve_core,
+          libshared],
+         [libgcrypt,
+          libgpg_error,
+          libm]],
+
+        [['src/resolve/test-dnssec-complex.c'],
+         [libsystemd_resolve_core,
+          libshared],
+         [libgcrypt,
+          libgpg_error,
+          libm],
+         [], '', 'manual'],
+]
+
+fuzzers += [
+        [['src/resolve/fuzz-dns-packet.c'],
+         [libsystemd_resolve_core,
+          libshared],
+         [libgcrypt,
+          libgpg_error,
+          libm]],
+]
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..2408bb9
--- /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>https://systemd.io</vendor_url>
+
+        <action id="org.freedesktop.resolve1.register-service">
+                <description gettext-domain="systemd">Register a DNS-SD service</description>
+                <message gettext-domain="systemd">Authentication is required to register a DNS-SD service</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.unregister-service">
+                <description gettext-domain="systemd">Unregister a DNS-SD service</description>
+                <message gettext-domain="systemd">Authentication is required to unregister a DNS-SD service</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-dns-servers">
+                <description gettext-domain="systemd">Set DNS servers</description>
+                <message gettext-domain="systemd">Authentication is required to set DNS servers.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-domains">
+                <description gettext-domain="systemd">Set domains</description>
+                <message gettext-domain="systemd">Authentication is required to set domains.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-default-route">
+                <description gettext-domain="systemd">Set default route</description>
+                <message gettext-domain="systemd">Authentication is required to set default route.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-llmnr">
+                <description gettext-domain="systemd">Enable/disable LLMNR</description>
+                <message gettext-domain="systemd">Authentication is required to enable or disable LLMNR.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-mdns">
+                <description gettext-domain="systemd">Enable/disable multicast DNS</description>
+                <message gettext-domain="systemd">Authentication is required to enable or disable multicast DNS.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-dns-over-tls">
+                <description gettext-domain="systemd">Enable/disable DNS over TLS</description>
+                <message gettext-domain="systemd">Authentication is required to enable or disable DNS over TLS.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-dnssec">
+                <description gettext-domain="systemd">Enable/disable DNSSEC</description>
+                <message gettext-domain="systemd">Authentication is required to enable or disable DNSSEC.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.set-dnssec-negative-trust-anchors">
+                <description gettext-domain="systemd">Set DNSSEC Negative Trust Anchors</description>
+                <message gettext-domain="systemd">Authentication is required to set DNSSEC Negative Trust Anchors.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+        <action id="org.freedesktop.resolve1.revert">
+                <description gettext-domain="systemd">Revert name resolution settings</description>
+                <message gettext-domain="systemd">Authentication is required to reset name resolution settings.</message>
+                <defaults>
+                        <allow_any>auth_admin</allow_any>
+                        <allow_inactive>auth_admin</allow_inactive>
+                        <allow_active>auth_admin_keep</allow_active>
+                </defaults>
+                <annotate key="org.freedesktop.policykit.owner">unix-user:systemd-resolve</annotate>
+        </action>
+
+</policyconfig>
diff --git a/src/resolve/org.freedesktop.resolve1.service b/src/resolve/org.freedesktop.resolve1.service
new file mode 100644
index 0000000..32a04f3
--- /dev/null
+++ b/src/resolve/org.freedesktop.resolve1.service
@@ -0,0 +1,14 @@
+#  SPDX-License-Identifier: LGPL-2.1-or-later
+#
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it
+#  under the terms of the GNU Lesser General Public License as published by
+#  the Free Software Foundation; either version 2.1 of the License, or
+#  (at your option) any later version.
+
+[D-BUS Service]
+Name=org.freedesktop.resolve1
+Exec=/bin/false
+User=root
+SystemdService=dbus-org.freedesktop.resolve1.service
diff --git a/src/resolve/resolv.conf b/src/resolve/resolv.conf
new file mode 100644
index 0000000..b4e9a96
--- /dev/null
+++ b/src/resolve/resolv.conf
@@ -0,0 +1,19 @@
+# This file belongs to man:systemd-resolved(8). Do not edit.
+#
+# This is a static resolv.conf file for connecting local clients to the
+# internal DNS stub resolver of systemd-resolved. This file lists no search
+# domains.
+#
+# Run "resolvectl status" to see details about the uplink DNS servers
+# currently in use.
+#
+# Third party programs must not access this file directly, but only through the
+# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
+# replace this symlink by a static file or a different symlink.
+#
+# See man:systemd-resolved.service(8) for details about the supported modes of
+# operation for /etc/resolv.conf.
+
+nameserver 127.0.0.53
+options edns0 trust-ad
+search .
diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c
new file mode 100644
index 0000000..991c62e
--- /dev/null
+++ b/src/resolve/resolvconf-compat.c
@@ -0,0 +1,277 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <getopt.h>
+#include <net/if.h>
+
+#include "alloc-util.h"
+#include "def.h"
+#include "dns-domain.h"
+#include "extract-word.h"
+#include "fileio.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "resolvconf-compat.h"
+#include "resolvectl.h"
+#include "resolved-def.h"
+#include "string-util.h"
+#include "strv.h"
+#include "terminal-util.h"
+
+static int resolvconf_help(void) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        r = terminal_urlify_man("resolvectl", "1", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%1$s -a INTERFACE < FILE\n"
+               "%1$s -d INTERFACE\n"
+               "\n"
+               "Register DNS server and domain configuration with systemd-resolved.\n\n"
+               "  -h --help     Show this help\n"
+               "     --version  Show package version\n"
+               "  -a            Register per-interface DNS server and domain data\n"
+               "  -d            Unregister per-interface DNS server and domain data\n"
+               "  -f            Ignore if specified interface does not exist\n"
+               "  -x            Send DNS traffic preferably over this interface\n"
+               "\n"
+               "This is a compatibility alias for the resolvectl(1) tool, providing native\n"
+               "command line compatibility with the resolvconf(8) tool of various Linux\n"
+               "distributions and BSD systems. Some options supported by other implementations\n"
+               "are not supported and are ignored: -m, -p, -u. Various options supported by other\n"
+               "implementations are not supported and will cause the invocation to fail:\n"
+               "-I, -i, -l, -R, -r, -v, -V, --enable-updates, --disable-updates,\n"
+               "--updates-are-enabled.\n"
+               "\nSee the %2$s for details.\n",
+               program_invocation_short_name,
+               link);
+
+        return 0;
+}
+
+static int parse_nameserver(const char *string) {
+        int r;
+
+        assert(string);
+
+        for (;;) {
+                _cleanup_free_ char *word = NULL;
+
+                r = extract_first_word(&string, &word, NULL, 0);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                if (strv_push(&arg_set_dns, word) < 0)
+                        return log_oom();
+
+                word = NULL;
+        }
+
+        return 0;
+}
+
+static int parse_search_domain(const char *string) {
+        int r;
+
+        assert(string);
+
+        for (;;) {
+                _cleanup_free_ char *word = NULL;
+
+                r = extract_first_word(&string, &word, NULL, EXTRACT_UNQUOTE);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                if (strv_push(&arg_set_domain, word) < 0)
+                        return log_oom();
+
+                word = NULL;
+        }
+
+        return 0;
+}
+
+int resolvconf_parse_argv(int argc, char *argv[]) {
+
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_ENABLE_UPDATES,
+                ARG_DISABLE_UPDATES,
+                ARG_UPDATES_ARE_ENABLED,
+        };
+
+        static const struct option options[] = {
+                { "help",                no_argument, NULL, 'h'                     },
+                { "version",             no_argument, NULL, ARG_VERSION             },
+
+                /* The following are specific to Debian's original resolvconf */
+                { "enable-updates",      no_argument, NULL, ARG_ENABLE_UPDATES      },
+                { "disable-updates",     no_argument, NULL, ARG_DISABLE_UPDATES     },
+                { "updates-are-enabled", no_argument, NULL, ARG_UPDATES_ARE_ENABLED },
+                {}
+        };
+
+        enum {
+                TYPE_REGULAR,
+                TYPE_PRIVATE,   /* -p: Not supported, treated identically to TYPE_REGULAR */
+                TYPE_EXCLUSIVE, /* -x */
+        } type = TYPE_REGULAR;
+
+        int c, r;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        /* openresolv checks these environment variables */
+        if (getenv("IF_EXCLUSIVE"))
+                type = TYPE_EXCLUSIVE;
+        if (getenv("IF_PRIVATE"))
+                type = TYPE_PRIVATE; /* not actually supported */
+
+        arg_mode = _MODE_INVALID;
+
+        while ((c = getopt_long(argc, argv, "hadxpfm:uIi:l:Rr:vV", options, NULL)) >= 0)
+                switch(c) {
+
+                case 'h':
+                        return resolvconf_help();
+
+                case ARG_VERSION:
+                        return version();
+
+                /* -a and -d is what everybody can agree on */
+                case 'a':
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case 'd':
+                        arg_mode = MODE_REVERT_LINK;
+                        break;
+
+                /* The exclusive/private/force stuff is an openresolv invention, we support in some skewed way */
+                case 'x':
+                        type = TYPE_EXCLUSIVE;
+                        break;
+
+                case 'p':
+                        type = TYPE_PRIVATE; /* not actually supported */
+                        break;
+
+                case 'f':
+                        arg_ifindex_permissive = true;
+                        break;
+
+                /* The metrics stuff is an openresolv invention we ignore (and don't really need) */
+                case 'm':
+                        log_debug("Switch -%c ignored.", c);
+                        break;
+
+                /* -u supposedly should "update all subscribers". We have no subscribers, hence let's make
+                    this a NOP, and exit immediately, cleanly. */
+                case 'u':
+                        log_info("Switch -%c ignored.", c);
+                        return 0;
+
+                /* The following options are openresolv inventions we don't support. */
+                case 'I':
+                case 'i':
+                case 'l':
+                case 'R':
+                case 'r':
+                case 'v':
+                case 'V':
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Switch -%c not supported.", c);
+
+                /* The Debian resolvconf commands we don't support. */
+                case ARG_ENABLE_UPDATES:
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Switch --enable-updates not supported.");
+                case ARG_DISABLE_UPDATES:
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Switch --disable-updates not supported.");
+                case ARG_UPDATES_ARE_ENABLED:
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "Switch --updates-are-enabled not supported.");
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached("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_resolvconf_mangle(argv[optind]);
+        if (r <= 0)
+                return r;
+
+        optind++;
+
+        if (arg_mode == MODE_SET_LINK) {
+                unsigned n = 0;
+
+                for (;;) {
+                        _cleanup_free_ char *line = NULL;
+                        const char *a, *l;
+
+                        r = read_line(stdin, LONG_LINE_MAX, &line);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to read from stdin: %m");
+                        if (r == 0)
+                                break;
+
+                        n++;
+
+                        l = strstrip(line);
+                        if (IN_SET(*l, '#', ';', 0))
+                                continue;
+
+                        a = first_word(l, "nameserver");
+                        if (a) {
+                                (void) parse_nameserver(a);
+                                continue;
+                        }
+
+                        a = first_word(l, "domain");
+                        if (!a)
+                                a = first_word(l, "search");
+                        if (a) {
+                                (void) parse_search_domain(a);
+                                continue;
+                        }
+
+                        log_syntax(NULL, LOG_DEBUG, "stdin", n, 0, "Ignoring resolv.conf line: %s", l);
+                }
+
+                if (type == TYPE_EXCLUSIVE) {
+
+                        /* If -x mode is selected, let's preferably route non-suffixed lookups to this interface. This
+                         * somewhat matches the original -x behaviour */
+
+                        r = strv_extend(&arg_set_domain, "~.");
+                        if (r < 0)
+                                return log_oom();
+
+                } else if (type == TYPE_PRIVATE)
+                        log_debug("Private DNS server data not supported, ignoring.");
+
+                if (!arg_set_dns)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "No DNS servers specified, refusing operation.");
+        }
+
+        return 1; /* work to do */
+}
diff --git a/src/resolve/resolvconf-compat.h b/src/resolve/resolvconf-compat.h
new file mode 100644
index 0000000..33a5318
--- /dev/null
+++ b/src/resolve/resolvconf-compat.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int resolvconf_parse_argv(int argc, char *argv[]);
diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c
new file mode 100644
index 0000000..7d13ed3
--- /dev/null
+++ b/src/resolve/resolvectl.c
@@ -0,0 +1,3449 @@
+/* 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 "hostname-util.h"
+#include "main-func.h"
+#include "missing_network.h"
+#include "netlink-util.h"
+#include "pager.h"
+#include "parse-argument.h"
+#include "parse-util.h"
+#include "pretty-print.h"
+#include "process-util.h"
+#include "resolvconf-compat.h"
+#include "resolvectl.h"
+#include "resolved-def.h"
+#include "resolved-dns-packet.h"
+#include "resolved-util.h"
+#include "socket-netlink.h"
+#include "sort-util.h"
+#include "stdio-util.h"
+#include "string-table.h"
+#include "strv.h"
+#include "terminal-util.h"
+#include "utf8.h"
+#include "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;
+        int ifi;
+
+        assert(s);
+
+        iface = strdup(s);
+        if (!iface)
+                return log_oom();
+
+        ifi = rtnl_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;
+}
+
+int ifname_resolvconf_mangle(const char *s) {
+        const char *dot;
+
+        assert(s);
+
+        dot = strchr(s, '.');
+        if (dot) {
+                _cleanup_free_ char *iface = NULL;
+
+                log_debug("Ignoring protocol specifier '%s'.", dot + 1);
+                iface = strndup(s, dot - s);
+                if (!iface)
+                        return log_oom();
+                return ifname_mangle(iface);
+        } else
+                return ifname_mangle(s);
+}
+
+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; Data was acquired via local or encrypted transport: %s%s\n",
+               rtt_str, ansi_normal(),
+               ansi_grey(),
+               yes_no(flags & SD_RESOLVED_AUTHENTICATED),
+               yes_no(flags & SD_RESOLVED_CONFIDENTIAL),
+               ansi_normal());
+
+        if ((flags & (SD_RESOLVED_FROM_MASK|SD_RESOLVED_SYNTHETIC)) != 0)
+                printf("%s-- Data from:%s%s%s%s%s%s\n",
+                       ansi_grey(),
+                       FLAGS_SET(flags, SD_RESOLVED_SYNTHETIC) ? " synthetic" : "",
+                       FLAGS_SET(flags, SD_RESOLVED_FROM_CACHE) ? " cache" : "",
+                       FLAGS_SET(flags, SD_RESOLVED_FROM_ZONE) ? " zone" : "",
+                       FLAGS_SET(flags, SD_RESOLVED_FROM_TRUST_ANCHOR) ? " trust-anchor" : "",
+                       FLAGS_SET(flags, SD_RESOLVED_FROM_NETWORK) ? " network" : "",
+                       ansi_normal());
+}
+
+static void print_ifindex_comment(int printed_so_far, int ifindex) {
+        char ifname[IF_NAMESIZE + 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 idna_candidate(const char *name, char **ret) {
+        _cleanup_free_ char *idnafied = NULL;
+        int r;
+
+        assert(name);
+        assert(ret);
+
+        r = dns_name_apply_idna(name, &idnafied);
+        if (r < 0)
+                return log_error_errno(r, "Failed to apply IDNA to name '%s': %m", name);
+        if (r > 0 && !streq(name, idnafied)) {
+                *ret = TAKE_PTR(idnafied);
+                return true;
+        }
+
+        *ret = NULL;
+        return false;
+}
+
+static bool single_label_nonsynthetic(const char *name) {
+        _cleanup_free_ char *first_label = NULL;
+        int r;
+
+        if (!dns_name_is_single_label(name))
+                return false;
+
+        if (is_localhost(name) || is_gateway_hostname(name))
+                return false;
+
+        r = resolve_system_hostname(NULL, &first_label);
+        if (r < 0) {
+                log_warning_errno(r, "Failed to determine the hostname: %m");
+                return false;
+        }
+
+        return !streq(name, first_label);
+}
+
+static int resolve_record(sd_bus *bus, const char *name, uint16_t class, uint16_t type, bool warn_missing) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL;
+        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+        _cleanup_free_ char *idnafied = NULL;
+        bool needs_authentication = false;
+        unsigned n = 0;
+        uint64_t flags;
+        usec_t ts;
+        int r;
+
+        assert(name);
+
+        log_debug("Resolving %s %s %s (interface %s).", name, dns_class_to_string(class), dns_type_to_string(type), isempty(arg_ifname) ? "*" : arg_ifname);
+
+        if (dns_name_dot_suffixed(name) == 0 && single_label_nonsynthetic(name))
+                log_notice("(Note that search domains are not appended when --type= is specified. "
+                           "Please specify fully qualified domain names, or remove --type= switch from invocation in order to request regular hostname resolution.)");
+
+        r = idna_candidate(name, &idnafied);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                log_notice("(Note that IDNA translation is not applied when --type= is specified. "
+                           "Please specify translated domain names — i.e. '%s' — when resolving raw records, or remove --type= switch from invocation in order to request regular hostname resolution.",
+                           idnafied);
+
+        r = bus_message_new_method_call(bus, &req, bus_resolve_mgr, "ResolveRecord");
+        if (r < 0)
+                return bus_log_create_error(r);
+
+        r = sd_bus_message_append(req, "isqqt", arg_ifindex, name, class, type, arg_flags);
+        if (r < 0)
+                return bus_log_create_error(r);
+
+        ts = now(CLOCK_MONOTONIC);
+
+        r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply);
+        if (r < 0) {
+                if (warn_missing || r != -ENXIO)
+                        log_error("%s: resolve call failed: %s", name, bus_error_message(&error, r));
+                return r;
+        }
+
+        ts = now(CLOCK_MONOTONIC) - ts;
+
+        r = sd_bus_message_enter_container(reply, 'a', "(iqqay)");
+        if (r < 0)
+                return bus_log_parse_error(r);
+
+        while ((r = sd_bus_message_enter_container(reply, 'r', "iqqay")) > 0) {
+                uint16_t c, t;
+                int ifindex;
+                const void *d;
+                size_t l;
+
+                assert_cc(sizeof(int) == sizeof(int32_t));
+
+                r = sd_bus_message_read(reply, "iqq", &ifindex, &c, &t);
+                if (r < 0)
+                        return bus_log_parse_error(r);
+
+                r = sd_bus_message_read_array(reply, 'y', &d, &l);
+                if (r < 0)
+                        return bus_log_parse_error(r);
+
+                r = sd_bus_message_exit_container(reply);
+                if (r < 0)
+                        return bus_log_parse_error(r);
+
+                if (arg_raw == RAW_PACKET) {
+                        uint64_t u64 = htole64(l);
+
+                        fwrite(&u64, sizeof(u64), 1, stdout);
+                        fwrite(d, 1, l, stdout);
+                } else {
+                        r = output_rr_packet(d, l, ifindex);
+                        if (r < 0)
+                                return r;
+                }
+
+                if (dns_type_needs_authentication(t))
+                        needs_authentication = true;
+
+                n++;
+        }
+        if (r < 0)
+                return bus_log_parse_error(r);
+
+        r = sd_bus_message_exit_container(reply);
+        if (r < 0)
+                return bus_log_parse_error(r);
+
+        r = sd_bus_message_read(reply, "t", &flags);
+        if (r < 0)
+                return bus_log_parse_error(r);
+
+        if (n == 0) {
+                if (warn_missing)
+                        log_error("%s: no records found", name);
+                return -ESRCH;
+        }
+
+        print_source(flags, ts);
+
+        if ((flags & SD_RESOLVED_AUTHENTICATED) == 0 && needs_authentication) {
+                fflush(stdout);
+
+                fprintf(stderr, "\n%s"
+                       "WARNING: The resources shown contain cryptographic key data which could not be\n"
+                       "         authenticated. It is not suitable to authenticate any communication.\n"
+                       "         This is usually indication that DNSSEC authentication was not enabled\n"
+                       "         or is not available for the selected protocol or DNS servers.%s\n",
+                       ansi_highlight_red(),
+                       ansi_normal());
+        }
+
+        return 0;
+}
+
+static int resolve_rfc4501(sd_bus *bus, const char *name) {
+        uint16_t type = 0, class = 0;
+        const char *p, *q, *n;
+        int r;
+
+        assert(bus);
+        assert(name);
+        assert(startswith(name, "dns:"));
+
+        /* Parse RFC 4501 dns: URIs */
+
+        p = name + 4;
+
+        if (p[0] == '/') {
+                const char *e;
+
+                if (p[1] != '/')
+                        goto invalid;
+
+                e = strchr(p + 2, '/');
+                if (!e)
+                        goto invalid;
+
+                if (e != p + 2)
+                        log_warning("DNS authority specification not supported; ignoring specified authority.");
+
+                p = e + 1;
+        }
+
+        q = strchr(p, '?');
+        if (q) {
+                n = strndupa(p, q - p);
+                q++;
+
+                for (;;) {
+                        const char *f;
+
+                        f = startswith_no_case(q, "class=");
+                        if (f) {
+                                _cleanup_free_ char *t = NULL;
+                                const char *e;
+
+                                if (class != 0)
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                               "DNS class specified twice.");
+
+                                e = strchrnul(f, ';');
+                                t = strndup(f, e - f);
+                                if (!t)
+                                        return log_oom();
+
+                                r = dns_class_from_string(t);
+                                if (r < 0)
+                                        return log_error_errno(r, "Unknown DNS class %s.", t);
+
+                                class = r;
+
+                                if (*e == ';') {
+                                        q = e + 1;
+                                        continue;
+                                }
+
+                                break;
+                        }
+
+                        f = startswith_no_case(q, "type=");
+                        if (f) {
+                                _cleanup_free_ char *t = NULL;
+                                const char *e;
+
+                                if (type != 0)
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                               "DNS type specified twice.");
+
+                                e = strchrnul(f, ';');
+                                t = strndup(f, e - f);
+                                if (!t)
+                                        return log_oom();
+
+                                r = dns_type_from_string(t);
+                                if (r < 0)
+                                        return log_error_errno(r, "Unknown DNS type %s: %m", t);
+
+                                type = r;
+
+                                if (*e == ';') {
+                                        q = e + 1;
+                                        continue;
+                                }
+
+                                break;
+                        }
+
+                        goto invalid;
+                }
+        } else
+                n = p;
+
+        if (class == 0)
+                class = arg_class ?: DNS_CLASS_IN;
+        if (type == 0)
+                type = arg_type ?: DNS_TYPE_A;
+
+        return resolve_record(bus, n, class, type, true);
+
+invalid:
+        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                               "Invalid DNS URI: %s", name);
+}
+
+static int verb_query(int argc, char **argv, void *userdata) {
+        sd_bus *bus = userdata;
+        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_infos = 0;
+
+        for (sd_netlink_message *i = reply; i; i = sd_netlink_message_next(i)) {
+                const char *name;
+                int ifindex;
+                uint16_t type;
+
+                r = sd_netlink_message_get_type(i, &type);
+                if (r < 0)
+                        return rtnl_log_parse_error(r);
+
+                if (type != RTM_NEWLINK)
+                        continue;
+
+                r = sd_rtnl_message_link_get_ifindex(i, &ifindex);
+                if (r < 0)
+                        return rtnl_log_parse_error(r);
+
+                if (ifindex == LOOPBACK_IFINDEX)
+                        continue;
+
+                r = sd_netlink_message_read_string(i, IFLA_IFNAME, &name);
+                if (r < 0)
+                        return rtnl_log_parse_error(r);
+
+                if (!GREEDY_REALLOC(infos, n_infos + 1))
+                        return log_oom();
+
+                infos[n_infos++] = (InterfaceInfo) { ifindex, name };
+        }
+
+        typesafe_qsort(infos, n_infos, interface_info_compare);
+
+        r = 0;
+        for (size_t i = 0; i < n_infos; i++) {
+                int q = status_ifindex(bus, infos[i].index, infos[i].name, mode, &empty_line);
+                if (q < 0 && r >= 0)
+                        r = q;
+        }
+
+        return r;
+}
+
+static int verb_status(int argc, char **argv, void *userdata) {
+        sd_bus *bus = userdata;
+        _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
+        int r = 0;
+
+        if (argc > 1) {
+                char **ifname;
+                bool empty_line = false;
+
+                STRV_FOREACH(ifname, argv + 1) {
+                        int ifindex, q;
+
+                        ifindex = rtnl_resolve_interface(&rtnl, *ifname);
+                        if (ifindex < 0) {
+                                log_warning_errno(ifindex, "Failed to resolve interface \"%s\", ignoring: %m", *ifname);
+                                continue;
+                        }
+
+                        q = status_ifindex(bus, ifindex, NULL, STATUS_ALL, &empty_line);
+                        if (q < 0)
+                                r = q;
+                }
+        } else
+                r = status_all(bus, STATUS_ALL);
+
+        return r;
+}
+
+static int call_dns(sd_bus *bus, char **dns, const BusLocator *locator, sd_bus_error *error, bool extended) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL;
+        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"
+               "     --validate=BOOL           Allow DNSSEC validation (default: yes)\n"
+               "     --synthesize=BOOL         Allow synthetic response (default: yes)\n"
+               "     --cache=BOOL              Allow response from cache (default: yes)\n"
+               "     --zone=BOOL               Allow response from locally registered mDNS/LLMNR\n"
+               "                               records (default: yes)\n"
+               "     --trust-anchor=BOOL       Allow response from local trust anchor (default: yes)\n"
+               "     --network=BOOL            Allow response from network (default: yes)\n"
+               "     --search=BOOL             Use search domains for single-label names (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)
+                                return log_error_errno(r, "Failed to parse RR record type %s: %m", optarg);
+
+                        arg_type = (uint16_t) r;
+                        assert((int) arg_type == r);
+
+                        arg_mode = MODE_RESOLVE_RECORD;
+                        break;
+
+                case 'c':
+                        if (streq(optarg, "help")) {
+                                help_dns_classes();
+                                return 0;
+                        }
+
+                        r = dns_class_from_string(optarg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse RR record class %s: %m", optarg);
+
+                        arg_class = (uint16_t) r;
+                        assert((int) arg_class == r);
+
+                        break;
+
+                case ARG_LEGEND:
+                        r = parse_boolean_argument("--legend=", optarg, &arg_legend);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                case 'p':
+                        if (streq(optarg, "help")) {
+                                help_protocol_types();
+                                return 0;
+                        } else if (streq(optarg, "dns"))
+                                arg_flags |= SD_RESOLVED_DNS;
+                        else if (streq(optarg, "llmnr"))
+                                arg_flags |= SD_RESOLVED_LLMNR;
+                        else if (streq(optarg, "llmnr-ipv4"))
+                                arg_flags |= SD_RESOLVED_LLMNR_IPV4;
+                        else if (streq(optarg, "llmnr-ipv6"))
+                                arg_flags |= SD_RESOLVED_LLMNR_IPV6;
+                        else if (streq(optarg, "mdns"))
+                                arg_flags |= SD_RESOLVED_MDNS;
+                        else if (streq(optarg, "mdns-ipv4"))
+                                arg_flags |= SD_RESOLVED_MDNS_IPV4;
+                        else if (streq(optarg, "mdns-ipv6"))
+                                arg_flags |= SD_RESOLVED_MDNS_IPV6;
+                        else
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Unknown protocol specifier: %s", optarg);
+
+                        break;
+
+                case ARG_SERVICE:
+                        arg_mode = MODE_RESOLVE_SERVICE;
+                        break;
+
+                case ARG_OPENPGP:
+                        arg_mode = MODE_RESOLVE_OPENPGP;
+                        break;
+
+                case ARG_TLSA:
+                        arg_mode = MODE_RESOLVE_TLSA;
+                        if (!optarg || service_family_is_valid(optarg))
+                                arg_service_family = optarg;
+                        else
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Unknown service family \"%s\".", optarg);
+                        break;
+
+                case ARG_RAW:
+                        if (on_tty())
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY),
+                                                       "Refusing to write binary data to tty.");
+
+                        if (optarg == NULL || streq(optarg, "payload"))
+                                arg_raw = RAW_PAYLOAD;
+                        else if (streq(optarg, "packet"))
+                                arg_raw = RAW_PACKET;
+                        else
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Unknown --raw specifier \"%s\".",
+                                                       optarg);
+
+                        arg_legend = false;
+                        break;
+
+                case ARG_CNAME:
+                        r = parse_boolean_argument("--cname=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_CNAME, r == 0);
+                        break;
+
+                case ARG_SERVICE_ADDRESS:
+                        r = parse_boolean_argument("--service-address=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_ADDRESS, r == 0);
+                        break;
+
+                case ARG_SERVICE_TXT:
+                        r = parse_boolean_argument("--service-txt=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0);
+                        break;
+
+                case ARG_SEARCH:
+                        r = parse_boolean_argument("--search=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_SEARCH, r == 0);
+                        break;
+
+                case ARG_STATISTICS:
+                        arg_mode = MODE_STATISTICS;
+                        break;
+
+                case ARG_RESET_STATISTICS:
+                        arg_mode = MODE_RESET_STATISTICS;
+                        break;
+
+                case ARG_FLUSH_CACHES:
+                        arg_mode = MODE_FLUSH_CACHES;
+                        break;
+
+                case ARG_RESET_SERVER_FEATURES:
+                        arg_mode = MODE_RESET_SERVER_FEATURES;
+                        break;
+
+                case ARG_STATUS:
+                        arg_mode = MODE_STATUS;
+                        break;
+
+                case ARG_NO_PAGER:
+                        arg_pager_flags |= PAGER_DISABLE;
+                        break;
+
+                case ARG_SET_DNS:
+                        r = strv_extend(&arg_set_dns, optarg);
+                        if (r < 0)
+                                return log_oom();
+
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case ARG_SET_DOMAIN:
+                        r = strv_extend(&arg_set_domain, optarg);
+                        if (r < 0)
+                                return log_oom();
+
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case ARG_SET_LLMNR:
+                        arg_set_llmnr = optarg;
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case ARG_SET_MDNS:
+                        arg_set_mdns = optarg;
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case ARG_SET_PRIVATE:
+                        arg_set_dns_over_tls = optarg;
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case ARG_SET_DNSSEC:
+                        arg_set_dnssec = optarg;
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case ARG_SET_NTA:
+                        r = strv_extend(&arg_set_nta, optarg);
+                        if (r < 0)
+                                return log_oom();
+
+                        arg_mode = MODE_SET_LINK;
+                        break;
+
+                case ARG_REVERT_LINK:
+                        arg_mode = MODE_REVERT_LINK;
+                        break;
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached("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_VALIDATE,
+                ARG_SYNTHESIZE,
+                ARG_CACHE,
+                ARG_ZONE,
+                ARG_TRUST_ANCHOR,
+                ARG_NETWORK,
+                ARG_SERVICE_ADDRESS,
+                ARG_SERVICE_TXT,
+                ARG_RAW,
+                ARG_SEARCH,
+                ARG_NO_PAGER,
+        };
+
+        static const struct option options[] = {
+                { "help",                  no_argument,       NULL, 'h'                       },
+                { "version",               no_argument,       NULL, ARG_VERSION               },
+                { "type",                  required_argument, NULL, 't'                       },
+                { "class",                 required_argument, NULL, 'c'                       },
+                { "legend",                required_argument, NULL, ARG_LEGEND                },
+                { "interface",             required_argument, NULL, 'i'                       },
+                { "protocol",              required_argument, NULL, 'p'                       },
+                { "cname",                 required_argument, NULL, ARG_CNAME                 },
+                { "validate",              required_argument, NULL, ARG_VALIDATE              },
+                { "synthesize",            required_argument, NULL, ARG_SYNTHESIZE            },
+                { "cache",                 required_argument, NULL, ARG_CACHE                 },
+                { "zone",                  required_argument, NULL, ARG_ZONE                  },
+                { "trust-anchor",          required_argument, NULL, ARG_TRUST_ANCHOR          },
+                { "network",               required_argument, NULL, ARG_NETWORK               },
+                { "service-address",       required_argument, NULL, ARG_SERVICE_ADDRESS       },
+                { "service-txt",           required_argument, NULL, ARG_SERVICE_TXT           },
+                { "raw",                   optional_argument, NULL, ARG_RAW                   },
+                { "search",                required_argument, NULL, ARG_SEARCH                },
+                { "no-pager",              no_argument,       NULL, ARG_NO_PAGER              },
+                {}
+        };
+
+        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)
+                                return log_error_errno(r, "Failed to parse RR record type %s: %m", optarg);
+
+                        arg_type = (uint16_t) r;
+                        assert((int) arg_type == r);
+
+                        break;
+
+                case 'c':
+                        if (streq(optarg, "help")) {
+                                help_dns_classes();
+                                return 0;
+                        }
+
+                        r = dns_class_from_string(optarg);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse RR record class %s: %m", optarg);
+
+                        arg_class = (uint16_t) r;
+                        assert((int) arg_class == r);
+
+                        break;
+
+                case ARG_LEGEND:
+                        r = parse_boolean_argument("--legend=", optarg, &arg_legend);
+                        if (r < 0)
+                                return r;
+                        break;
+
+                case 'p':
+                        if (streq(optarg, "help")) {
+                                help_protocol_types();
+                                return 0;
+                        } else if (streq(optarg, "dns"))
+                                arg_flags |= SD_RESOLVED_DNS;
+                        else if (streq(optarg, "llmnr"))
+                                arg_flags |= SD_RESOLVED_LLMNR;
+                        else if (streq(optarg, "llmnr-ipv4"))
+                                arg_flags |= SD_RESOLVED_LLMNR_IPV4;
+                        else if (streq(optarg, "llmnr-ipv6"))
+                                arg_flags |= SD_RESOLVED_LLMNR_IPV6;
+                        else if (streq(optarg, "mdns"))
+                                arg_flags |= SD_RESOLVED_MDNS;
+                        else if (streq(optarg, "mdns-ipv4"))
+                                arg_flags |= SD_RESOLVED_MDNS_IPV4;
+                        else if (streq(optarg, "mdns-ipv6"))
+                                arg_flags |= SD_RESOLVED_MDNS_IPV6;
+                        else
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Unknown protocol specifier: %s",
+                                                       optarg);
+
+                        break;
+
+                case ARG_RAW:
+                        if (on_tty())
+                                return log_error_errno(SYNTHETIC_ERRNO(ENOTTY),
+                                                       "Refusing to write binary data to tty.");
+
+                        if (optarg == NULL || streq(optarg, "payload"))
+                                arg_raw = RAW_PAYLOAD;
+                        else if (streq(optarg, "packet"))
+                                arg_raw = RAW_PACKET;
+                        else
+                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+                                                       "Unknown --raw specifier \"%s\".",
+                                                       optarg);
+
+                        arg_legend = false;
+                        break;
+
+                case ARG_CNAME:
+                        r = parse_boolean_argument("--cname=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_CNAME, r == 0);
+                        break;
+
+                case ARG_VALIDATE:
+                        r = parse_boolean_argument("--validate=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_VALIDATE, r == 0);
+                        break;
+
+                case ARG_SYNTHESIZE:
+                        r = parse_boolean_argument("--synthesize=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_SYNTHESIZE, r == 0);
+                        break;
+
+                case ARG_CACHE:
+                        r = parse_boolean_argument("--cache=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_CACHE, r == 0);
+                        break;
+
+                case ARG_ZONE:
+                        r = parse_boolean_argument("--zone=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_ZONE, r == 0);
+                        break;
+
+                case ARG_TRUST_ANCHOR:
+                        r = parse_boolean_argument("--trust-anchor=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_TRUST_ANCHOR, r == 0);
+                        break;
+
+                case ARG_NETWORK:
+                        r = parse_boolean_argument("--network=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_NETWORK, r == 0);
+                        break;
+
+                case ARG_SERVICE_ADDRESS:
+                        r = parse_boolean_argument("--service-address=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_ADDRESS, r == 0);
+                        break;
+
+                case ARG_SERVICE_TXT:
+                        r = parse_boolean_argument("--service-txt=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0);
+                        break;
+
+                case ARG_SEARCH:
+                        r = parse_boolean_argument("--search=", optarg, NULL);
+                        if (r < 0)
+                                return r;
+                        SET_FLAG(arg_flags, SD_RESOLVED_NO_SEARCH, r == 0);
+                        break;
+
+                case ARG_NO_PAGER:
+                        arg_pager_flags |= PAGER_DISABLE;
+                        break;
+
+                case '?':
+                        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();
+
+        if (invoked_as(argv, "resolvconf"))
+                r = resolvconf_parse_argv(argc, argv);
+        else if (invoked_as(argv, "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..1d0f147
--- /dev/null
+++ b/src/resolve/resolvectl.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <in-addr-util.h>
+#include <stdbool.h>
+#include <sys/types.h>
+
+typedef enum ExecutionMode {
+        MODE_RESOLVE_HOST,
+        MODE_RESOLVE_RECORD,
+        MODE_RESOLVE_SERVICE,
+        MODE_RESOLVE_OPENPGP,
+        MODE_RESOLVE_TLSA,
+        MODE_STATISTICS,
+        MODE_RESET_STATISTICS,
+        MODE_FLUSH_CACHES,
+        MODE_RESET_SERVER_FEATURES,
+        MODE_STATUS,
+        MODE_SET_LINK,
+        MODE_REVERT_LINK,
+        _MODE_INVALID = -EINVAL,
+} ExecutionMode;
+
+extern ExecutionMode arg_mode;
+extern char **arg_set_dns;
+extern char **arg_set_domain;
+extern bool arg_ifindex_permissive;
+
+int ifname_mangle(const char *s);
+int ifname_resolvconf_mangle(const char *s);
diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c
new file mode 100644
index 0000000..61d4b50
--- /dev/null
+++ b/src/resolve/resolved-bus.c
@@ -0,0 +1,2282 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-get-properties.h"
+#include "bus-log-control-api.h"
+#include "bus-message-util.h"
+#include "bus-polkit.h"
+#include "dns-domain.h"
+#include "format-util.h"
+#include "memory-util.h"
+#include "missing_capability.h"
+#include "resolved-bus.h"
+#include "resolved-def.h"
+#include "resolved-dns-synthesize.h"
+#include "resolved-dnssd-bus.h"
+#include "resolved-dnssd.h"
+#include "resolved-link-bus.h"
+#include "resolved-resolv-conf.h"
+#include "socket-netlink.h"
+#include "stdio-util.h"
+#include "strv.h"
+#include "syslog-util.h"
+#include "user-util.h"
+#include "utf8.h"
+
+BUS_DEFINE_PROPERTY_GET_ENUM(bus_property_get_resolve_support, resolve_support, ResolveSupport);
+
+static int query_on_bus_track(sd_bus_track *t, void *userdata) {
+        DnsQuery *q = 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_NO_SOURCE:
+                return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_NO_SOURCE, "All suitable resolution sources turned off");
+
+        case DNS_TRANSACTION_STUB_LOOP:
+                return sd_bus_reply_method_errorf(q->bus_request, BUS_ERROR_STUB_LOOP, "Configured DNS server loops back to us");
+
+        case DNS_TRANSACTION_RCODE_FAILURE: {
+                _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
+
+                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 *query) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        _cleanup_free_ char *normalized = NULL;
+        DnsQuestion *question;
+        DnsResourceRecord *rr;
+        unsigned added = 0;
+        int ifindex, r;
+
+        assert(q);
+
+        if (q->state != DNS_TRANSACTION_SUCCESS) {
+                r = reply_query_state(q);
+                goto finish;
+        }
+
+        r = dns_query_process_cname_many(q);
+        if (r == -ELOOP) {
+                r = 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_CNAME) {
+                /* This was a cname, and the query was restarted. */
+                TAKE_PTR(q);
+                return;
+        }
+
+        r = sd_bus_message_new_method_return(q->bus_request, &reply);
+        if (r < 0)
+                goto finish;
+
+        r = sd_bus_message_open_container(reply, 'a', "(iiay)");
+        if (r < 0)
+                goto finish;
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
+
+                r = dns_question_matches_rr(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue;
+
+                r = append_address(reply, rr, ifindex);
+                if (r < 0)
+                        goto finish;
+
+                if (!canonical)
+                        canonical = dns_resource_record_ref(rr);
+
+                added++;
+        }
+
+        if (added <= 0) {
+                r = 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,
+                        dns_query_reply_flags_make(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);
+        }
+}
+
+static int validate_and_mangle_flags(
+                const char *name,
+                uint64_t *flags,
+                uint64_t ok,
+                sd_bus_error *error) {
+
+        assert(flags);
+
+        /* Checks that the client supplied interface index and flags parameter actually are valid and make
+         * sense in our method call context. Specifically:
+         *
+         * 1. Checks that the interface index is either 0 (meaning *all* interfaces) or positive
+         *
+         * 2. Only the protocols flags and a bunch of NO_XYZ flags are set, at most. Plus additional flags
+         *    specific to our method, passed in the "ok" parameter.
+         *
+         * 3. If zero protocol flags are specified it is automatically turned into *all* protocols. This way
+         *    clients can simply pass 0 as flags and all will work as it should. They can also use this so
+         *    that clients don't have to know all the protocols resolved implements, but can just specify 0
+         *    to mean "all supported protocols".
+         */
+
+        if (*flags & ~(SD_RESOLVED_PROTOCOLS_ALL|
+                       SD_RESOLVED_NO_CNAME|
+                       SD_RESOLVED_NO_VALIDATE|
+                       SD_RESOLVED_NO_SYNTHESIZE|
+                       SD_RESOLVED_NO_CACHE|
+                       SD_RESOLVED_NO_ZONE|
+                       SD_RESOLVED_NO_TRUST_ANCHOR|
+                       SD_RESOLVED_NO_NETWORK|
+                       ok))
+                return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags parameter");
+
+        if ((*flags & SD_RESOLVED_PROTOCOLS_ALL) == 0) /* If no protocol is enabled, enable all */
+                *flags |= SD_RESOLVED_PROTOCOLS_ALL;
+
+        /* Imply SD_RESOLVED_NO_SEARCH if permitted and name is dot suffixed. */
+        if (name && FLAGS_SET(ok, SD_RESOLVED_NO_SEARCH) && dns_name_dot_suffixed(name) > 0)
+                *flags |= SD_RESOLVED_NO_SEARCH;
+
+        return 0;
+}
+
+static int parse_as_address(sd_bus_message *m, int ifindex, const char *hostname, int family, uint64_t flags) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_free_ char *canonical = NULL;
+        union in_addr_union parsed;
+        int r, ff, parsed_ifindex = 0;
+
+        /* Check if the hostname is actually already an IP address formatted as string. In that case just parse it,
+         * let's not attempt to look it up. */
+
+        r = in_addr_ifindex_from_string_auto(hostname, &ff, &parsed, &parsed_ifindex);
+        if (r < 0) /* not an address */
+                return 0;
+
+        if (family != AF_UNSPEC && ff != family)
+                return sd_bus_reply_method_errorf(m, BUS_ERROR_NO_SUCH_RR, "The specified address is not of the requested family.");
+        if (ifindex > 0 && parsed_ifindex > 0 && parsed_ifindex != ifindex)
+                return sd_bus_reply_method_errorf(m, BUS_ERROR_NO_SUCH_RR, "The specified address interface index does not match requested interface.");
+
+        if (parsed_ifindex > 0)
+                ifindex = parsed_ifindex;
+
+        r = sd_bus_message_new_method_return(m, &reply);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(reply, 'a', "(iiay)");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_open_container(reply, 'r', "iiay");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(reply, "ii", ifindex, ff);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append_array(reply, 'y', &parsed, FAMILY_ADDRESS_SIZE(ff));
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_close_container(reply);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_close_container(reply);
+        if (r < 0)
+                return r;
+
+        /* When an IP address is specified we just return it as canonical name, in order to avoid a DNS
+         * look-up. However, we reformat it to make sure it's in a truly canonical form (i.e. on IPv6 the inner
+         * omissions are always done the same way). */
+        r = in_addr_ifindex_to_string(ff, &parsed, ifindex, &canonical);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(reply, "st", canonical,
+                                  SD_RESOLVED_FLAGS_MAKE(dns_synthesize_protocol(flags), ff, true, true) |
+                                  SD_RESOLVED_SYNTHETIC);
+        if (r < 0)
+                return r;
+
+        return sd_bus_send(sd_bus_message_get_bus(m), reply, NULL);
+}
+
+void bus_client_log(sd_bus_message *m, const char *what) {
+        _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL;
+        const char *comm = NULL;
+        uid_t uid = UID_INVALID;
+        pid_t pid = 0;
+        int r;
+
+        assert(m);
+        assert(what);
+
+        if (!DEBUG_LOGGING)
+                return;
+
+        r = sd_bus_query_sender_creds(m, SD_BUS_CREDS_PID|SD_BUS_CREDS_UID|SD_BUS_CREDS_COMM|SD_BUS_CREDS_AUGMENT, &creds);
+        if (r < 0)
+                return (void) log_debug_errno(r, "Failed to query client credentials, ignoring: %m");
+
+        (void) sd_bus_creds_get_uid(creds, &uid);
+        (void) sd_bus_creds_get_pid(creds, &pid);
+        (void) sd_bus_creds_get_comm(creds, &comm);
+
+        log_debug("D-Bus %s request from client PID " PID_FMT " (%s) with UID " UID_FMT,
+                  what, pid, strna(comm), uid);
+}
+
+static int bus_method_resolve_hostname(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+        Manager *m = userdata;
+        const char *hostname;
+        int family, ifindex;
+        uint64_t flags;
+        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_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index");
+
+        if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Unknown address family %i", family);
+
+        r = validate_and_mangle_flags(hostname, &flags, SD_RESOLVED_NO_SEARCH, error);
+        if (r < 0)
+                return r;
+
+        r = parse_as_address(message, ifindex, hostname, family, flags);
+        if (r != 0)
+                return r;
+
+        r = dns_name_is_valid(hostname);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid hostname '%s'", hostname);
+
+        r = dns_question_new_address(&question_utf8, family, hostname, false);
+        if (r < 0)
+                return r;
+
+        r = dns_question_new_address(&question_idna, family, hostname, true);
+        if (r < 0 && r != -EALREADY)
+                return r;
+
+        bus_client_log(message, "hostname resolution");
+
+        r = dns_query_new(m, &q, question_utf8, question_idna ?: question_utf8, NULL, ifindex, flags);
+        if (r < 0)
+                return r;
+
+        q->bus_request = sd_bus_message_ref(message);
+        q->request_family = family;
+        q->complete = bus_method_resolve_hostname_complete;
+
+        r = dns_query_bus_track(q, message);
+        if (r < 0)
+                return r;
+
+        r = dns_query_go(q);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(q);
+        return 1;
+}
+
+static void bus_method_resolve_address_complete(DnsQuery *query) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        DnsQuestion *question;
+        DnsResourceRecord *rr;
+        unsigned added = 0;
+        int ifindex, r;
+
+        assert(q);
+
+        if (q->state != DNS_TRANSACTION_SUCCESS) {
+                r = reply_query_state(q);
+                goto finish;
+        }
+
+        r = dns_query_process_cname_many(q);
+        if (r == -ELOOP) {
+                r = 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_CNAME) {
+                /* This was a cname, and the query was restarted. */
+                TAKE_PTR(q);
+                return;
+        }
+
+        r = sd_bus_message_new_method_return(q->bus_request, &reply);
+        if (r < 0)
+                goto finish;
+
+        r = sd_bus_message_open_container(reply, 'a', "(is)");
+        if (r < 0)
+                goto finish;
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
+                _cleanup_free_ char *normalized = NULL;
+
+                r = dns_question_matches_rr(question, rr, NULL);
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue;
+
+                r = dns_name_normalize(rr->ptr.name, 0, &normalized);
+                if (r < 0)
+                        goto finish;
+
+                r = sd_bus_message_append(reply, "(is)", ifindex, normalized);
+                if (r < 0)
+                        goto finish;
+
+                added++;
+        }
+
+        if (added <= 0) {
+                _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", dns_query_reply_flags_make(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);
+        }
+}
+
+static int bus_method_resolve_address(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+        Manager *m = userdata;
+        union in_addr_union a;
+        int family, ifindex;
+        uint64_t flags;
+        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_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index");
+
+        r = validate_and_mangle_flags(NULL, &flags, 0, error);
+        if (r < 0)
+                return r;
+
+        r = dns_question_new_reverse(&question, family, &a);
+        if (r < 0)
+                return r;
+
+        bus_client_log(message, "address resolution");
+
+        r = dns_query_new(m, &q, question, question, NULL, ifindex, flags|SD_RESOLVED_NO_SEARCH);
+        if (r < 0)
+                return r;
+
+        q->bus_request = sd_bus_message_ref(message);
+        q->request_family = family;
+        q->request_address = a;
+        q->complete = bus_method_resolve_address_complete;
+
+        r = dns_query_bus_track(q, message);
+        if (r < 0)
+                return r;
+
+        r = dns_query_go(q);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(q);
+        return 1;
+}
+
+static int bus_message_append_rr(sd_bus_message *m, DnsResourceRecord *rr, int ifindex) {
+        int r;
+
+        assert(m);
+        assert(rr);
+
+        r = sd_bus_message_open_container(m, 'r', "iqqay");
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append(m, "iqq",
+                                  ifindex,
+                                  rr->key->class,
+                                  rr->key->type);
+        if (r < 0)
+                return r;
+
+        r = dns_resource_record_to_wire_format(rr, false);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append_array(m, 'y', rr->wire_format, rr->wire_format_size);
+        if (r < 0)
+                return r;
+
+        return sd_bus_message_close_container(m);
+}
+
+static void bus_method_resolve_record_complete(DnsQuery *query) {
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        DnsResourceRecord *rr;
+        DnsQuestion *question;
+        unsigned added = 0;
+        int ifindex;
+        int r;
+
+        assert(q);
+
+        if (q->state != DNS_TRANSACTION_SUCCESS) {
+                r = reply_query_state(q);
+                goto finish;
+        }
+
+        r = dns_query_process_cname_many(q);
+        if (r == -ELOOP) {
+                r = 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_CNAME) {
+                /* This was a cname, and the query was restarted. */
+                TAKE_PTR(q);
+                return;
+        }
+
+        r = sd_bus_message_new_method_return(q->bus_request, &reply);
+        if (r < 0)
+                goto finish;
+
+        r = sd_bus_message_open_container(reply, 'a', "(iqqay)");
+        if (r < 0)
+                goto finish;
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
+                r = dns_question_matches_rr(question, rr, NULL);
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue;
+
+                r = bus_message_append_rr(reply, rr, ifindex);
+                if (r < 0)
+                        goto finish;
+
+                added++;
+        }
+
+        if (added <= 0) {
+                r = 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", dns_query_reply_flags_make(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);
+        }
+}
+
+static int bus_method_resolve_record(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+        _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+        Manager *m = userdata;
+        uint16_t class, type;
+        const char *name;
+        int r, ifindex;
+        uint64_t flags;
+
+        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_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index");
+
+        r = dns_name_is_valid(name);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid name '%s'", name);
+
+        if (!dns_type_is_valid_query(type))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Specified resource record type %" PRIu16 " may not be used in a query.", type);
+        if (dns_type_is_zone_transer(type))
+                return sd_bus_error_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Zone transfers not permitted via this programming interface.");
+        if (dns_type_is_obsolete(type))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Specified DNS resource record type %" PRIu16 " is obsolete.", type);
+
+        r = validate_and_mangle_flags(name, &flags, 0, error);
+        if (r < 0)
+                return r;
+
+        question = dns_question_new(1);
+        if (!question)
+                return -ENOMEM;
+
+        key = dns_resource_key_new(class, type, name);
+        if (!key)
+                return -ENOMEM;
+
+        r = dns_question_add(question, key, 0);
+        if (r < 0)
+                return r;
+
+        bus_client_log(message, "resource record resolution");
+
+        /* Setting SD_RESOLVED_CLAMP_TTL: let's request that the TTL is fixed up for locally cached entries,
+         * after all we return it in the wire format blob. */
+        r = dns_query_new(m, &q, question, question, NULL, ifindex, flags|SD_RESOLVED_NO_SEARCH|SD_RESOLVED_CLAMP_TTL);
+        if (r < 0)
+                return r;
+
+        q->bus_request = sd_bus_message_ref(message);
+        q->complete = bus_method_resolve_record_complete;
+
+        r = dns_query_bus_track(q, message);
+        if (r < 0)
+                return r;
+
+        r = dns_query_go(q);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(q);
+        return 1;
+}
+
+static int append_srv(DnsQuery *q, sd_bus_message *reply, DnsResourceRecord *rr) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
+        _cleanup_free_ char *normalized = NULL;
+        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 *query) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
+        _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
+        _cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        DnsQuestion *question;
+        DnsResourceRecord *rr;
+        unsigned added = 0;
+        DnsQuery *aux;
+        int r;
+
+        assert(q);
+
+        if (q->block_all_complete > 0) {
+                TAKE_PTR(q);
+                return;
+        }
+
+        if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) {
+                DnsQuery *bad = NULL;
+                bool have_success = false;
+
+                LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) {
+
+                        switch (aux->state) {
+
+                        case DNS_TRANSACTION_PENDING:
+                                /* If an auxiliary query is still pending, let's wait */
+                                TAKE_PTR(q);
+                                return;
+
+                        case DNS_TRANSACTION_SUCCESS:
+                                if (aux->auxiliary_result == 0)
+                                        have_success = true;
+                                else
+                                        bad = aux;
+                                break;
+
+                        default:
+                                bad = aux;
+                                break;
+                        }
+                }
+
+                if (!have_success) {
+                        /* We can only return one error, hence pick the last error we encountered */
+
+                        assert(bad);
+
+                        if (bad->state == DNS_TRANSACTION_SUCCESS) {
+                                assert(bad->auxiliary_result != 0);
+
+                                if (bad->auxiliary_result == -ELOOP) {
+                                        r = 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;
+                                }
+
+                                assert(bad->auxiliary_result < 0);
+                                r = bad->auxiliary_result;
+                                goto finish;
+                        }
+
+                        r = reply_query_state(bad);
+                        goto finish;
+                }
+        }
+
+        r = sd_bus_message_new_method_return(q->bus_request, &reply);
+        if (r < 0)
+                goto finish;
+
+        r = sd_bus_message_open_container(reply, 'a', "(qqqsa(iiay)s)");
+        if (r < 0)
+                goto finish;
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        DNS_ANSWER_FOREACH(rr, q->answer) {
+                r = dns_question_matches_rr(question, rr, NULL);
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue;
+
+                r = append_srv(q, reply, rr);
+                if (r < 0)
+                        goto finish;
+                if (r == 0) /* not an SRV record */
+                        continue;
+
+                if (!canonical)
+                        canonical = dns_resource_record_ref(rr);
+
+                added++;
+        }
+
+        if (added <= 0) {
+                r = 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,
+                        dns_query_reply_flags_make(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);
+        }
+}
+
+static void resolve_service_hostname_complete(DnsQuery *q) {
+        int r;
+
+        assert(q);
+        assert(q->auxiliary_for);
+
+        if (q->state != DNS_TRANSACTION_SUCCESS) {
+                resolve_service_all_complete(q->auxiliary_for);
+                return;
+        }
+
+        r = dns_query_process_cname_many(q);
+        if (r == DNS_QUERY_CNAME) /* This was a cname, and the query was restarted. */
+                return;
+
+        /* This auxiliary lookup is finished or failed, let's see if all are finished now. */
+        q->auxiliary_result = r < 0 ? r : 0;
+        resolve_service_all_complete(q->auxiliary_for);
+}
+
+static int resolve_service_hostname(DnsQuery *q, DnsResourceRecord *rr, int ifindex) {
+        _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *aux = NULL;
+        int r;
+
+        assert(q);
+        assert(rr);
+        assert(rr->key);
+        assert(rr->key->type == DNS_TYPE_SRV);
+
+        /* OK, we found an SRV record for the service. Let's resolve
+         * the hostname included in it */
+
+        r = dns_question_new_address(&question, q->request_family, rr->srv.name, false);
+        if (r < 0)
+                return r;
+
+        r = dns_query_new(q->manager, &aux, question, question, NULL, ifindex, q->flags|SD_RESOLVED_NO_SEARCH);
+        if (r < 0)
+                return r;
+
+        aux->bus_request = sd_bus_message_ref(q->bus_request);
+        aux->request_family = q->request_family;
+        aux->complete = resolve_service_hostname_complete;
+
+        r = dns_query_make_auxiliary(aux, q);
+        if (r == -EAGAIN)
+                /* Too many auxiliary lookups? If so, don't complain,
+                 * let's just not add this one, we already have more
+                 * than enough */
+                return 0;
+        if (r < 0)
+                return r;
+
+        /* Note that auxiliary queries do not track the original bus
+         * client, only the primary request does that. */
+
+        r = dns_query_go(aux);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(aux);
+        return 1;
+}
+
+static void bus_method_resolve_service_complete(DnsQuery *query) {
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        bool has_root_domain = false;
+        DnsResourceRecord *rr;
+        DnsQuestion *question;
+        unsigned found = 0;
+        int ifindex, r;
+
+        assert(q);
+
+        if (q->state != DNS_TRANSACTION_SUCCESS) {
+                r = reply_query_state(q);
+                goto finish;
+        }
+
+        r = dns_query_process_cname_many(q);
+        if (r == -ELOOP) {
+                r = 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_CNAME) {
+                /* This was a cname, and the query was restarted. */
+                TAKE_PTR(q);
+                return;
+        }
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
+                r = dns_question_matches_rr(question, rr, NULL);
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue;
+
+                if (rr->key->type != DNS_TYPE_SRV)
+                        continue;
+
+                if (dns_name_is_root(rr->srv.name)) {
+                        has_root_domain = true;
+                        continue;
+                }
+
+                if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) {
+                        q->block_all_complete++;
+                        r = resolve_service_hostname(q, rr, ifindex);
+                        q->block_all_complete--;
+
+                        if (r < 0)
+                                goto finish;
+                }
+
+                found++;
+        }
+
+        if (has_root_domain && found <= 0) {
+                /* If there's exactly one SRV RR and it uses
+                 * the root domain as hostname, then the
+                 * service is explicitly not offered on the
+                 * domain. Report this as a recognizable
+                 * error. See RFC 2782, Section "Usage
+                 * Rules". */
+                r = 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(TAKE_PTR(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);
+        }
+}
+
+static int bus_method_resolve_service(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+        const char *name, *type, *domain;
+        Manager *m = userdata;
+        int family, ifindex;
+        uint64_t flags;
+        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_set(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid interface index");
+
+        if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Unknown address family %i", family);
+
+        if (isempty(name))
+                name = NULL;
+        else if (!dns_service_name_is_valid(name))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid service name '%s'", name);
+
+        if (isempty(type))
+                type = NULL;
+        else if (!dns_srv_type_is_valid(type))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid SRV service type '%s'", type);
+
+        r = dns_name_is_valid(domain);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid domain '%s'", domain);
+
+        if (name && !type)
+                return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Service name cannot be specified without service type.");
+
+        r = validate_and_mangle_flags(name, &flags, SD_RESOLVED_NO_TXT|SD_RESOLVED_NO_ADDRESS, error);
+        if (r < 0)
+                return r;
+
+        r = dns_question_new_service(&question_utf8, name, type, domain, !(flags & SD_RESOLVED_NO_TXT), false);
+        if (r < 0)
+                return r;
+
+        r = dns_question_new_service(&question_idna, name, type, domain, !(flags & SD_RESOLVED_NO_TXT), true);
+        if (r < 0)
+                return r;
+
+        bus_client_log(message, "service resolution");
+
+        r = dns_query_new(m, &q, question_utf8, question_idna, NULL, ifindex, flags|SD_RESOLVED_NO_SEARCH);
+        if (r < 0)
+                return r;
+
+        q->bus_request = sd_bus_message_ref(message);
+        q->request_family = family;
+        q->complete = bus_method_resolve_service_complete;
+
+        r = dns_query_bus_track(q, message);
+        if (r < 0)
+                return r;
+
+        r = dns_query_go(q);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(q);
+        return 1;
+}
+
+int bus_dns_server_append(sd_bus_message *reply, DnsServer *s, bool with_ifindex, bool extended) {
+        int r;
+
+        assert(reply);
+
+        if (!s) {
+                if (with_ifindex) {
+                        if (extended)
+                                return sd_bus_message_append(reply, "(iiayqs)", 0, AF_UNSPEC, 0, 0, NULL);
+                        else
+                                return sd_bus_message_append(reply, "(iiay)", 0, AF_UNSPEC, 0);
+                } else {
+                        if (extended)
+                                return sd_bus_message_append(reply, "(iayqs)", AF_UNSPEC, 0, 0, NULL);
+                        else
+                                return sd_bus_message_append(reply, "(iay)", AF_UNSPEC, 0);
+                }
+        }
+
+        r = sd_bus_message_open_container(reply, 'r', with_ifindex ? (extended ? "iiayqs" : "iiay") : (extended ? "iayqs" : "iay"));
+        if (r < 0)
+                return r;
+
+        if (with_ifindex) {
+                r = sd_bus_message_append(reply, "i", dns_server_ifindex(s));
+                if (r < 0)
+                        return r;
+        }
+
+        r = sd_bus_message_append(reply, "i", s->family);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_message_append_array(reply, 'y', &s->address, FAMILY_ADDRESS_SIZE(s->family));
+        if (r < 0)
+                return r;
+
+        if (extended) {
+                r = sd_bus_message_append(reply, "q", s->port);
+                if (r < 0)
+                        return r;
+
+                r = sd_bus_message_append(reply, "s", s->server_name);
+                if (r < 0)
+                        return r;
+        }
+
+        return sd_bus_message_close_container(reply);
+}
+
+static int bus_property_get_dns_servers_internal(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                const char *property,
+                sd_bus_message *reply,
+                void *userdata,
+                sd_bus_error *error,
+                bool extended) {
+
+        Manager *m = 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);
+
+        bus_client_log(message, "statistics reset");
+
+        LIST_FOREACH(scopes, s, m->dns_scopes)
+                s->cache.n_hit = s->cache.n_miss = 0;
+
+        m->n_transactions_total = 0;
+        zero(m->n_dnssec_verdict);
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+static int get_any_link(Manager *m, int ifindex, Link **ret, sd_bus_error *error) {
+        Link *l;
+
+        assert(m);
+        assert(ret);
+
+        l = hashmap_get(m->links, INT_TO_PTR(ifindex));
+        if (!l)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_LINK, "Link %i not known", ifindex);
+
+        *ret = l;
+        return 0;
+}
+
+static int call_link_method(Manager *m, sd_bus_message *message, sd_bus_message_handler_t handler, sd_bus_error *error) {
+        int ifindex, r;
+        Link *l;
+
+        assert(m);
+        assert(message);
+        assert(handler);
+
+        r = bus_message_read_ifindex(message, error, &ifindex);
+        if (r < 0)
+                return r;
+
+        r = get_any_link(m, ifindex, &l, error);
+        if (r < 0)
+                return r;
+
+        return handler(message, l, error);
+}
+
+static int bus_method_set_link_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_dns_servers, error);
+}
+
+static int bus_method_set_link_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_dns_servers_ex, error);
+}
+
+static int bus_method_set_link_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_domains, error);
+}
+
+static int bus_method_set_link_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_default_route, error);
+}
+
+static int bus_method_set_link_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_llmnr, error);
+}
+
+static int bus_method_set_link_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_mdns, error);
+}
+
+static int bus_method_set_link_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_dns_over_tls, error);
+}
+
+static int bus_method_set_link_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_dnssec, error);
+}
+
+static int bus_method_set_link_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_set_dnssec_negative_trust_anchors, error);
+}
+
+static int bus_method_revert_link(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return call_link_method(userdata, message, bus_link_method_revert, error);
+}
+
+static int bus_method_get_link(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_free_ char *p = NULL;
+        Manager *m = 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);
+
+        bus_client_log(message, "cache flush");
+
+        manager_flush_caches(m, LOG_INFO);
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+static int bus_method_reset_server_features(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Manager *m = userdata;
+
+        assert(message);
+        assert(m);
+
+        bus_client_log(message, "server feature reset");
+
+        manager_reset_server_features(m);
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+static int dnssd_service_on_bus_track(sd_bus_track *t, void *userdata) {
+        DnssdService *s = 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_set(error, SD_BUS_ERROR_NOT_SUPPORTED, "Support for MulticastDNS is disabled");
+
+        service = new0(DnssdService, 1);
+        if (!service)
+                return log_oom();
+
+        r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_creds_get_euid(creds, &euid);
+        if (r < 0)
+                return r;
+        service->originator = euid;
+
+        r = sd_bus_message_read(message, "sssqqq", &name, &name_template, &type,
+                                &service->port, &service->priority,
+                                &service->weight);
+        if (r < 0)
+                return r;
+
+        s = hashmap_get(m->dnssd_services, name);
+        if (s)
+                return sd_bus_error_setf(error, BUS_ERROR_DNSSD_SERVICE_EXISTS, "DNS-SD service '%s' exists already", name);
+
+        if (!dnssd_srv_type_is_valid(type))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "DNS-SD service type '%s' is invalid", type);
+
+        service->name = strdup(name);
+        if (!service->name)
+                return log_oom();
+
+        service->name_template = strdup(name_template);
+        if (!service->name_template)
+                return log_oom();
+
+        service->type = strdup(type);
+        if (!service->type)
+                return log_oom();
+
+        r = dnssd_render_instance_name(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_set(error, SD_BUS_ERROR_INVALID_ARGS, "Keys in DNS-SD TXT RRs can't be empty");
+
+                        if (!ascii_is_valid(key))
+                                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "TXT key '%s' contains non-ASCII symbols", key);
+
+                        r = sd_bus_message_read_array(message, 'y', &value, &size);
+                        if (r < 0)
+                                return r;
+
+                        r = dnssd_txt_item_new_from_data(key, value, size, &i);
+                        if (r < 0)
+                                return r;
+
+                        LIST_INSERT_AFTER(items, txt_data->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_put(&m->dnssd_services, &string_hash_ops, service->name, service);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_track_new(sd_bus_message_get_bus(message), &bus_track, dnssd_service_on_bus_track, service);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_track_add_sender(bus_track, message);
+        if (r < 0)
+                return r;
+
+        service->manager = m;
+
+        service = NULL;
+
+        manager_refresh_rrs(m);
+
+        return sd_bus_reply_method_return(message, "o", path);
+}
+
+static int call_dnssd_method(Manager *m, sd_bus_message *message, sd_bus_message_handler_t handler, sd_bus_error *error) {
+        _cleanup_free_ char *name = NULL;
+        DnssdService *s = NULL;
+        const char *path;
+        int r;
+
+        assert(m);
+        assert(message);
+        assert(handler);
+
+        r = sd_bus_message_read(message, "o", &path);
+        if (r < 0)
+                return r;
+
+        r = sd_bus_path_decode(path, "/org/freedesktop/resolve1/dnssd", &name);
+        if (r == 0)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_DNSSD_SERVICE, "DNS-SD service with object path '%s' does not exist", path);
+        if (r < 0)
+                return r;
+
+        s = hashmap_get(m->dnssd_services, name);
+        if (!s)
+                return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_DNSSD_SERVICE, "DNS-SD service '%s' not known", name);
+
+        return handler(message, s, error);
+}
+
+static int bus_method_unregister_service(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Manager *m = 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);
+
+        if (sd_bus_is_ready(manager->bus) <= 0)
+                return 0;
+
+        char **l = strv_from_stdarg_alloca(property);
+
+        int r = sd_bus_emit_properties_changed_strv(
+                        manager->bus,
+                        "/org/freedesktop/resolve1",
+                        "org.freedesktop.resolve1.Manager",
+                        l);
+        if (r < 0)
+                log_notice_errno(r, "Failed to emit notification about changed property %s: %m", property);
+        return r;
+}
diff --git a/src/resolve/resolved-bus.h b/src/resolve/resolved-bus.h
new file mode 100644
index 0000000..6c2bd26
--- /dev/null
+++ b/src/resolve/resolved-bus.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "bus-object.h"
+#include "resolved-manager.h"
+
+extern const BusObjectImplementation manager_object;
+
+int manager_connect_bus(Manager *m);
+int _manager_send_changed(Manager *manager, const char *property, ...) _sentinel_;
+#define manager_send_changed(manager, ...) _manager_send_changed(manager, __VA_ARGS__, NULL)
+int bus_dns_server_append(sd_bus_message *reply, DnsServer *s, bool with_ifindex, bool extended);
+int bus_property_get_resolve_support(sd_bus *bus, const char *path, const char *interface,
+                                     const char *property, sd_bus_message *reply,
+                                     void *userdata, sd_bus_error *error);
+
+void bus_client_log(sd_bus_message *m, const char *what);
diff --git a/src/resolve/resolved-conf.c b/src/resolve/resolved-conf.c
new file mode 100644
index 0000000..283c063
--- /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, DNS_LABEL_MAX, specifier_table, NULL, NULL, &name);
+        if (r < 0) {
+                log_syntax(unit, LOG_WARNING, filename, line, r,
+                           "Invalid service instance name template '%s', ignoring assignment: %m", rvalue);
+                return 0;
+        }
+
+        if (!dns_service_name_is_valid(name)) {
+                log_syntax(unit, LOG_WARNING, filename, line, 0,
+                           "Service instance name template '%s' renders to invalid name '%s'. Ignoring assignment.",
+                           rvalue, name);
+                return 0;
+        }
+
+        return free_and_strdup_warn(&s->name_template, rvalue);
+}
+
+int config_parse_dnssd_service_type(
+                const char *unit,
+                const char *filename,
+                unsigned line,
+                const char *section,
+                unsigned section_line,
+                const char *lvalue,
+                int ltype,
+                const char *rvalue,
+                void *data,
+                void *userdata) {
+
+        DnssdService *s = 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_UNESCAPE_RELAX);
+                if (r == 0)
+                        break;
+                if (r == -ENOMEM)
+                        return log_oom();
+                if (r < 0) {
+                        log_syntax(unit, LOG_WARNING, filename, line, r, "Invalid syntax, ignoring: %s", rvalue);
+                        return 0;
+                }
+
+                r = split_pair(word, "=", &key, &value);
+                if (r == -ENOMEM)
+                        return log_oom();
+                if (r == -EINVAL)
+                        key = TAKE_PTR(word);
+
+                if (!ascii_is_valid(key)) {
+                        log_syntax(unit, LOG_WARNING, filename, line, 0, "Invalid key, ignoring: %s", key);
+                        continue;
+                }
+
+                switch (ltype) {
+
+                case DNS_TXT_ITEM_DATA:
+                        if (value) {
+                                r = unbase64mem(value, strlen(value), &decoded, &length);
+                                if (r == -ENOMEM)
+                                        return log_oom();
+                                if (r < 0) {
+                                        log_syntax(unit, LOG_WARNING, filename, line, r,
+                                                   "Invalid base64 encoding, ignoring: %s", value);
+                                        continue;
+                                }
+                        }
+
+                        r = dnssd_txt_item_new_from_data(key, decoded, length, &i);
+                        if (r < 0)
+                                return log_oom();
+                        break;
+
+                case DNS_TXT_ITEM_TEXT:
+                        r = dnssd_txt_item_new_from_string(key, value, &i);
+                        if (r < 0)
+                                return log_oom();
+                        break;
+
+                default:
+                        assert_not_reached("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..36092af
--- /dev/null
+++ b/src/resolve/resolved-def.h
@@ -0,0 +1,79 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <inttypes.h>
+
+#include "time-util.h"
+
+/* Input + Output: The various protocols we can use */
+#define SD_RESOLVED_DNS             (UINT64_C(1) << 0)
+#define SD_RESOLVED_LLMNR_IPV4      (UINT64_C(1) << 1)
+#define SD_RESOLVED_LLMNR_IPV6      (UINT64_C(1) << 2)
+#define SD_RESOLVED_MDNS_IPV4       (UINT64_C(1) << 3)
+#define SD_RESOLVED_MDNS_IPV6       (UINT64_C(1) << 4)
+
+/* Input: Don't follow CNAMEs/DNAMEs */
+#define SD_RESOLVED_NO_CNAME        (UINT64_C(1) << 5)
+
+/* Input: When doing service (SRV) resolving, don't resolve associated mDNS-style TXT records */
+#define SD_RESOLVED_NO_TXT          (UINT64_C(1) << 6)
+
+/* Input: When doing service (SRV) resolving, don't resolve A/AAA RR for included hostname */
+#define SD_RESOLVED_NO_ADDRESS      (UINT64_C(1) << 7)
+
+/* Input: Don't apply search domain logic to request */
+#define SD_RESOLVED_NO_SEARCH       (UINT64_C(1) << 8)
+
+/* Output: Result is authenticated */
+#define SD_RESOLVED_AUTHENTICATED   (UINT64_C(1) << 9)
+
+/* Input: Don't DNSSEC validate request */
+#define SD_RESOLVED_NO_VALIDATE     (UINT64_C(1) << 10)
+
+/* Input: Don't answer request from locally synthesized records (which includes /etc/hosts) */
+#define SD_RESOLVED_NO_SYNTHESIZE   (UINT64_C(1) << 11)
+
+/* Input: Don't answer request from cache */
+#define SD_RESOLVED_NO_CACHE        (UINT64_C(1) << 12)
+
+/* Input: Don't answer request from locally registered public LLMNR/mDNS RRs */
+#define SD_RESOLVED_NO_ZONE         (UINT64_C(1) << 13)
+
+/* Input: Don't answer request from locally configured trust anchors. */
+#define SD_RESOLVED_NO_TRUST_ANCHOR (UINT64_C(1) << 14)
+
+/* Input: Don't go to network for this request */
+#define SD_RESOLVED_NO_NETWORK      (UINT64_C(1) << 15)
+
+/* Input: Require that request is answered from a "primary" answer, i.e. not from RRs acquired as
+ * side-effect of a previous transaction */
+#define SD_RESOLVED_REQUIRE_PRIMARY (UINT64_C(1) << 16)
+
+/* Input: If reply is answered from cache, the TTLs will be adjusted by age of cache entry */
+#define SD_RESOLVED_CLAMP_TTL       (UINT64_C(1) << 17)
+
+/* Output: Result was only sent via encrypted channels, or never left this system */
+#define SD_RESOLVED_CONFIDENTIAL    (UINT64_C(1) << 18)
+
+/* Output: Result was (at least partially) synthesized locally */
+#define SD_RESOLVED_SYNTHETIC       (UINT64_C(1) << 19)
+
+/* Output: Result was (at least partially) answered from cache */
+#define SD_RESOLVED_FROM_CACHE      (UINT64_C(1) << 20)
+
+/* Output: Result was (at least partially) answered from local zone */
+#define SD_RESOLVED_FROM_ZONE       (UINT64_C(1) << 21)
+
+/* Output: Result was (at least partially) answered from trust anchor */
+#define SD_RESOLVED_FROM_TRUST_ANCHOR (UINT64_C(1) << 22)
+
+/* Output: Result was (at least partially) answered from network */
+#define SD_RESOLVED_FROM_NETWORK    (UINT64_C(1) << 23)
+
+#define SD_RESOLVED_LLMNR           (SD_RESOLVED_LLMNR_IPV4|SD_RESOLVED_LLMNR_IPV6)
+#define SD_RESOLVED_MDNS            (SD_RESOLVED_MDNS_IPV4|SD_RESOLVED_MDNS_IPV6)
+#define SD_RESOLVED_PROTOCOLS_ALL   (SD_RESOLVED_MDNS|SD_RESOLVED_LLMNR|SD_RESOLVED_DNS)
+
+#define SD_RESOLVED_FROM_MASK       (SD_RESOLVED_FROM_CACHE|SD_RESOLVED_FROM_ZONE|SD_RESOLVED_FROM_TRUST_ANCHOR|SD_RESOLVED_FROM_NETWORK)
+
+#define SD_RESOLVED_QUERY_TIMEOUT_USEC (120 * USEC_PER_SEC)
diff --git a/src/resolve/resolved-dns-answer.c b/src/resolve/resolved-dns-answer.c
new file mode 100644
index 0000000..978d148
--- /dev/null
+++ b/src/resolve/resolved-dns-answer.c
@@ -0,0 +1,983 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <stdio.h>
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "random-util.h"
+#include "resolved-dns-answer.h"
+#include "resolved-dns-dnssec.h"
+#include "string-util.h"
+
+static void dns_answer_item_hash_func(const DnsAnswerItem *a, struct siphash *state) {
+        assert(a);
+        assert(state);
+
+        siphash24_compress(&a->ifindex, sizeof(a->ifindex), state);
+
+        dns_resource_record_hash_func(a->rr, state);
+}
+
+static int dns_answer_item_compare_func(const DnsAnswerItem *a, const DnsAnswerItem *b) {
+        int r;
+
+        assert(a);
+        assert(b);
+
+        r = CMP(a->ifindex, b->ifindex);
+        if (r != 0)
+                return r;
+
+        return dns_resource_record_compare_func(a->rr, b->rr);
+}
+
+DEFINE_PRIVATE_HASH_OPS(dns_answer_item_hash_ops, DnsAnswerItem, dns_answer_item_hash_func, dns_answer_item_compare_func);
+
+DnsAnswer *dns_answer_new(size_t n) {
+        _cleanup_set_free_ Set *s = NULL;
+        DnsAnswer *a;
+
+        if (n > UINT16_MAX) /* We can only place 64K RRs in an answer at max */
+                n = UINT16_MAX;
+
+        s = set_new(&dns_answer_item_hash_ops);
+        if (!s)
+                return NULL;
+
+        /* Higher multipliers give slightly higher efficiency through hash collisions, but the gains
+         * quickly drop off after 2. */
+        if (set_reserve(s, n * 2) < 0)
+                return NULL;
+
+        a = malloc0(offsetof(DnsAnswer, items) + sizeof(DnsAnswerItem) * n);
+        if (!a)
+                return NULL;
+
+        a->n_ref = 1;
+        a->n_allocated = n;
+        a->set_items = TAKE_PTR(s);
+        return a;
+}
+
+static void dns_answer_flush(DnsAnswer *a) {
+        DnsAnswerItem *item;
+
+        if (!a)
+                return;
+
+        a->set_items = set_free(a->set_items);
+
+        DNS_ANSWER_FOREACH_ITEM(item, a) {
+                dns_resource_record_unref(item->rr);
+                dns_resource_record_unref(item->rrsig);
+        }
+
+        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,
+                DnsResourceRecord *rrsig) {
+
+        int r;
+
+        assert(rr);
+
+        if (!a)
+                return -ENOSPC;
+
+        if (a->n_rrs >= a->n_allocated)
+                return -ENOSPC;
+
+        a->items[a->n_rrs] = (DnsAnswerItem) {
+                .rr = rr,
+                .ifindex = ifindex,
+                .flags = flags,
+                .rrsig = dns_resource_record_ref(rrsig),
+        };
+
+        r = set_put(a->set_items, &a->items[a->n_rrs]);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -EEXIST;
+
+        dns_resource_record_ref(rr);
+        a->n_rrs++;
+
+        return 1;
+}
+
+static int dns_answer_add_raw_all(DnsAnswer *a, DnsAnswer *source) {
+        DnsAnswerItem *item;
+        int r;
+
+        DNS_ANSWER_FOREACH_ITEM(item, source) {
+                r = dns_answer_add_raw(
+                                a,
+                                item->rr,
+                                item->ifindex,
+                                item->flags,
+                                item->rrsig);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int dns_answer_add(
+                DnsAnswer *a,
+                DnsResourceRecord *rr,
+                int ifindex,
+                DnsAnswerFlags flags,
+                DnsResourceRecord *rrsig) {
+
+        DnsAnswerItem tmp, *exist;
+
+        assert(rr);
+
+        if (!a)
+                return -ENOSPC;
+        if (a->n_ref > 1)
+                return -EBUSY;
+
+        tmp = (DnsAnswerItem) {
+                .rr = rr,
+                .ifindex = ifindex,
+        };
+
+        exist = set_get(a->set_items, &tmp);
+        if (exist) {
+                /* There's already an RR of the same RRset in place! Let's see if the TTLs more or less
+                 * match. We don't really care if they match precisely, but we do care whether one is 0 and
+                 * the other is not. See RFC 2181, Section 5.2. */
+                if ((rr->ttl == 0) != (exist->rr->ttl == 0))
+                        return -EINVAL;
+
+                /* Entry already exists, keep the entry with the higher TTL. */
+                if (rr->ttl > exist->rr->ttl) {
+                        dns_resource_record_unref(exist->rr);
+                        exist->rr = dns_resource_record_ref(rr);
+
+                        /* Update RRSIG and RR at the same time */
+                        if (rrsig) {
+                                dns_resource_record_ref(rrsig);
+                                dns_resource_record_unref(exist->rrsig);
+                                exist->rrsig = rrsig;
+                        }
+                }
+
+                exist->flags |= flags;
+                return 0;
+        }
+
+        return dns_answer_add_raw(a, rr, ifindex, flags, rrsig);
+}
+
+static int dns_answer_add_all(DnsAnswer *a, DnsAnswer *b) {
+        DnsAnswerItem *item;
+        int r;
+
+        DNS_ANSWER_FOREACH_ITEM(item, b) {
+                r = dns_answer_add(a, item->rr, item->ifindex, item->flags, item->rrsig);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int dns_answer_add_extend(
+                DnsAnswer **a,
+                DnsResourceRecord *rr,
+                int ifindex,
+                DnsAnswerFlags flags,
+                DnsResourceRecord *rrsig) {
+
+        int r;
+
+        assert(a);
+        assert(rr);
+
+        r = dns_answer_reserve_or_clone(a, 1);
+        if (r < 0)
+                return r;
+
+        return dns_answer_add(*a, rr, ifindex, flags, rrsig);
+}
+
+int dns_answer_add_soa(DnsAnswer *a, const char *name, uint32_t ttl, int ifindex) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *soa = NULL;
+
+        soa = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_SOA, name);
+        if (!soa)
+                return -ENOMEM;
+
+        soa->ttl = ttl;
+
+        soa->soa.mname = strdup(name);
+        if (!soa->soa.mname)
+                return -ENOMEM;
+
+        soa->soa.rname = strjoin("root.", name);
+        if (!soa->soa.rname)
+                return -ENOMEM;
+
+        soa->soa.serial = 1;
+        soa->soa.refresh = 1;
+        soa->soa.retry = 1;
+        soa->soa.expire = 1;
+        soa->soa.minimum = ttl;
+
+        return dns_answer_add(a, soa, ifindex, DNS_ANSWER_AUTHENTICATED, NULL);
+}
+
+int dns_answer_match_key(DnsAnswer *a, const DnsResourceKey *key, DnsAnswerFlags *ret_flags) {
+        DnsAnswerFlags flags = 0, i_flags;
+        DnsResourceRecord *i;
+        bool found = false;
+        int r;
+
+        assert(key);
+
+        DNS_ANSWER_FOREACH_FLAGS(i, i_flags, a) {
+                r = dns_resource_key_match_rr(key, i, NULL);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                if (!ret_flags)
+                        return 1;
+
+                if (found)
+                        flags &= i_flags;
+                else {
+                        flags = i_flags;
+                        found = true;
+                }
+        }
+
+        if (ret_flags)
+                *ret_flags = flags;
+
+        return found;
+}
+
+bool dns_answer_contains_nsec_or_nsec3(DnsAnswer *a) {
+        DnsResourceRecord *i;
+
+        DNS_ANSWER_FOREACH(i, a)
+                if (IN_SET(i->key->type, DNS_TYPE_NSEC, DNS_TYPE_NSEC3))
+                        return true;
+
+        return false;
+}
+
+int dns_answer_contains_zone_nsec3(DnsAnswer *answer, const char *zone) {
+        DnsResourceRecord *rr;
+        int r;
+
+        /* Checks whether the specified answer contains at least one NSEC3 RR in the specified zone */
+
+        DNS_ANSWER_FOREACH(rr, answer) {
+                const char *p;
+
+                if (rr->key->type != DNS_TYPE_NSEC3)
+                        continue;
+
+                p = dns_resource_key_name(rr->key);
+                r = dns_name_parent(&p);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                r = dns_name_equal(p, zone);
+                if (r != 0)
+                        return r;
+        }
+
+        return false;
+}
+
+bool dns_answer_contains(DnsAnswer *answer, DnsResourceRecord *rr) {
+        DnsResourceRecord *i;
+
+        DNS_ANSWER_FOREACH(i, answer)
+                if (dns_resource_record_equal(i, rr))
+                        return true;
+
+        return false;
+}
+
+int dns_answer_find_soa(
+                DnsAnswer *a,
+                const DnsResourceKey *key,
+                DnsResourceRecord **ret,
+                DnsAnswerFlags *ret_flags) {
+
+        DnsResourceRecord *rr, *soa = NULL;
+        DnsAnswerFlags rr_flags, soa_flags = 0;
+        int r;
+
+        assert(key);
+
+        /* For a SOA record we can never find a matching SOA record */
+        if (key->type == DNS_TYPE_SOA)
+                goto not_found;
+
+        DNS_ANSWER_FOREACH_FLAGS(rr, rr_flags, a) {
+                r = dns_resource_key_match_soa(key, rr->key);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+
+                        if (soa) {
+                                r = dns_name_endswith(dns_resource_key_name(rr->key), dns_resource_key_name(soa->key));
+                                if (r < 0)
+                                        return r;
+                                if (r > 0)
+                                        continue;
+                        }
+
+                        soa = rr;
+                        soa_flags = rr_flags;
+                }
+        }
+
+        if (!soa)
+                goto not_found;
+
+        if (ret)
+                *ret = soa;
+        if (ret_flags)
+                *ret_flags = soa_flags;
+
+        return 1;
+
+not_found:
+        if (ret)
+                *ret = NULL;
+        if (ret_flags)
+                *ret_flags = 0;
+
+        return 0;
+}
+
+int dns_answer_find_cname_or_dname(
+                DnsAnswer *a,
+                const DnsResourceKey *key,
+                DnsResourceRecord **ret,
+                DnsAnswerFlags *ret_flags) {
+
+        DnsResourceRecord *rr;
+        DnsAnswerFlags rr_flags;
+        int r;
+
+        assert(key);
+
+        /* For a {C,D}NAME record we can never find a matching {C,D}NAME record */
+        if (!dns_type_may_redirect(key->type))
+                return 0;
+
+        DNS_ANSWER_FOREACH_FLAGS(rr, rr_flags, a) {
+                r = dns_resource_key_match_cname_or_dname(key, rr->key, NULL);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        if (ret)
+                                *ret = rr;
+                        if (ret_flags)
+                                *ret_flags = rr_flags;
+                        return 1;
+                }
+        }
+
+        if (ret)
+                *ret = NULL;
+        if (ret_flags)
+                *ret_flags = 0;
+
+        return 0;
+}
+
+int dns_answer_merge(DnsAnswer *a, DnsAnswer *b, DnsAnswer **ret) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *k = NULL;
+        int r;
+
+        assert(ret);
+
+        if (a == b) {
+                *ret = dns_answer_ref(a);
+                return 0;
+        }
+
+        if (dns_answer_size(a) <= 0) {
+                *ret = dns_answer_ref(b);
+                return 0;
+        }
+
+        if (dns_answer_size(b) <= 0) {
+                *ret = dns_answer_ref(a);
+                return 0;
+        }
+
+        k = dns_answer_new(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;
+                DnsAnswerItem *item;
+
+                copy = dns_answer_new((*a)->n_rrs);
+                if (!copy)
+                        return -ENOMEM;
+
+                DNS_ANSWER_FOREACH_ITEM(item, *a) {
+                        r = dns_resource_key_equal(item->rr->key, key);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                continue;
+
+                        r = dns_answer_add_raw(copy, item->rr, item->ifindex, item->flags, item->rrsig);
+                        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);
+                        dns_resource_record_unref((*a)->items[i].rrsig);
+
+                        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;
+                DnsAnswerItem *item;
+
+                copy = dns_answer_new((*a)->n_rrs);
+                if (!copy)
+                        return -ENOMEM;
+
+                DNS_ANSWER_FOREACH_ITEM(item, *a) {
+                        r = dns_resource_record_equal(item->rr, rm);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                continue;
+
+                        r = dns_answer_add_raw(copy, item->rr, item->ifindex, item->flags, item->rrsig);
+                        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);
+                        dns_resource_record_unref((*a)->items[i].rrsig);
+                        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_answer_keys(DnsAnswer **a, DnsAnswer *b) {
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *prev = NULL;
+        DnsAnswerItem *item;
+        int r;
+
+        /* Removes all items from '*a' that have a matching key in 'b' */
+
+        DNS_ANSWER_FOREACH_ITEM(item, b) {
+
+                if (prev && dns_resource_key_equal(item->rr->key, prev)) /* Skip this one, we already looked at it */
+                        continue;
+
+                r = dns_answer_remove_by_key(a, item->rr->key);
+                if (r < 0)
+                        return r;
+
+                /* Let's remember this entry's RR key, to optimize the loop a bit: if we have an RRset with
+                 * more than one item then we don't need to remove the key multiple times */
+                dns_resource_key_unref(prev);
+                prev = dns_resource_key_ref(item->rr->key);
+        }
+
+        return 0;
+}
+
+int dns_answer_copy_by_key(
+                DnsAnswer **a,
+                DnsAnswer *source,
+                const DnsResourceKey *key,
+                DnsAnswerFlags or_flags,
+                DnsResourceRecord *rrsig) {
+
+        DnsAnswerItem *item;
+        int r;
+
+        assert(a);
+        assert(key);
+
+        /* Copy all RRs matching the specified key from source into *a */
+
+        DNS_ANSWER_FOREACH_ITEM(item, source) {
+
+                r = dns_resource_key_equal(item->rr->key, key);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                /* 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, item->rr, item->ifindex, item->flags|or_flags, rrsig ?: item->rrsig);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int dns_answer_move_by_key(
+                DnsAnswer **to,
+                DnsAnswer **from,
+                const DnsResourceKey *key,
+                DnsAnswerFlags or_flags,
+                DnsResourceRecord *rrsig) {
+        int r;
+
+        assert(to);
+        assert(from);
+        assert(key);
+
+        r = dns_answer_copy_by_key(to, *from, key, or_flags, rrsig);
+        if (r < 0)
+                return r;
+
+        return dns_answer_remove_by_key(from, key);
+}
+
+void dns_answer_order_by_scope(DnsAnswer *a, bool prefer_link_local) {
+        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 (dns_resource_record_is_link_local_address(a->items[i].rr) != 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;
+                int r;
+
+                if ((*a)->n_ref > 1)
+                        return -EBUSY;
+
+                ns = (*a)->n_rrs;
+                assert(ns <= UINT16_MAX); /* Maximum number of RRs we can stick into a DNS packet section */
+
+                if (n_free > UINT16_MAX - ns) /* overflow check */
+                        ns = UINT16_MAX;
+                else
+                        ns += n_free;
+
+                if ((*a)->n_allocated >= ns)
+                        return 0;
+
+                /* Allocate more than we need, but not more than UINT16_MAX */
+                if (ns <= UINT16_MAX/2)
+                        ns *= 2;
+                else
+                        ns = UINT16_MAX;
+
+                /* This must be done before realloc() below. Otherwise, the original DnsAnswer object
+                 * may be broken. */
+                r = set_reserve((*a)->set_items, ns);
+                if (r < 0)
+                        return r;
+
+                n = realloc(*a, offsetof(DnsAnswer, items) + sizeof(DnsAnswerItem) * ns);
+                if (!n)
+                        return -ENOMEM;
+
+                n->n_allocated = ns;
+
+                /* Previously all items are stored in the set, and the enough memory area is allocated
+                 * in the above. So set_put() in the below cannot fail. */
+                set_clear(n->set_items);
+                for (size_t i = 0; i < n->n_rrs; i++)
+                        assert_se(set_put(n->set_items, &n->items[i]) > 0);
+        } 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) {
+        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. */
+
+        if (*a && (*a)->n_ref > 1) {
+                _cleanup_(dns_answer_unrefp) DnsAnswer *n = NULL;
+                size_t ns;
+
+                ns = (*a)->n_rrs;
+                assert(ns <= UINT16_MAX); /* Maximum number of RRs we can stick into a DNS packet section */
+
+                if (n_free > UINT16_MAX - ns) /* overflow check */
+                        ns = UINT16_MAX;
+                else if (n_free > 0) { /* Increase size and double the result, just in case — except if the
+                                        * increase is specified as 0, in which case we just allocate the
+                                        * exact amount as before, under the assumption this is just a request
+                                        * to copy the answer. */
+                        ns += n_free;
+
+                        if (ns <= UINT16_MAX/2) /* overflow check */
+                                ns *= 2;
+                        else
+                                ns = UINT16_MAX;
+                }
+
+                n = dns_answer_new(ns);
+                if (!n)
+                        return -ENOMEM;
+
+                r = dns_answer_add_raw_all(n, *a);
+                if (r < 0)
+                        return r;
+
+                dns_answer_unref(*a);
+                assert_se(*a = TAKE_PTR(n));
+        } else if (n_free > 0) {
+                r = dns_answer_reserve(a, n_free);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+/*
+ * This function is not used in the code base, but is useful when debugging. Do not delete.
+ */
+void dns_answer_dump(DnsAnswer *answer, FILE *f) {
+        DnsAnswerItem *item;
+
+        if (!f)
+                f = stdout;
+
+        DNS_ANSWER_FOREACH_ITEM(item, answer) {
+                const char *t;
+
+                fputc('\t', f);
+
+                t = dns_resource_record_to_string(item->rr);
+                if (!t) {
+                        log_oom();
+                        continue;
+                }
+
+                fputs(t, f);
+                fputs("\t;", f);
+                fprintf(f, " ttl=%" PRIu32, item->rr->ttl);
+
+                if (item->ifindex != 0)
+                        fprintf(f, " ifindex=%i", item->ifindex);
+                if (item->rrsig)
+                        fputs(" rrsig", f);
+                if (item->flags & DNS_ANSWER_AUTHENTICATED)
+                        fputs(" authenticated", f);
+                if (item->flags & DNS_ANSWER_CACHEABLE)
+                        fputs(" cacheable", f);
+                if (item->flags & DNS_ANSWER_SHARED_OWNER)
+                        fputs(" shared-owner", f);
+                if (item->flags & DNS_ANSWER_CACHE_FLUSH)
+                        fputs(" cache-flush", f);
+                if (item->flags & DNS_ANSWER_GOODBYE)
+                        fputs(" goodbye", f);
+                if (item->flags & DNS_ANSWER_SECTION_ANSWER)
+                        fputs(" section-answer", f);
+                if (item->flags & DNS_ANSWER_SECTION_AUTHORITY)
+                        fputs(" section-authority", f);
+                if (item->flags & DNS_ANSWER_SECTION_ADDITIONAL)
+                        fputs(" section-additional", f);
+
+                fputc('\n', f);
+        }
+}
+
+int dns_answer_has_dname_for_cname(DnsAnswer *a, DnsResourceRecord *cname) {
+        DnsResourceRecord *rr;
+        int r;
+
+        assert(cname);
+
+        /* Checks whether the answer contains a DNAME record that indicates that the specified CNAME record is
+         * synthesized from it */
+
+        if (cname->key->type != DNS_TYPE_CNAME)
+                return 0;
+
+        DNS_ANSWER_FOREACH(rr, a) {
+                _cleanup_free_ char *n = NULL;
+
+                if (rr->key->type != DNS_TYPE_DNAME)
+                        continue;
+                if (rr->key->class != cname->key->class)
+                        continue;
+
+                r = dns_name_change_suffix(cname->cname.name, rr->dname.name, dns_resource_key_name(rr->key), &n);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                r = dns_name_equal(n, dns_resource_key_name(cname->key));
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        return 1;
+        }
+
+        return 0;
+}
+
+void dns_answer_randomize(DnsAnswer *a) {
+        size_t n;
+
+        /* Permutes the answer list randomly (Knuth shuffle) */
+
+        n = dns_answer_size(a);
+        if (n <= 1)
+                return;
+
+        for (size_t i = 0; i < n; i++) {
+                size_t k;
+
+                k = random_u64_range(n);
+                if (k == i)
+                        continue;
+
+                SWAP_TWO(a->items[i], a->items[k]);
+        }
+}
+
+uint32_t dns_answer_min_ttl(DnsAnswer *a) {
+        uint32_t ttl = UINT32_MAX;
+        DnsResourceRecord *rr;
+
+        /* Return the smallest TTL of all RRs in this answer */
+
+        DNS_ANSWER_FOREACH(rr, a) {
+                /* Don't consider OPT (where the TTL field is used for other purposes than an actual TTL) */
+
+                if (dns_type_is_pseudo(rr->key->type) ||
+                    dns_class_is_pseudo(rr->key->class))
+                        continue;
+
+                ttl = MIN(ttl, rr->ttl);
+        }
+
+        return ttl;
+}
diff --git a/src/resolve/resolved-dns-answer.h b/src/resolve/resolved-dns-answer.h
new file mode 100644
index 0000000..3eb573b
--- /dev/null
+++ b/src/resolve/resolved-dns-answer.h
@@ -0,0 +1,140 @@
+/* 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"
+#include "set.h"
+
+/* A simple array of resource records. We keep track of the originating ifindex for each RR where that makes
+ * sense, so that we can qualify A and AAAA RRs referring to a local link with the right ifindex.
+ *
+ * Note that we usually encode the empty DnsAnswer object as a simple NULL. */
+
+typedef enum DnsAnswerFlags {
+        DNS_ANSWER_AUTHENTICATED      = 1 << 0, /* Item has been authenticated */
+        DNS_ANSWER_CACHEABLE          = 1 << 1, /* Item is subject to caching */
+        DNS_ANSWER_SHARED_OWNER       = 1 << 2, /* For mDNS: RRset may be owner by multiple peers */
+        DNS_ANSWER_CACHE_FLUSH        = 1 << 3, /* For mDNS: sets cache-flush bit in the rrclass of response records */
+        DNS_ANSWER_GOODBYE            = 1 << 4, /* For mDNS: item is subject to disappear */
+        DNS_ANSWER_SECTION_ANSWER     = 1 << 5, /* When parsing: RR originates from answer section */
+        DNS_ANSWER_SECTION_AUTHORITY  = 1 << 6, /* When parsing: RR originates from authority section */
+        DNS_ANSWER_SECTION_ADDITIONAL = 1 << 7, /* When parsing: RR originates from additional section */
+
+        DNS_ANSWER_MASK_SECTIONS      = DNS_ANSWER_SECTION_ANSWER|
+                                        DNS_ANSWER_SECTION_AUTHORITY|
+                                        DNS_ANSWER_SECTION_ADDITIONAL,
+} DnsAnswerFlags;
+
+struct DnsAnswerItem {
+        DnsResourceRecord *rr;
+        DnsResourceRecord *rrsig; /* Optionally, also store RRSIG RR that successfully validates this item */
+        int ifindex;
+        DnsAnswerFlags flags;
+};
+
+struct DnsAnswer {
+        unsigned n_ref;
+        Set *set_items; /* Used by dns_answer_add() for optimization. */
+        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, DnsResourceRecord *rrsig);
+int dns_answer_add_extend(DnsAnswer **a, DnsResourceRecord *rr, int ifindex, DnsAnswerFlags flags, DnsResourceRecord *rrsig);
+int dns_answer_add_soa(DnsAnswer *a, const char *name, uint32_t ttl, int ifindex);
+
+int dns_answer_match_key(DnsAnswer *a, const DnsResourceKey *key, DnsAnswerFlags *ret_flags);
+bool dns_answer_contains_nsec_or_nsec3(DnsAnswer *a);
+int dns_answer_contains_zone_nsec3(DnsAnswer *answer, const char *zone);
+bool dns_answer_contains(DnsAnswer *answer, DnsResourceRecord *rr);
+
+int dns_answer_find_soa(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *ret_flags);
+int dns_answer_find_cname_or_dname(DnsAnswer *a, const DnsResourceKey *key, DnsResourceRecord **ret, DnsAnswerFlags *ret_flags);
+
+int dns_answer_merge(DnsAnswer *a, DnsAnswer *b, DnsAnswer **ret);
+int dns_answer_extend(DnsAnswer **a, DnsAnswer *b);
+
+void dns_answer_order_by_scope(DnsAnswer *a, bool prefer_link_local);
+
+int dns_answer_reserve(DnsAnswer **a, size_t n_free);
+int dns_answer_reserve_or_clone(DnsAnswer **a, size_t n_free);
+
+int dns_answer_remove_by_key(DnsAnswer **a, const DnsResourceKey *key);
+int dns_answer_remove_by_rr(DnsAnswer **a, DnsResourceRecord *rr);
+int dns_answer_remove_by_answer_keys(DnsAnswer **a, DnsAnswer *b);
+
+int dns_answer_copy_by_key(DnsAnswer **a, DnsAnswer *source, const DnsResourceKey *key, DnsAnswerFlags or_flags, DnsResourceRecord *rrsig);
+int dns_answer_move_by_key(DnsAnswer **to, DnsAnswer **from, const DnsResourceKey *key, DnsAnswerFlags or_flags, DnsResourceRecord *rrsig);
+
+int dns_answer_has_dname_for_cname(DnsAnswer *a, DnsResourceRecord *cname);
+
+static inline size_t dns_answer_size(DnsAnswer *a) {
+        return a ? 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);
+
+void dns_answer_randomize(DnsAnswer *a);
+
+uint32_t dns_answer_min_ttl(DnsAnswer *a);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsAnswer*, dns_answer_unref);
+
+#define _DNS_ANSWER_FOREACH(q, kk, a)                                   \
+        for (size_t UNIQ_T(i, q) = ({                                   \
+                        (kk) = dns_answer_isempty(a) ? NULL : (a)->items[0].rr; \
+                        0;                                              \
+                });                                                     \
+             UNIQ_T(i, q) < dns_answer_size(a);                         \
+             UNIQ_T(i, q)++,                                            \
+                     (kk) = UNIQ_T(i, q) < dns_answer_size(a) ? (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) = dns_answer_isempty(a) ? NULL : (a)->items[0].rr; \
+                                (ifi) = dns_answer_isempty(a) ? 0 : (a)->items[0].ifindex; \
+                                0;                                      \
+                        });                                             \
+             UNIQ_T(i, q) < dns_answer_size(a);                         \
+             UNIQ_T(i, q)++,                                            \
+                     (kk) = UNIQ_T(i, q) < dns_answer_size(a) ? (a)->items[UNIQ_T(i, q)].rr : NULL, \
+                     (ifi) = UNIQ_T(i, q) < dns_answer_size(a) ? (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) = dns_answer_isempty(a) ? NULL : (a)->items[0].rr; \
+                                (fl) = dns_answer_isempty(a) ? 0 : (a)->items[0].flags; \
+                                0;                                      \
+                        });                                             \
+             UNIQ_T(i, q) < dns_answer_size(a);                         \
+             UNIQ_T(i, q)++,                                            \
+                     (kk) = UNIQ_T(i, q) < dns_answer_size(a) ? (a)->items[UNIQ_T(i, q)].rr : NULL, \
+                     (fl) = UNIQ_T(i, q) < dns_answer_size(a) ? (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_ITEM(q, item, a)                            \
+        for (size_t UNIQ_T(i, q) = ({                                   \
+                                (item) = dns_answer_isempty(a) ? NULL : (a)->items; \
+                                0;                                      \
+                        });                                             \
+             UNIQ_T(i, q) < dns_answer_size(a);                         \
+             UNIQ_T(i, q)++,                                            \
+                     (item) = (UNIQ_T(i, q) < dns_answer_size(a)) ? (a)->items + UNIQ_T(i, q) : NULL)
+
+#define DNS_ANSWER_FOREACH_ITEM(item, a) _DNS_ANSWER_FOREACH_ITEM(UNIQ, item, a)
diff --git a/src/resolve/resolved-dns-cache.c b/src/resolve/resolved-dns-cache.c
new file mode 100644
index 0000000..2a9a600
--- /dev/null
+++ b/src/resolve/resolved-dns-cache.c
@@ -0,0 +1,1360 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+
+#include "af-list.h"
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "format-util.h"
+#include "resolved-dns-answer.h"
+#include "resolved-dns-cache.h"
+#include "resolved-dns-packet.h"
+#include "string-util.h"
+
+/* Never cache more than 4K entries. RFC 1536, Section 5 suggests to
+ * leave DNS caches unbounded, but that's crazy. */
+#define CACHE_MAX 4096
+
+/* We never keep any item longer than 2h in our cache */
+#define CACHE_TTL_MAX_USEC (2 * USEC_PER_HOUR)
+
+/* How long to cache strange rcodes, i.e. rcodes != SUCCESS and != NXDOMAIN (specifically: that's only SERVFAIL for
+ * now) */
+#define CACHE_TTL_STRANGE_RCODE_USEC (10 * USEC_PER_SEC)
+
+#define CACHEABLE_QUERY_FLAGS (SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL)
+
+typedef enum DnsCacheItemType DnsCacheItemType;
+typedef struct DnsCacheItem DnsCacheItem;
+
+enum DnsCacheItemType {
+        DNS_CACHE_POSITIVE,
+        DNS_CACHE_NODATA,
+        DNS_CACHE_NXDOMAIN,
+        DNS_CACHE_RCODE,      /* "strange" RCODE (effective only SERVFAIL for now) */
+};
+
+struct DnsCacheItem {
+        DnsCacheItemType type;
+        int rcode;
+        DnsResourceKey *key;     /* The key for this item, i.e. the lookup key */
+        DnsResourceRecord *rr;   /* The RR for this item, i.e. the lookup value for positive queries */
+        DnsAnswer *answer;       /* The full validated answer, if this is an RRset acquired via a "primary" lookup */
+        DnsPacket *full_packet;  /* The full packet this information was acquired with */
+
+        usec_t until;
+        uint64_t query_flags;    /* SD_RESOLVED_AUTHENTICATED and/or SD_RESOLVED_CONFIDENTIAL */
+        DnssecResult dnssec_result;
+
+        int ifindex;
+        int owner_family;
+        union in_addr_union owner_address;
+
+        unsigned prioq_idx;
+        LIST_FIELDS(DnsCacheItem, by_key);
+
+        bool shared_owner;
+};
+
+/* Returns true if this is a cache item created as result of an explicit lookup, or created as "side-effect"
+ * of another request. "Primary" entries will carry the full answer data (with NSEC, …) that can aso prove
+ * wildcard expansion, non-existance and such, while entries that were created as "side-effect" just contain
+ * immediate RR data for the specified RR key, but nothing else. */
+#define DNS_CACHE_ITEM_IS_PRIMARY(item) (!!(item)->answer)
+
+static const char *dns_cache_item_type_to_string(DnsCacheItem *item) {
+        assert(item);
+
+        switch (item->type) {
+
+        case DNS_CACHE_POSITIVE:
+                return "POSITIVE";
+
+        case DNS_CACHE_NODATA:
+                return "NODATA";
+
+        case DNS_CACHE_NXDOMAIN:
+                return "NXDOMAIN";
+
+        case DNS_CACHE_RCODE:
+                return dns_rcode_to_string(item->rcode);
+        }
+
+        return NULL;
+}
+
+static DnsCacheItem* dns_cache_item_free(DnsCacheItem *i) {
+        if (!i)
+                return NULL;
+
+        dns_resource_record_unref(i->rr);
+        dns_resource_key_unref(i->key);
+        dns_answer_unref(i->answer);
+        dns_packet_unref(i->full_packet);
+        return mfree(i);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsCacheItem*, dns_cache_item_free);
+
+static void dns_cache_item_unlink_and_free(DnsCache *c, DnsCacheItem *i) {
+        DnsCacheItem *first;
+
+        assert(c);
+
+        if (!i)
+                return;
+
+        first = hashmap_get(c->by_key, i->key);
+        LIST_REMOVE(by_key, first, i);
+
+        if (first)
+                assert_se(hashmap_replace(c->by_key, first->key, first) >= 0);
+        else
+                hashmap_remove(c->by_key, i->key);
+
+        prioq_remove(c->by_expiry, i, &i->prioq_idx);
+
+        dns_cache_item_free(i);
+}
+
+static bool dns_cache_remove_by_rr(DnsCache *c, DnsResourceRecord *rr) {
+        DnsCacheItem *first, *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) {
+                _unused_ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *k = NULL;
+
+                /* Keep a reference to the original key, while we manipulate the list. */
+                k = dns_resource_key_ref(first->key);
+
+                /* Now, try to reduce the number of keys we keep */
+                dns_resource_key_reduce(&first->key, &i->key);
+
+                if (first->rr)
+                        dns_resource_key_reduce(&first->rr->key, &i->key);
+                if (i->rr)
+                        dns_resource_key_reduce(&i->rr->key, &i->key);
+
+                LIST_PREPEND(by_key, first, i);
+                assert_se(hashmap_replace(c->by_key, first->key, first) >= 0);
+        } else {
+                r = hashmap_put(c->by_key, i->key, i);
+                if (r < 0) {
+                        prioq_remove(c->by_expiry, i, &i->prioq_idx);
+                        return r;
+                }
+        }
+
+        return 0;
+}
+
+static DnsCacheItem* dns_cache_get(DnsCache *c, DnsResourceRecord *rr) {
+        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 min_ttl,
+                uint32_t nsec_ttl,
+                usec_t timestamp,
+                bool use_soa_minimum) {
+
+        uint32_t ttl;
+        usec_t u;
+
+        assert(rr);
+
+        ttl = MIN(min_ttl, nsec_ttl);
+        if (rr->key->type == DNS_TYPE_SOA && use_soa_minimum) {
+                /* If this is a SOA RR, and it is requested, clamp to the SOA's minimum field. This is used
+                 * when we do negative caching, to determine the TTL for the negative caching entry. See RFC
+                 * 2308, Section 5. */
+
+                if (ttl > rr->soa.minimum)
+                        ttl = rr->soa.minimum;
+        }
+
+        u = ttl * USEC_PER_SEC;
+        if (u > CACHE_TTL_MAX_USEC)
+                u = CACHE_TTL_MAX_USEC;
+
+        if (rr->expiry != USEC_INFINITY) {
+                usec_t left;
+
+                /* Make use of the DNSSEC RRSIG expiry info, if we have it */
+
+                left = LESS_BY(rr->expiry, now(CLOCK_REALTIME));
+                if (u > left)
+                        u = left;
+        }
+
+        return timestamp + u;
+}
+
+static void dns_cache_item_update_positive(
+                DnsCache *c,
+                DnsCacheItem *i,
+                DnsResourceRecord *rr,
+                DnsAnswer *answer,
+                DnsPacket *full_packet,
+                uint32_t min_ttl,
+                uint64_t query_flags,
+                bool shared_owner,
+                DnssecResult dnssec_result,
+                usec_t timestamp,
+                int ifindex,
+                int owner_family,
+                const union in_addr_union *owner_address) {
+
+        assert(c);
+        assert(i);
+        assert(rr);
+        assert(owner_address);
+
+        i->type = DNS_CACHE_POSITIVE;
+
+        if (!i->by_key_prev)
+                /* We are the first item in the list, we need to
+                 * update the key used in the hashmap */
+
+                assert_se(hashmap_replace(c->by_key, rr->key, i) >= 0);
+
+        dns_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);
+
+        dns_answer_ref(answer);
+        dns_answer_unref(i->answer);
+        i->answer = answer;
+
+        dns_packet_ref(full_packet);
+        dns_packet_unref(i->full_packet);
+        i->full_packet = full_packet;
+
+        i->until = calculate_until(rr, min_ttl, UINT32_MAX, timestamp, false);
+        i->query_flags = query_flags & CACHEABLE_QUERY_FLAGS;
+        i->shared_owner = shared_owner;
+        i->dnssec_result = dnssec_result;
+
+        i->ifindex = ifindex;
+
+        i->owner_family = owner_family;
+        i->owner_address = *owner_address;
+
+        prioq_reshuffle(c->by_expiry, i, &i->prioq_idx);
+}
+
+static int dns_cache_put_positive(
+                DnsCache *c,
+                DnsResourceRecord *rr,
+                DnsAnswer *answer,
+                DnsPacket *full_packet,
+                uint64_t query_flags,
+                bool shared_owner,
+                DnssecResult dnssec_result,
+                usec_t timestamp,
+                int ifindex,
+                int owner_family,
+                const union in_addr_union *owner_address) {
+
+        _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+        DnsCacheItem *existing;
+        uint32_t min_ttl;
+        int r;
+
+        assert(c);
+        assert(rr);
+        assert(owner_address);
+
+        /* Never cache pseudo RRs */
+        if (dns_class_is_pseudo(rr->key->class))
+                return 0;
+        if (dns_type_is_pseudo(rr->key->type))
+                return 0;
+
+        /* Determine the minimal TTL of all RRs in the answer plus the one by the main RR we are supposed to
+         * cache. Since we cache whole answers to questions we should never return answers where only some
+         * RRs are still valid, hence find the lowest here */
+        min_ttl = MIN(dns_answer_min_ttl(answer), rr->ttl);
+
+        /* New TTL is 0? Delete this specific entry... */
+        if (min_ttl <= 0) {
+                r = dns_cache_remove_by_rr(c, rr);
+                log_debug("%s: %s",
+                          r > 0 ? "Removed zero TTL entry from cache" : "Not caching zero TTL cache entry",
+                          dns_resource_key_to_string(rr->key, key_str, sizeof key_str));
+                return 0;
+        }
+
+        /* Entry exists already? Update TTL, timestamp and owner */
+        existing = dns_cache_get(c, rr);
+        if (existing) {
+                dns_cache_item_update_positive(
+                                c,
+                                existing,
+                                rr,
+                                answer,
+                                full_packet,
+                                min_ttl,
+                                query_flags,
+                                shared_owner,
+                                dnssec_result,
+                                timestamp,
+                                ifindex,
+                                owner_family,
+                                owner_address);
+                return 0;
+        }
+
+        /* 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),
+                .answer = dns_answer_ref(answer),
+                .full_packet = dns_packet_ref(full_packet),
+                .until = calculate_until(rr, min_ttl, UINT32_MAX, timestamp, false),
+                .query_flags = query_flags & CACHEABLE_QUERY_FLAGS,
+                .shared_owner = shared_owner,
+                .dnssec_result = dnssec_result,
+                .ifindex = ifindex,
+                .owner_family = owner_family,
+                .owner_address = *owner_address,
+                .prioq_idx = PRIOQ_IDX_NULL,
+        };
+
+        r = dns_cache_link_item(c, i);
+        if (r < 0)
+                return r;
+
+        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%s cache entry for %s "USEC_FMT"s on %s/%s/%s",
+                          FLAGS_SET(i->query_flags, SD_RESOLVED_AUTHENTICATED) ? "authenticated" : "unauthenticated",
+                          FLAGS_SET(i->query_flags, SD_RESOLVED_CONFIDENTIAL) ? "confidential" : "non-confidential",
+                          i->shared_owner ? " shared" : "",
+                          dns_resource_key_to_string(i->key, key_str, sizeof key_str),
+                          (i->until - timestamp) / USEC_PER_SEC,
+                          i->ifindex == 0 ? "*" : 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,
+                DnsAnswer *answer,
+                DnsPacket *full_packet,
+                uint64_t query_flags,
+                DnssecResult dnssec_result,
+                uint32_t nsec_ttl,
+                usec_t timestamp,
+                DnsResourceRecord *soa,
+                int owner_family,
+                const union in_addr_union *owner_address) {
+
+        _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+        int r;
+
+        assert(c);
+        assert(key);
+        assert(owner_address);
+
+        /* Never cache pseudo RR keys. DNS_TYPE_ANY is particularly
+         * important to filter out as we use this as a pseudo-type for
+         * NXDOMAIN entries */
+        if (dns_class_is_pseudo(key->class))
+                return 0;
+        if (dns_type_is_pseudo(key->type))
+                return 0;
+
+        if (IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN)) {
+                if (!soa)
+                        return 0;
+
+                /* For negative replies, check if we have a TTL of a SOA */
+                if (nsec_ttl <= 0 || soa->soa.minimum <= 0 || soa->ttl <= 0) {
+                        log_debug("Not caching negative entry with zero SOA/NSEC/NSEC3 TTL: %s",
+                                  dns_resource_key_to_string(key, key_str, sizeof key_str));
+                        return 0;
+                }
+        } else if (rcode != DNS_RCODE_SERVFAIL)
+                return 0;
+
+        r = dns_cache_init(c);
+        if (r < 0)
+                return r;
+
+        dns_cache_make_space(c, 1);
+
+        i = new(DnsCacheItem, 1);
+        if (!i)
+                return -ENOMEM;
+
+        *i = (DnsCacheItem) {
+                .type =
+                        rcode == DNS_RCODE_SUCCESS ? DNS_CACHE_NODATA :
+                        rcode == DNS_RCODE_NXDOMAIN ? DNS_CACHE_NXDOMAIN : DNS_CACHE_RCODE,
+                .query_flags = query_flags & CACHEABLE_QUERY_FLAGS,
+                .dnssec_result = dnssec_result,
+                .owner_family = owner_family,
+                .owner_address = *owner_address,
+                .prioq_idx = PRIOQ_IDX_NULL,
+                .rcode = rcode,
+                .answer = dns_answer_ref(answer),
+                .full_packet = dns_packet_ref(full_packet),
+        };
+
+        /* Determine how long to cache this entry. In case we have some RRs in the answer use the lowest TTL
+         * of any of them. Typically that's the SOA's TTL, which is OK, but could possibly be lower because
+         * of some other RR. Let's better take the lowest option here than a needlessly high one */
+        i->until =
+                i->type == DNS_CACHE_RCODE ? timestamp + CACHE_TTL_STRANGE_RCODE_USEC :
+                calculate_until(soa, dns_answer_min_ttl(answer), nsec_ttl, timestamp, true);
+
+        if (i->type == DNS_CACHE_NXDOMAIN) {
+                /* NXDOMAIN entries should apply equally to all types, so we use ANY as
+                 * a pseudo type for this purpose here. */
+                i->key = dns_resource_key_new(key->class, DNS_TYPE_ANY, dns_resource_key_name(key));
+                if (!i->key)
+                        return -ENOMEM;
+
+                /* Make sure to remove any previous entry for this
+                 * specific ANY key. (For non-ANY keys the cache data
+                 * is already cleared by the caller.) Note that we
+                 * don't bother removing positive or NODATA cache
+                 * items in this case, because it would either be slow
+                 * or require explicit indexing by name */
+                dns_cache_remove_by_key(c, key);
+        } else
+                i->key = dns_resource_key_ref(key);
+
+        r = dns_cache_link_item(c, i);
+        if (r < 0)
+                return r;
+
+        log_debug("Added %s cache entry for %s "USEC_FMT"s",
+                  dns_cache_item_type_to_string(i),
+                  dns_resource_key_to_string(i->key, key_str, sizeof key_str),
+                  (i->until - timestamp) / USEC_PER_SEC);
+
+        i = NULL;
+        return 0;
+}
+
+static void dns_cache_remove_previous(
+                DnsCache *c,
+                DnsResourceKey *key,
+                DnsAnswer *answer) {
+
+        DnsResourceRecord *rr;
+        DnsAnswerFlags flags;
+
+        assert(c);
+
+        /* First, if we were passed a key (i.e. on LLMNR/DNS, but
+         * not on mDNS), delete all matching old RRs, so that we only
+         * keep complete by_key in place. */
+        if (key)
+                dns_cache_remove_by_key(c, key);
+
+        /* Second, flush all entries matching the answer, unless this
+         * is an RR that is explicitly marked to be "shared" between
+         * peers (i.e. mDNS RRs without the flush-cache bit set). */
+        DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
+                if ((flags & DNS_ANSWER_CACHEABLE) == 0)
+                        continue;
+
+                if (flags & DNS_ANSWER_SHARED_OWNER)
+                        continue;
+
+                dns_cache_remove_by_key(c, rr->key);
+        }
+}
+
+static bool rr_eligible(DnsResourceRecord *rr) {
+        assert(rr);
+
+        /* When we see an NSEC/NSEC3 RR, we'll only cache it if it is from the lower zone, not the upper zone, since
+         * that's where the interesting bits are (with exception of DS RRs). Of course, this way we cannot derive DS
+         * existence from any cached NSEC/NSEC3, but that should be fine. */
+
+        switch (rr->key->type) {
+
+        case DNS_TYPE_NSEC:
+                return !systemd_bitmap_isset(rr->nsec.types, DNS_TYPE_NS) ||
+                        systemd_bitmap_isset(rr->nsec.types, DNS_TYPE_SOA);
+
+        case DNS_TYPE_NSEC3:
+                return !systemd_bitmap_isset(rr->nsec3.types, DNS_TYPE_NS) ||
+                        systemd_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,
+                DnsPacket *full_packet,
+                uint64_t query_flags,
+                DnssecResult dnssec_result,
+                uint32_t nsec_ttl,
+                int owner_family,
+                const union in_addr_union *owner_address) {
+
+        DnsResourceRecord *soa = NULL;
+        bool weird_rcode = false;
+        DnsAnswerItem *item;
+        DnsAnswerFlags flags;
+        unsigned cache_keys;
+        usec_t timestamp;
+        int r;
+
+        assert(c);
+        assert(owner_address);
+
+        dns_cache_remove_previous(c, key, answer);
+
+        /* We only care for positive replies and NXDOMAINs, on all other replies we will simply flush the respective
+         * entries, and that's it. (Well, with one further exception: since some DNS zones (akamai!) return SERVFAIL
+         * consistently for some lookups, and forwarders tend to propagate that we'll cache that too, but only for a
+         * short time.) */
+
+        if (IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN)) {
+                if (dns_answer_isempty(answer)) {
+                        if (key) {
+                                char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+
+                                log_debug("Not caching negative entry without a SOA record: %s",
+                                          dns_resource_key_to_string(key, key_str, sizeof key_str));
+                        }
+
+                        return 0;
+                }
+
+        } else {
+                /* Only cache SERVFAIL as "weird" rcode for now. We can add more later, should that turn out to be
+                 * beneficial. */
+                if (rcode != DNS_RCODE_SERVFAIL)
+                        return 0;
+
+                weird_rcode = true;
+        }
+
+        cache_keys = dns_answer_size(answer);
+        if (key)
+                cache_keys++;
+
+        /* Make some space for our new entries */
+        dns_cache_make_space(c, cache_keys);
+
+        timestamp = now(clock_boottime_or_monotonic());
+
+        /* Second, add in positive entries for all contained RRs */
+        DNS_ANSWER_FOREACH_ITEM(item, answer) {
+                int primary = false;
+
+                if (!FLAGS_SET(item->flags, DNS_ANSWER_CACHEABLE) ||
+                    !rr_eligible(item->rr))
+                        continue;
+
+                if (key) {
+                        /* We store the auxiliary RRs and packet data in the cache only if they were in
+                         * direct response to the original query. If we cache an RR we also received, and
+                         * that is just auxiliary information we can't use the data, hence don't. */
+
+                        primary = dns_resource_key_match_rr(key, item->rr, NULL);
+                        if (primary < 0)
+                                return primary;
+                        if (primary == 0) {
+                                primary = dns_resource_key_match_cname_or_dname(key, item->rr->key, NULL);
+                                if (primary < 0)
+                                        return primary;
+                        }
+                }
+
+                if (!primary) {
+                        DnsCacheItem *first;
+
+                        /* Do not replace existing cache items for primary lookups with non-primary
+                         * data. After all the primary lookup data is a lot more useful. */
+                        first = hashmap_get(c->by_key, item->rr->key);
+                        if (first && DNS_CACHE_ITEM_IS_PRIMARY(first))
+                                return 0;
+                }
+
+                r = dns_cache_put_positive(
+                                c,
+                                item->rr,
+                                primary ? answer : NULL,
+                                primary ? full_packet : NULL,
+                                ((item->flags & DNS_ANSWER_AUTHENTICATED) ? SD_RESOLVED_AUTHENTICATED : 0) |
+                                (query_flags & SD_RESOLVED_CONFIDENTIAL),
+                                item->flags & DNS_ANSWER_SHARED_OWNER,
+                                dnssec_result,
+                                timestamp,
+                                item->ifindex,
+                                owner_family,
+                                owner_address);
+                if (r < 0)
+                        goto fail;
+        }
+
+        if (!key) /* mDNS doesn't know negative caching, really */
+                return 0;
+
+        /* Third, add in negative entries if the key has no RR */
+        r = dns_answer_match_key(answer, key, NULL);
+        if (r < 0)
+                goto fail;
+        if (r > 0)
+                return 0;
+
+        /* But not if it has a matching CNAME/DNAME (the negative caching will be done on the canonical name,
+         * not on the alias) */
+        r = dns_answer_find_cname_or_dname(answer, key, NULL, NULL);
+        if (r < 0)
+                goto fail;
+        if (r > 0)
+                return 0;
+
+        /* See https://tools.ietf.org/html/rfc2308, which say that a matching SOA record in the packet is used to
+         * enable negative caching. We apply one exception though: if we are about to cache a weird rcode we do so
+         * regardless of a SOA. */
+        r = dns_answer_find_soa(answer, key, &soa, &flags);
+        if (r < 0)
+                goto fail;
+        if (r == 0 && !weird_rcode)
+                return 0;
+        if (r > 0) {
+                /* Refuse using the SOA data if it is unsigned, but the key is signed */
+                if (FLAGS_SET(query_flags, SD_RESOLVED_AUTHENTICATED) &&
+                    (flags & DNS_ANSWER_AUTHENTICATED) == 0)
+                        return 0;
+        }
+
+        if (cache_mode == DNS_CACHE_MODE_NO_NEGATIVE) {
+                char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+                log_debug("Not caching negative entry for: %s, cache mode set to no-negative",
+                          dns_resource_key_to_string(key, key_str, sizeof key_str));
+                return 0;
+        }
+
+        r = dns_cache_put_negative(
+                        c,
+                        key,
+                        rcode,
+                        answer,
+                        full_packet,
+                        query_flags,
+                        dnssec_result,
+                        nsec_ttl,
+                        timestamp,
+                        soa,
+                        owner_family, owner_address);
+        if (r < 0)
+                goto fail;
+
+        return 0;
+
+fail:
+        /* Adding all RRs failed. Let's clean up what we already
+         * added, just in case */
+
+        if (key)
+                dns_cache_remove_by_key(c, key);
+
+        DNS_ANSWER_FOREACH_ITEM(item, answer) {
+                if ((item->flags & DNS_ANSWER_CACHEABLE) == 0)
+                        continue;
+
+                dns_cache_remove_by_key(c, item->rr->key);
+        }
+
+        return r;
+}
+
+static DnsCacheItem *dns_cache_get_by_key_follow_cname_dname_nsec(DnsCache *c, DnsResourceKey *k) {
+        DnsCacheItem *i;
+        const char *n;
+        int r;
+
+        assert(c);
+        assert(k);
+
+        /* If we hit some OOM error, or suchlike, we don't care too
+         * much, after all this is just a cache */
+
+        i = hashmap_get(c->by_key, k);
+        if (i)
+                return i;
+
+        n = dns_resource_key_name(k);
+
+        /* Check if we have an NXDOMAIN cache item for the name, notice that we use
+         * the pseudo-type ANY for NXDOMAIN cache items. */
+        i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_ANY, n));
+        if (i && i->type == DNS_CACHE_NXDOMAIN)
+                return i;
+
+        if (dns_type_may_redirect(k->type)) {
+                /* Check if we have a CNAME record instead */
+                i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_CNAME, n));
+                if (i && i->type != DNS_CACHE_NODATA)
+                        return i;
+
+                /* OK, let's look for cached DNAME records. */
+                for (;;) {
+                        if (isempty(n))
+                                return NULL;
+
+                        i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_DNAME, n));
+                        if (i && i->type != DNS_CACHE_NODATA)
+                                return i;
+
+                        /* Jump one label ahead */
+                        r = dns_name_parent(&n);
+                        if (r <= 0)
+                                return NULL;
+                }
+        }
+
+        if (k->type != DNS_TYPE_NSEC) {
+                /* Check if we have an NSEC record instead for the name. */
+                i = hashmap_get(c->by_key, &DNS_RESOURCE_KEY_CONST(k->class, DNS_TYPE_NSEC, n));
+                if (i)
+                        return i;
+        }
+
+        return NULL;
+}
+
+static int answer_add_clamp_ttl(
+                DnsAnswer **answer,
+                DnsResourceRecord *rr,
+                int ifindex,
+                DnsAnswerFlags answer_flags,
+                DnsResourceRecord *rrsig,
+                uint64_t query_flags,
+                usec_t until,
+                usec_t current) {
+
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *patched = NULL, *patched_rrsig = NULL;
+        int r;
+
+        assert(answer);
+        assert(rr);
+
+        if (FLAGS_SET(query_flags, SD_RESOLVED_CLAMP_TTL)) {
+                uint32_t left_ttl;
+
+                assert(current > 0);
+
+                /* Let's determine how much time is left for this cache entry. Note that we round down, but
+                 * clamp this to be 1s at minimum, since we usually want records to remain cached better too
+                 * short a time than too long a time, but otoh don't want to return 0 ever, since that has
+                 * special semantics in various contexts — in particular in mDNS */
+
+                left_ttl = MAX(1U, LESS_BY(until, current) / USEC_PER_SEC);
+
+                patched = dns_resource_record_ref(rr);
+
+                r = dns_resource_record_clamp_ttl(&patched, left_ttl);
+                if (r < 0)
+                        return r;
+
+                rr = patched;
+
+                if (rrsig) {
+                        patched_rrsig = dns_resource_record_ref(rrsig);
+                        r = dns_resource_record_clamp_ttl(&patched_rrsig, left_ttl);
+                        if (r < 0)
+                                return r;
+
+                        rrsig = patched_rrsig;
+                }
+        }
+
+        r = dns_answer_add_extend(answer, rr, ifindex, answer_flags, rrsig);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+int dns_cache_lookup(
+                DnsCache *c,
+                DnsResourceKey *key,
+                uint64_t query_flags,
+                int *ret_rcode,
+                DnsAnswer **ret_answer,
+                DnsPacket **ret_full_packet,
+                uint64_t *ret_query_flags,
+                DnssecResult *ret_dnssec_result) {
+
+        _cleanup_(dns_packet_unrefp) DnsPacket *full_packet = NULL;
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+        unsigned n = 0;
+        int r;
+        bool nxdomain = false;
+        DnsCacheItem *j, *first, *nsec = NULL;
+        bool have_authenticated = false, have_non_authenticated = false, have_confidential = false, have_non_confidential = false;
+        usec_t current = 0;
+        int found_rcode = -1;
+        DnssecResult dnssec_result = -1;
+        int have_dnssec_result = -1;
+
+        assert(c);
+        assert(key);
+
+        if (key->type == DNS_TYPE_ANY || key->class == DNS_CLASS_ANY) {
+                /* If we have ANY lookups we don't use the cache, so that the caller refreshes via the
+                 * network. */
+
+                log_debug("Ignoring cache for ANY lookup: %s",
+                          dns_resource_key_to_string(key, key_str, sizeof key_str));
+                goto miss;
+        }
+
+        first = dns_cache_get_by_key_follow_cname_dname_nsec(c, key);
+        if (!first) {
+                /* If one question cannot be answered we need to refresh */
+
+                log_debug("Cache miss for %s",
+                          dns_resource_key_to_string(key, key_str, sizeof key_str));
+                goto miss;
+        }
+
+        if (FLAGS_SET(query_flags, SD_RESOLVED_CLAMP_TTL)) {
+                /* 'current' is always passed to answer_add_clamp_ttl(), but is only used conditionally.
+                 * We'll do the same assert there to make sure that it was initialized properly. */
+                current = now(clock_boottime_or_monotonic());
+                assert(current > 0);
+        }
+
+        LIST_FOREACH(by_key, j, first) {
+                /* If the caller doesn't allow us to answer questions from cache data learned from
+                 * "side-effect", skip this entry. */
+                if (FLAGS_SET(query_flags, SD_RESOLVED_REQUIRE_PRIMARY) &&
+                    !DNS_CACHE_ITEM_IS_PRIMARY(j)) {
+                        log_debug("Primary answer was requested for cache lookup for %s, which we don't have.",
+                                  dns_resource_key_to_string(key, key_str, sizeof key_str));
+
+                        goto miss;
+                }
+
+                if (j->type == DNS_CACHE_NXDOMAIN)
+                        nxdomain = true;
+                else if (j->type == DNS_CACHE_RCODE)
+                        found_rcode = j->rcode;
+                else if (j->rr) {
+                        if (j->rr->key->type == DNS_TYPE_NSEC)
+                                nsec = j;
+
+                        n++;
+                }
+
+                if (FLAGS_SET(j->query_flags, SD_RESOLVED_AUTHENTICATED))
+                        have_authenticated = true;
+                else
+                        have_non_authenticated = true;
+
+                if (FLAGS_SET(j->query_flags, SD_RESOLVED_CONFIDENTIAL))
+                        have_confidential = true;
+                else
+                        have_non_confidential = true;
+
+                if (j->dnssec_result < 0) {
+                        have_dnssec_result = false; /* an entry without dnssec result? then invalidate things for good */
+                        dnssec_result = _DNSSEC_RESULT_INVALID;
+                } else if (have_dnssec_result < 0) {
+                        have_dnssec_result = true; /* So far no result seen, let's pick this one up */
+                        dnssec_result = j->dnssec_result;
+                } else if (have_dnssec_result > 0 && j->dnssec_result != dnssec_result) {
+                        have_dnssec_result = false; /* conflicting result seen? then invalidate for good */
+                        dnssec_result = _DNSSEC_RESULT_INVALID;
+                }
+
+                /* Append the answer RRs to our answer. Ideally we have the answer object, which we
+                 * preferably use. But if the cached entry was generated as "side-effect" of a reply,
+                 * i.e. from validated auxiliary records rather than from the main reply, then we use the
+                 * individual RRs only instead. */
+                if (j->answer) {
+
+                        /* Minor optimization, if the full answer object of this and the previous RR is the
+                         * same, don't bother adding it again. Typically we store a full RRset here, hence
+                         * that should be the case. */
+                        if (!j->by_key_prev || j->answer != j->by_key_prev->answer) {
+                                DnsAnswerItem *item;
+
+                                DNS_ANSWER_FOREACH_ITEM(item, j->answer) {
+                                        r = answer_add_clamp_ttl(
+                                                        &answer,
+                                                        item->rr,
+                                                        item->ifindex,
+                                                        item->flags,
+                                                        item->rrsig,
+                                                        query_flags,
+                                                        j->until,
+                                                        current);
+                                        if (r < 0)
+                                                return r;
+                                }
+                        }
+
+                } else if (j->rr) {
+                        r = answer_add_clamp_ttl(
+                                        &answer,
+                                        j->rr,
+                                        j->ifindex,
+                                        FLAGS_SET(j->query_flags, SD_RESOLVED_AUTHENTICATED) ? DNS_ANSWER_AUTHENTICATED : 0,
+                                        NULL,
+                                        query_flags,
+                                        j->until,
+                                        current);
+                        if (r < 0)
+                                return r;
+                }
+
+                /* We'll return any packet we have for this. Typically all cache entries for the same key
+                 * should come from the same packet anyway, hence it doesn't really matter which packet we
+                 * return here, they should all resolve to the same anyway. */
+                if (!full_packet && j->full_packet)
+                        full_packet = dns_packet_ref(j->full_packet);
+        }
+
+        if (found_rcode >= 0) {
+                log_debug("RCODE %s cache hit for %s",
+                          dns_rcode_to_string(found_rcode),
+                          dns_resource_key_to_string(key, key_str, sizeof(key_str)));
+
+                if (ret_rcode)
+                        *ret_rcode = found_rcode;
+                if (ret_answer)
+                        *ret_answer = TAKE_PTR(answer);
+                if (ret_full_packet)
+                        *ret_full_packet = TAKE_PTR(full_packet);
+                if (ret_query_flags)
+                        *ret_query_flags = 0;
+                if (ret_dnssec_result)
+                        *ret_dnssec_result = dnssec_result;
+
+                c->n_hit++;
+                return 1;
+        }
+
+        if (nsec && !IN_SET(key->type, DNS_TYPE_NSEC, DNS_TYPE_DS)) {
+                /* Note that we won't derive information for DS RRs from an NSEC, because we only cache NSEC
+                 * RRs from the lower-zone of a zone cut, but the DS RRs are on the upper zone. */
+
+                log_debug("NSEC NODATA cache hit for %s",
+                          dns_resource_key_to_string(key, key_str, sizeof key_str));
+
+                /* We only found an NSEC record that matches our name.  If it says the type doesn't exist
+                 * report NODATA. Otherwise report a cache miss. */
+
+                if (ret_rcode)
+                        *ret_rcode = DNS_RCODE_SUCCESS;
+                if (ret_answer)
+                        *ret_answer = TAKE_PTR(answer);
+                if (ret_full_packet)
+                        *ret_full_packet = TAKE_PTR(full_packet);
+                if (ret_query_flags)
+                        *ret_query_flags = nsec->query_flags;
+                if (ret_dnssec_result)
+                        *ret_dnssec_result = nsec->dnssec_result;
+
+                if (!systemd_bitmap_isset(nsec->rr->nsec.types, key->type) &&
+                    !systemd_bitmap_isset(nsec->rr->nsec.types, DNS_TYPE_CNAME) &&
+                    !systemd_bitmap_isset(nsec->rr->nsec.types, DNS_TYPE_DNAME)) {
+                        c->n_hit++;
+                        return 1;
+                }
+
+                c->n_miss++;
+                return 0;
+        }
+
+        log_debug("%s cache hit for %s",
+                  n > 0    ? "Positive" :
+                  nxdomain ? "NXDOMAIN" : "NODATA",
+                  dns_resource_key_to_string(key, key_str, sizeof key_str));
+
+        if (n <= 0) {
+                c->n_hit++;
+
+                if (ret_rcode)
+                        *ret_rcode = nxdomain ? DNS_RCODE_NXDOMAIN : DNS_RCODE_SUCCESS;
+                if (ret_answer)
+                        *ret_answer = TAKE_PTR(answer);
+                if (ret_full_packet)
+                        *ret_full_packet = TAKE_PTR(full_packet);
+                if (ret_query_flags)
+                        *ret_query_flags =
+                                ((have_authenticated && !have_non_authenticated) ? SD_RESOLVED_AUTHENTICATED : 0) |
+                                ((have_confidential && !have_non_confidential) ? SD_RESOLVED_CONFIDENTIAL : 0);
+                if (ret_dnssec_result)
+                        *ret_dnssec_result = dnssec_result;
+
+                return 1;
+        }
+
+        c->n_hit++;
+
+        if (ret_rcode)
+                *ret_rcode = DNS_RCODE_SUCCESS;
+        if (ret_answer)
+                *ret_answer = TAKE_PTR(answer);
+        if (ret_full_packet)
+                *ret_full_packet = TAKE_PTR(full_packet);
+        if (ret_query_flags)
+                *ret_query_flags =
+                        ((have_authenticated && !have_non_authenticated) ? SD_RESOLVED_AUTHENTICATED : 0) |
+                        ((have_confidential && !have_non_confidential) ? SD_RESOLVED_CONFIDENTIAL : 0);
+        if (ret_dnssec_result)
+                *ret_dnssec_result = dnssec_result;
+
+        return n;
+
+miss:
+        if (ret_rcode)
+                *ret_rcode = DNS_RCODE_SUCCESS;
+        if (ret_answer)
+                *ret_answer = NULL;
+        if (ret_full_packet)
+                *ret_full_packet = NULL;
+        if (ret_query_flags)
+                *ret_query_flags = 0;
+        if (ret_dnssec_result)
+                *ret_dnssec_result = _DNSSEC_RESULT_INVALID;
+
+        c->n_miss++;
+        return 0;
+}
+
+int dns_cache_check_conflicts(DnsCache *cache, DnsResourceRecord *rr, int owner_family, const union in_addr_union *owner_address) {
+        DnsCacheItem *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..621b52f
--- /dev/null
+++ b/src/resolve/resolved-dns-cache.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "hashmap.h"
+#include "list.h"
+#include "prioq.h"
+#include "resolve-util.h"
+#include "resolved-dns-dnssec.h"
+#include "time-util.h"
+
+typedef struct DnsCache {
+        Hashmap *by_key;
+        Prioq *by_expiry;
+        unsigned n_hit;
+        unsigned n_miss;
+} DnsCache;
+
+#include "resolved-dns-answer.h"
+#include "resolved-dns-packet.h"
+#include "resolved-dns-question.h"
+#include "resolved-dns-rr.h"
+
+void dns_cache_flush(DnsCache *c);
+void dns_cache_prune(DnsCache *c);
+
+int dns_cache_put(
+                DnsCache *c,
+                DnsCacheMode cache_mode,
+                DnsResourceKey *key,
+                int rcode,
+                DnsAnswer *answer,
+                DnsPacket *full_packet,
+                uint64_t query_flags,
+                DnssecResult dnssec_result,
+                uint32_t nsec_ttl,
+                int owner_family,
+                const union in_addr_union *owner_address);
+
+int dns_cache_lookup(
+                DnsCache *c,
+                DnsResourceKey *key,
+                uint64_t query_flags,
+                int *ret_rcode,
+                DnsAnswer **ret_answer,
+                DnsPacket **ret_full_packet,
+                uint64_t *ret_query_flags,
+                DnssecResult *ret_dnssec_result);
+
+int dns_cache_check_conflicts(DnsCache *cache, DnsResourceRecord *rr, int owner_family, const union in_addr_union *owner_address);
+
+void dns_cache_dump(DnsCache *cache, FILE *f);
+bool dns_cache_is_empty(DnsCache *cache);
+
+unsigned dns_cache_size(DnsCache *cache);
+
+int dns_cache_export_shared_to_packet(DnsCache *cache, DnsPacket *p);
diff --git a/src/resolve/resolved-dns-dnssec.c b/src/resolve/resolved-dns-dnssec.c
new file mode 100644
index 0000000..c187ac6
--- /dev/null
+++ b/src/resolve/resolved-dns-dnssec.c
@@ -0,0 +1,2257 @@
+/* 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;
+
+        /* The algorithm from RFC 4034, Appendix B. */
+
+        assert(dnskey);
+        assert(dnskey->key->type == DNS_TYPE_DNSKEY);
+
+        f = (uint32_t) dnskey->dnskey.flags;
+
+        if (mask_revoke)
+                f &= ~DNSKEY_FLAG_REVOKE;
+
+        sum = f + ((((uint32_t) dnskey->dnskey.protocol) << 8) + (uint32_t) dnskey->dnskey.algorithm);
+
+        p = dnskey->dnskey.key;
+
+        for (size_t i = 0; i < dnskey->dnskey.key_size; i++)
+                sum += (i & 1) == 0 ? (uint32_t) p[i] << 8 : (uint32_t) p[i];
+
+        sum += (sum >> 16) & UINT32_C(0xFFFF);
+
+        return sum & UINT32_C(0xFFFF);
+}
+
+#if HAVE_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 != UINT8_MAX)
+                return 0;
+
+        if (rrsig->rrsig.inception > rrsig->rrsig.expiration)
+                return -EINVAL;
+
+        name = dns_resource_key_name(rrsig->key);
+
+        n_key_labels = dns_name_count_labels(name);
+        if (n_key_labels < 0)
+                return n_key_labels;
+        if (rrsig->rrsig.labels > n_key_labels)
+                return -EINVAL;
+
+        n_signer_labels = dns_name_count_labels(rrsig->rrsig.signer);
+        if (n_signer_labels < 0)
+                return n_signer_labels;
+        if (n_signer_labels > rrsig->rrsig.labels)
+                return -EINVAL;
+
+        r = dns_name_skip(name, n_key_labels - n_signer_labels, &name);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -EINVAL;
+
+        /* Check if the signer is really a suffix of us */
+        r = dns_name_equal(name, rrsig->rrsig.signer);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -EINVAL;
+
+        assert(n_key_labels < UINT8_MAX); /* UINT8_MAX/-1 means unsigned. */
+        rrsig->n_skip_labels_source = n_key_labels - rrsig->rrsig.labels;
+        rrsig->n_skip_labels_signer = n_key_labels - n_signer_labels;
+
+        return 0;
+}
+
+static int dnssec_rrsig_expired(DnsResourceRecord *rrsig, usec_t realtime) {
+        usec_t expiration, inception, skew;
+
+        assert(rrsig);
+        assert(rrsig->key->type == DNS_TYPE_RRSIG);
+
+        if (realtime == USEC_INFINITY)
+                realtime = now(CLOCK_REALTIME);
+
+        expiration = rrsig->rrsig.expiration * USEC_PER_SEC;
+        inception = rrsig->rrsig.inception * USEC_PER_SEC;
+
+        /* Consider inverted validity intervals as expired */
+        if (inception > expiration)
+                return true;
+
+        /* Permit a certain amount of clock skew of 10% of the valid
+         * time range. This takes inspiration from unbound's
+         * resolver. */
+        skew = (expiration - inception) / 10;
+        if (skew > SKEW_MAX)
+                skew = SKEW_MAX;
+
+        if (inception < skew)
+                inception = 0;
+        else
+                inception -= skew;
+
+        if (expiration + skew < expiration)
+                expiration = USEC_INFINITY;
+        else
+                expiration += skew;
+
+        return realtime < inception || realtime > expiration;
+}
+
+static 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) {
+
+        assert(list);
+        assert(n > 0);
+        assert(rrsig);
+
+        for (unsigned k = 0; k < n; k++) {
+                DnsResourceRecord *rr = list[k];
+
+                /* Pick the TTL as the minimum of the RR's TTL, the
+                 * RR's original TTL according to the RRSIG and the
+                 * RRSIG's own TTL, see RFC 4035, Section 5.3.3 */
+                rr->ttl = MIN3(rr->ttl, rrsig->rrsig.original_ttl, rrsig->ttl);
+                rr->expiry = rrsig->rrsig.expiration * USEC_PER_SEC;
+
+                /* Copy over information about the signer and wildcard source of synthesis */
+                rr->n_skip_labels_source = rrsig->n_skip_labels_source;
+                rr->n_skip_labels_signer = rrsig->n_skip_labels_signer;
+        }
+
+        rrsig->expiry = rrsig->rrsig.expiration * USEC_PER_SEC;
+}
+
+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 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 (size_t 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: {
+                gcry_error_t err;
+
+                /* 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;
+
+                err = gcry_md_open(&md, md_algorithm, 0);
+                if (gcry_err_code(err) != GPG_ERR_NO_ERROR || !md)
+                        return -EIO;
+
+                hash_size = gcry_md_get_algo_dlen(md_algorithm);
+                assert(hash_size > 0);
+
+                gcry_md_write(md, sig_data, sig_size);
+
+                hash = gcry_md_read(md, 0);
+                if (!hash)
+                        return -EIO;
+        }
+        }
+
+        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;
+        gcry_error_t err;
+        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;
+
+        err = gcry_md_open(&md, md_algorithm, 0);
+        if (gcry_err_code(err) != GPG_ERR_NO_ERROR || !md)
+                return -EIO;
+
+        gcry_md_write(md, wire_format, r);
+        if (mask_revoke)
+                md_add_uint16(md, dnskey->dnskey.flags & ~DNSKEY_FLAG_REVOKE);
+        else
+                md_add_uint16(md, dnskey->dnskey.flags);
+        md_add_uint8(md, dnskey->dnskey.protocol);
+        md_add_uint8(md, dnskey->dnskey.algorithm);
+        gcry_md_write(md, dnskey->dnskey.key, dnskey->dnskey.key_size);
+
+        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];
+        _cleanup_(gcry_md_closep) gcry_md_hd_t md = NULL;
+        gcry_error_t err;
+        size_t hash_size;
+        int algorithm;
+        void *result;
+        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;
+
+        err = gcry_md_open(&md, algorithm, 0);
+        if (gcry_err_code(err) != GPG_ERR_NO_ERROR || !md)
+                return -EIO;
+
+        gcry_md_write(md, wire_format, r);
+        gcry_md_write(md, nsec3->nsec3.salt, nsec3->nsec3.salt_size);
+
+        result = gcry_md_read(md, 0);
+        if (!result)
+                return -EIO;
+
+        for (unsigned k = 0; k < nsec3->nsec3.iterations; k++) {
+                uint8_t tmp[hash_size];
+                memcpy(tmp, result, hash_size);
+
+                gcry_md_reset(md);
+                gcry_md_write(md, tmp, hash_size);
+                gcry_md_write(md, nsec3->nsec3.salt, nsec3->nsec3.salt_size);
+
+                result = gcry_md_read(md, 0);
+                if (!result)
+                        return -EIO;
+        }
+
+        memcpy(ret, result, hash_size);
+        return (int) hash_size;
+}
+
+static int nsec3_is_good(DnsResourceRecord *rr, DnsResourceRecord *nsec3) {
+        const char *a, *b;
+        int r;
+
+        assert(rr);
+
+        if (rr->key->type != DNS_TYPE_NSEC3)
+                return 0;
+
+        /* RFC 5155, Section 8.2 says we MUST ignore NSEC3 RRs with flags != 0 or 1 */
+        if (!IN_SET(rr->nsec3.flags, 0, 1))
+                return 0;
+
+        /* Ignore NSEC3 RRs whose algorithm we don't know */
+        if (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, UINT8_MAX))
+                return 0;
+        /* Ignore NSEC3 RRs that are located anywhere else than one label below the zone */
+        if (!IN_SET(rr->n_skip_labels_signer, 1, UINT8_MAX))
+                return 0;
+
+        if (!nsec3)
+                return 1;
+
+        /* If a second NSEC3 RR is specified, also check if they are from the same zone. */
+
+        if (nsec3 == rr) /* Shortcut */
+                return 1;
+
+        if (rr->key->class != nsec3->key->class)
+                return 0;
+        if (rr->nsec3.algorithm != nsec3->nsec3.algorithm)
+                return 0;
+        if (rr->nsec3.iterations != nsec3->nsec3.iterations)
+                return 0;
+        if (rr->nsec3.salt_size != nsec3->nsec3.salt_size)
+                return 0;
+        if (memcmp_safe(rr->nsec3.salt, nsec3->nsec3.salt, rr->nsec3.salt_size) != 0)
+                return 0;
+
+        a = dns_resource_key_name(rr->key);
+        r = dns_name_parent(&a); /* strip off hash */
+        if (r <= 0)
+                return r;
+
+        b = dns_resource_key_name(nsec3->key);
+        r = dns_name_parent(&b); /* strip off hash */
+        if (r <= 0)
+                return r;
+
+        /* Make sure both have the same parent */
+        return dns_name_equal(a, b);
+}
+
+static int nsec3_hashed_domain_format(const uint8_t *hashed, size_t hashed_size, const char *zone, char **ret) {
+        _cleanup_free_ char *l = NULL;
+        char *j;
+
+        assert(hashed);
+        assert(hashed_size > 0);
+        assert(zone);
+        assert(ret);
+
+        l = base32hexmem(hashed, hashed_size, false);
+        if (!l)
+                return -ENOMEM;
+
+        j = strjoin(l, ".", zone);
+        if (!j)
+                return -ENOMEM;
+
+        *ret = j;
+        return (int) hashed_size;
+}
+
+static int nsec3_hashed_domain_make(DnsResourceRecord *nsec3, const char *domain, const char *zone, char **ret) {
+        uint8_t hashed[DNSSEC_HASH_SIZE_MAX];
+        int hashed_size;
+
+        assert(nsec3);
+        assert(domain);
+        assert(zone);
+        assert(ret);
+
+        hashed_size = dnssec_nsec3_hash(nsec3, domain, hashed);
+        if (hashed_size < 0)
+                return hashed_size;
+
+        return nsec3_hashed_domain_format(hashed, (size_t) hashed_size, zone, ret);
+}
+
+/* See RFC 5155, Section 8
+ * First try to find a NSEC3 record that matches our query precisely, if that fails, find the closest
+ * enclosure. Secondly, find a proof that there is no closer enclosure and either a proof that there
+ * is no wildcard domain as a direct descendant of the closest enclosure, or find an NSEC3 record that
+ * matches the wildcard domain.
+ *
+ * Based on this we can prove either the existence of the record in @key, or NXDOMAIN or NODATA, or
+ * that there is no proof either way. The latter is the case if a proof of non-existence of a given
+ * name uses an NSEC3 record with the opt-out bit set. Lastly, if we are given insufficient NSEC3 records
+ * to conclude anything we indicate this by returning NO_RR. */
+static int dnssec_test_nsec3(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) {
+        _cleanup_free_ char *next_closer_domain = NULL, *wildcard_domain = NULL;
+        const char *zone, *p, *pp = NULL, *wildcard;
+        DnsResourceRecord *rr, *enclosure_rr, *zone_rr, *wildcard_rr = NULL;
+        DnsAnswerFlags flags;
+        int hashed_size, r;
+        bool a, no_closer = false, no_wildcard = false, optout = false;
+
+        assert(key);
+        assert(result);
+
+        /* First step, find the zone name and the NSEC3 parameters of the zone.
+         * it is sufficient to look for the longest common suffix we find with
+         * any NSEC3 RR in the response. Any NSEC3 record will do as all NSEC3
+         * records from a given zone in a response must use the same
+         * parameters. */
+        zone = dns_resource_key_name(key);
+        for (;;) {
+                DNS_ANSWER_FOREACH_FLAGS(zone_rr, flags, answer) {
+                        r = nsec3_is_good(zone_rr, NULL);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        r = dns_name_equal_skip(dns_resource_key_name(zone_rr->key), 1, zone);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                goto found_zone;
+                }
+
+                /* Strip one label from the front */
+                r = dns_name_parent(&zone);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+        }
+
+        *result = DNSSEC_NSEC_NO_RR;
+        return 0;
+
+found_zone:
+        /* Second step, find the closest encloser NSEC3 RR in 'answer' that matches 'key' */
+        p = dns_resource_key_name(key);
+        for (;;) {
+                _cleanup_free_ char *hashed_domain = NULL;
+
+                hashed_size = nsec3_hashed_domain_make(zone_rr, p, zone, &hashed_domain);
+                if (hashed_size == -EOPNOTSUPP) {
+                        *result = DNSSEC_NSEC_UNSUPPORTED_ALGORITHM;
+                        return 0;
+                }
+                if (hashed_size < 0)
+                        return hashed_size;
+
+                DNS_ANSWER_FOREACH_FLAGS(enclosure_rr, flags, answer) {
+
+                        r = nsec3_is_good(enclosure_rr, zone_rr);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        if (enclosure_rr->nsec3.next_hashed_name_size != (size_t) hashed_size)
+                                continue;
+
+                        r = dns_name_equal(dns_resource_key_name(enclosure_rr->key), hashed_domain);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                a = flags & DNS_ANSWER_AUTHENTICATED;
+                                goto found_closest_encloser;
+                        }
+                }
+
+                /* We didn't find the closest encloser with this name,
+                 * but let's remember this domain name, it might be
+                 * the next closer name */
+
+                pp = p;
+
+                /* Strip one label from the front */
+                r = dns_name_parent(&p);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+        }
+
+        *result = DNSSEC_NSEC_NO_RR;
+        return 0;
+
+found_closest_encloser:
+        /* We found a closest encloser in 'p'; next closer is 'pp' */
+
+        if (!pp) {
+                /* We have an exact match! If we area looking for a DS RR, then we must insist that we got the NSEC3 RR
+                 * from the parent. Otherwise the one from the child. Do so, by checking whether SOA and NS are
+                 * appropriately set. */
+
+                if (key->type == DNS_TYPE_DS) {
+                        if (systemd_bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_SOA))
+                                return -EBADMSG;
+                } else {
+                        if (systemd_bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_NS) &&
+                            !systemd_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 (systemd_bitmap_isset(enclosure_rr->nsec3.types, key->type))
+                        *result = DNSSEC_NSEC_FOUND;
+                else if (systemd_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 (systemd_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 (systemd_bitmap_isset(enclosure_rr->nsec3.types, DNS_TYPE_NS) &&
+            !systemd_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 (systemd_bitmap_isset(wildcard_rr->nsec3.types, key->type))
+                        *result = DNSSEC_NSEC_FOUND;
+                else if (systemd_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 (systemd_bitmap_isset(rr->nsec.types, DNS_TYPE_DNAME))
+                return 1;
+
+        if (systemd_bitmap_isset(rr->nsec.types, DNS_TYPE_NS) && !systemd_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 (systemd_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 (systemd_bitmap_isset(rr->nsec.types, DNS_TYPE_NS) &&
+                                    !systemd_bitmap_isset(rr->nsec.types, DNS_TYPE_SOA))
+                                        continue;
+                        }
+
+                        if (systemd_bitmap_isset(rr->nsec.types, key->type))
+                                *result = DNSSEC_NSEC_FOUND;
+                        else if (systemd_bitmap_isset(rr->nsec.types, DNS_TYPE_CNAME))
+                                *result = DNSSEC_NSEC_CNAME;
+                        else
+                                *result = DNSSEC_NSEC_NODATA;
+
+                        if (authenticated)
+                                *authenticated = flags & DNS_ANSWER_AUTHENTICATED;
+                        if (ttl)
+                                *ttl = rr->ttl;
+
+                        return 0;
+                }
+
+                /* Check if the name we are looking for is an empty non-terminal within the owner or next name
+                 * of the NSEC RR. */
+                r = dnssec_nsec_in_path(rr, name);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        *result = DNSSEC_NSEC_NODATA;
+
+                        if (authenticated)
+                                *authenticated = flags & DNS_ANSWER_AUTHENTICATED;
+                        if (ttl)
+                                *ttl = rr->ttl;
+
+                        return 0;
+                }
+
+                /* The following two "covering" checks, are not useful if the NSEC is from the parent */
+                r = dnssec_nsec_from_parent_zone(rr, name);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        continue;
+
+                /* Check if this NSEC RR proves the absence of an explicit RR under this name */
+                r = dnssec_nsec_covers(rr, name);
+                if (r < 0)
+                        return r;
+                if (r > 0 && (!covering_rr || !covering_rr_authenticated)) {
+                        covering_rr = rr;
+                        covering_rr_authenticated = flags & DNS_ANSWER_AUTHENTICATED;
+                }
+        }
+
+        if (covering_rr) {
+                _cleanup_free_ char *wc = NULL;
+                r = dnssec_nsec_generate_wildcard(covering_rr, name, &wc);
+                if (r < 0)
+                        return r;
+
+                DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
+
+                        if (rr->key->class != key->class)
+                                continue;
+
+                        if (rr->key->type != DNS_TYPE_NSEC)
+                                continue;
+
+                        /* Check if this NSEC RR proves the nonexistence of the wildcard */
+                        r = dnssec_nsec_covers(rr, wc);
+                        if (r < 0)
+                                return r;
+                        if (r > 0 && (!wildcard_rr || !wildcard_rr_authenticated)) {
+                                wildcard_rr = rr;
+                                wildcard_rr_authenticated = flags & DNS_ANSWER_AUTHENTICATED;
+                        }
+                }
+        }
+
+        if (covering_rr && wildcard_rr) {
+                /* If we could prove that neither the name itself, nor the wildcard at the closest encloser exists, we
+                 * proved the NXDOMAIN case. */
+                *result = DNSSEC_NSEC_NXDOMAIN;
+
+                if (authenticated)
+                        *authenticated = covering_rr_authenticated && wildcard_rr_authenticated;
+                if (ttl)
+                        *ttl = MIN(covering_rr->ttl, wildcard_rr->ttl);
+
+                return 0;
+        }
+
+        /* OK, this was not sufficient. Let's see if NSEC3 can help. */
+        if (have_nsec3)
+                return dnssec_test_nsec3(answer, key, result, authenticated, ttl);
+
+        /* No appropriate NSEC RR found, report this. */
+        *result = DNSSEC_NSEC_NO_RR;
+        return 0;
+}
+
+static int dnssec_nsec_test_enclosed(DnsAnswer *answer, uint16_t type, const char *name, const char *zone, bool *authenticated) {
+        DnsResourceRecord *rr;
+        DnsAnswerFlags flags;
+        int r;
+
+        assert(name);
+        assert(zone);
+
+        /* Checks whether there's an NSEC/NSEC3 that proves that the specified 'name' is non-existing in the specified
+         * 'zone'. The 'zone' must be a suffix of the 'name'. */
+
+        DNS_ANSWER_FOREACH_FLAGS(rr, flags, answer) {
+                bool found = false;
+
+                if (rr->key->type != type && type != DNS_TYPE_ANY)
+                        continue;
+
+                switch (rr->key->type) {
+
+                case DNS_TYPE_NSEC:
+
+                        /* We only care for NSEC RRs from the indicated zone */
+                        r = dns_resource_record_is_signer(rr, zone);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        r = dns_name_between(dns_resource_key_name(rr->key), name, rr->nsec.next_domain_name);
+                        if (r < 0)
+                                return r;
+
+                        found = r > 0;
+                        break;
+
+                case DNS_TYPE_NSEC3: {
+                        _cleanup_free_ char *hashed_domain = NULL, *next_hashed_domain = NULL;
+
+                        /* We only care for NSEC3 RRs from the indicated zone */
+                        r = dns_resource_record_is_signer(rr, zone);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        r = nsec3_is_good(rr, NULL);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                break;
+
+                        /* Format the domain we are testing with the NSEC3 RR's hash function */
+                        r = nsec3_hashed_domain_make(
+                                        rr,
+                                        name,
+                                        zone,
+                                        &hashed_domain);
+                        if (r < 0)
+                                return r;
+                        if ((size_t) r != rr->nsec3.next_hashed_name_size)
+                                break;
+
+                        /* Format the NSEC3's next hashed name as proper domain name */
+                        r = nsec3_hashed_domain_format(
+                                        rr->nsec3.next_hashed_name,
+                                        rr->nsec3.next_hashed_name_size,
+                                        zone,
+                                        &next_hashed_domain);
+                        if (r < 0)
+                                return r;
+
+                        r = dns_name_between(dns_resource_key_name(rr->key), hashed_domain, next_hashed_domain);
+                        if (r < 0)
+                                return r;
+
+                        found = r > 0;
+                        break;
+                }
+
+                default:
+                        continue;
+                }
+
+                if (found) {
+                        if (authenticated)
+                                *authenticated = flags & DNS_ANSWER_AUTHENTICATED;
+                        return 1;
+                }
+        }
+
+        return 0;
+}
+
+static int dnssec_test_positive_wildcard_nsec3(
+                DnsAnswer *answer,
+                const char *name,
+                const char *source,
+                const char *zone,
+                bool *authenticated) {
+
+        const char *next_closer = NULL;
+        int r;
+
+        /* Run a positive NSEC3 wildcard proof. Specifically:
+         *
+         * A proof that the "next closer" of the generating wildcard does not exist.
+         *
+         * Note a key difference between the NSEC3 and NSEC versions of the proof. NSEC RRs don't have to exist for
+         * empty non-transients. NSEC3 RRs however have to. This means it's sufficient to check if the next closer name
+         * exists for the NSEC3 RR and we are done.
+         *
+         * To prove that a.b.c.d.e.f is rightfully synthesized from a wildcard *.d.e.f all we have to check is that
+         * c.d.e.f does not exist. */
+
+        for (;;) {
+                next_closer = name;
+                r = dns_name_parent(&name);
+                if (r <= 0)
+                        return r;
+
+                r = dns_name_equal(name, source);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        break;
+        }
+
+        return dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC3, next_closer, zone, authenticated);
+}
+
+static int dnssec_test_positive_wildcard_nsec(
+                DnsAnswer *answer,
+                const char *name,
+                const char *source,
+                const char *zone,
+                bool *_authenticated) {
+
+        bool authenticated = true;
+        int r;
+
+        /* Run a positive NSEC wildcard proof. Specifically:
+         *
+         * A proof that there's neither a wildcard name nor a non-wildcard name that is a suffix of the name "name" and
+         * a prefix of the synthesizing source "source" in the zone "zone".
+         *
+         * See RFC 5155, Section 8.8 and RFC 4035, Section 5.3.4
+         *
+         * Note that if we want to prove that a.b.c.d.e.f is rightfully synthesized from a wildcard *.d.e.f, then we
+         * have to prove that none of the following exist:
+         *
+         *      1) a.b.c.d.e.f
+         *      2) *.b.c.d.e.f
+         *      3)   b.c.d.e.f
+         *      4)   *.c.d.e.f
+         *      5)     c.d.e.f
+         */
+
+        for (;;) {
+                _cleanup_free_ char *wc = NULL;
+                bool a = false;
+
+                /* Check if there's an NSEC or NSEC3 RR that proves that the mame we determined is really non-existing,
+                 * i.e between the owner name and the next name of an NSEC RR. */
+                r = dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC, name, zone, &a);
+                if (r <= 0)
+                        return r;
+
+                authenticated = authenticated && a;
+
+                /* Strip one label off */
+                r = dns_name_parent(&name);
+                if (r <= 0)
+                        return r;
+
+                /* Did we reach the source of synthesis? */
+                r = dns_name_equal(name, source);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        /* Successful exit */
+                        *_authenticated = authenticated;
+                        return 1;
+                }
+
+                /* Safety check, that the source of synthesis is still our suffix */
+                r = dns_name_endswith(name, source);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return -EBADMSG;
+
+                /* Replace the label we stripped off with an asterisk */
+                wc = strjoin("*.", name);
+                if (!wc)
+                        return -ENOMEM;
+
+                /* And check if the proof holds for the asterisk name, too */
+                r = dnssec_nsec_test_enclosed(answer, DNS_TYPE_NSEC, wc, zone, &a);
+                if (r <= 0)
+                        return r;
+
+                authenticated = authenticated && a;
+                /* In the next iteration we'll check the non-asterisk-prefixed version */
+        }
+}
+
+int dnssec_test_positive_wildcard(
+                DnsAnswer *answer,
+                const char *name,
+                const char *source,
+                const char *zone,
+                bool *authenticated) {
+
+        int r;
+
+        assert(name);
+        assert(source);
+        assert(zone);
+        assert(authenticated);
+
+        r = dns_answer_contains_zone_nsec3(answer, zone);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                return dnssec_test_positive_wildcard_nsec3(answer, name, source, zone, authenticated);
+        else
+                return dnssec_test_positive_wildcard_nsec(answer, name, source, zone, authenticated);
+}
+
+#else
+
+int dnssec_verify_rrset(
+                DnsAnswer *a,
+                const DnsResourceKey *key,
+                DnsResourceRecord *rrsig,
+                DnsResourceRecord *dnskey,
+                usec_t realtime,
+                DnssecResult *result) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, bool revoked_ok) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_verify_rrset_search(
+                DnsAnswer *a,
+                const DnsResourceKey *key,
+                DnsAnswer *validated_dnskeys,
+                usec_t realtime,
+                DnssecResult *result,
+                DnsResourceRecord **ret_rrsig) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_verify_dnskey_by_ds_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl) {
+
+        return -EOPNOTSUPP;
+}
+
+int dnssec_test_positive_wildcard(
+                DnsAnswer *answer,
+                const char *name,
+                const char *source,
+                const char *zone,
+                bool *authenticated) {
+
+        return -EOPNOTSUPP;
+}
+
+#endif
+
+static const char* const dnssec_result_table[_DNSSEC_RESULT_MAX] = {
+        [DNSSEC_VALIDATED]             = "validated",
+        [DNSSEC_VALIDATED_WILDCARD]    = "validated-wildcard",
+        [DNSSEC_INVALID]               = "invalid",
+        [DNSSEC_SIGNATURE_EXPIRED]     = "signature-expired",
+        [DNSSEC_UNSUPPORTED_ALGORITHM] = "unsupported-algorithm",
+        [DNSSEC_NO_SIGNATURE]          = "no-signature",
+        [DNSSEC_MISSING_KEY]           = "missing-key",
+        [DNSSEC_UNSIGNED]              = "unsigned",
+        [DNSSEC_FAILED_AUXILIARY]      = "failed-auxiliary",
+        [DNSSEC_NSEC_MISMATCH]         = "nsec-mismatch",
+        [DNSSEC_INCOMPATIBLE_SERVER]   = "incompatible-server",
+};
+DEFINE_STRING_TABLE_LOOKUP(dnssec_result, DnssecResult);
+
+static const char* const dnssec_verdict_table[_DNSSEC_VERDICT_MAX] = {
+        [DNSSEC_SECURE]        = "secure",
+        [DNSSEC_INSECURE]      = "insecure",
+        [DNSSEC_BOGUS]         = "bogus",
+        [DNSSEC_INDETERMINATE] = "indeterminate",
+};
+DEFINE_STRING_TABLE_LOOKUP(dnssec_verdict, DnssecVerdict);
diff --git a/src/resolve/resolved-dns-dnssec.h b/src/resolve/resolved-dns-dnssec.h
new file mode 100644
index 0000000..954bb3e
--- /dev/null
+++ b/src/resolve/resolved-dns-dnssec.h
@@ -0,0 +1,81 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef enum DnssecResult DnssecResult;
+typedef enum DnssecVerdict DnssecVerdict;
+
+#include "dns-domain.h"
+#include "resolved-dns-answer.h"
+#include "resolved-dns-rr.h"
+
+enum DnssecResult {
+        /* These five are returned by dnssec_verify_rrset() */
+        DNSSEC_VALIDATED,
+        DNSSEC_VALIDATED_WILDCARD, /* Validated via a wildcard RRSIG, further NSEC/NSEC3 checks necessary */
+        DNSSEC_INVALID,
+        DNSSEC_SIGNATURE_EXPIRED,
+        DNSSEC_UNSUPPORTED_ALGORITHM,
+
+        /* These two are added by dnssec_verify_rrset_search() */
+        DNSSEC_NO_SIGNATURE,
+        DNSSEC_MISSING_KEY,
+
+        /* These two are added by the DnsTransaction logic */
+        DNSSEC_UNSIGNED,
+        DNSSEC_FAILED_AUXILIARY,
+        DNSSEC_NSEC_MISMATCH,
+        DNSSEC_INCOMPATIBLE_SERVER,
+
+        _DNSSEC_RESULT_MAX,
+        _DNSSEC_RESULT_INVALID = -EINVAL,
+};
+
+enum DnssecVerdict {
+        DNSSEC_SECURE,
+        DNSSEC_INSECURE,
+        DNSSEC_BOGUS,
+        DNSSEC_INDETERMINATE,
+
+        _DNSSEC_VERDICT_MAX,
+        _DNSSEC_VERDICT_INVALID = -EINVAL,
+};
+
+#define DNSSEC_CANONICAL_HOSTNAME_MAX (DNS_HOSTNAME_MAX + 2)
+
+/* The longest digest we'll ever generate, of all digest algorithms we support */
+#define DNSSEC_HASH_SIZE_MAX (MAX(20, 32))
+
+int dnssec_rrsig_match_dnskey(DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, bool revoked_ok);
+int dnssec_key_match_rrsig(const DnsResourceKey *key, DnsResourceRecord *rrsig);
+
+int dnssec_verify_rrset(DnsAnswer *answer, const DnsResourceKey *key, DnsResourceRecord *rrsig, DnsResourceRecord *dnskey, usec_t realtime, DnssecResult *result);
+int dnssec_verify_rrset_search(DnsAnswer *answer, const DnsResourceKey *key, DnsAnswer *validated_dnskeys, usec_t realtime, DnssecResult *result, DnsResourceRecord **rrsig);
+
+int dnssec_verify_dnskey_by_ds(DnsResourceRecord *dnskey, DnsResourceRecord *ds, bool mask_revoke);
+int dnssec_verify_dnskey_by_ds_search(DnsResourceRecord *dnskey, DnsAnswer *validated_ds);
+
+int dnssec_has_rrsig(DnsAnswer *a, const DnsResourceKey *key);
+
+uint16_t dnssec_keytag(DnsResourceRecord *dnskey, bool mask_revoke);
+
+int dnssec_nsec3_hash(DnsResourceRecord *nsec3, const char *name, void *ret);
+
+typedef enum DnssecNsecResult {
+        DNSSEC_NSEC_NO_RR,     /* No suitable NSEC/NSEC3 RR found */
+        DNSSEC_NSEC_CNAME,     /* Didn't find what was asked for, but did find CNAME */
+        DNSSEC_NSEC_UNSUPPORTED_ALGORITHM,
+        DNSSEC_NSEC_NXDOMAIN,
+        DNSSEC_NSEC_NODATA,
+        DNSSEC_NSEC_FOUND,
+        DNSSEC_NSEC_OPTOUT,
+} DnssecNsecResult;
+
+int dnssec_nsec_test(DnsAnswer *answer, DnsResourceKey *key, DnssecNsecResult *result, bool *authenticated, uint32_t *ttl);
+
+int dnssec_test_positive_wildcard(DnsAnswer *a, const char *name, const char *source, const char *zone, bool *authenticated);
+
+const char* dnssec_result_to_string(DnssecResult m) _const_;
+DnssecResult dnssec_result_from_string(const char *s) _pure_;
+
+const char* dnssec_verdict_to_string(DnssecVerdict m) _const_;
+DnssecVerdict dnssec_verdict_from_string(const char *s) _pure_;
diff --git a/src/resolve/resolved-dns-packet.c b/src/resolve/resolved-dns-packet.c
new file mode 100644
index 0000000..512ea2e
--- /dev/null
+++ b/src/resolve/resolved-dns-packet.c
@@ -0,0 +1,2665 @@
+/* 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_MAX,
+                .opt_size = SIZE_MAX,
+        };
+
+        *ret = p;
+
+        return 0;
+}
+
+void dns_packet_set_flags(DnsPacket *p, bool dnssec_checking_disabled, bool truncated) {
+
+        DnsPacketHeader *h;
+
+        assert(p);
+
+        h = DNS_PACKET_HEADER(p);
+
+        switch(p->protocol) {
+        case DNS_PROTOCOL_LLMNR:
+                assert(!truncated);
+
+                h->flags = htobe16(DNS_PACKET_MAKE_FLAGS(0 /* qr */,
+                                                         0 /* opcode */,
+                                                         0 /* c */,
+                                                         0 /* tc */,
+                                                         0 /* t */,
+                                                         0 /* ra */,
+                                                         0 /* ad */,
+                                                         0 /* cd */,
+                                                         0 /* rcode */));
+                break;
+
+        case DNS_PROTOCOL_MDNS:
+                h->flags = htobe16(DNS_PACKET_MAKE_FLAGS(0         /* qr */,
+                                                         0         /* opcode */,
+                                                         0         /* aa */,
+                                                         truncated /* tc */,
+                                                         0         /* rd (ask for recursion) */,
+                                                         0         /* ra */,
+                                                         0         /* ad */,
+                                                         0         /* cd */,
+                                                         0         /* rcode */));
+                break;
+
+        default:
+                assert(!truncated);
+
+                h->flags = htobe16(DNS_PACKET_MAKE_FLAGS(0 /* qr */,
+                                                         0 /* opcode */,
+                                                         0 /* aa */,
+                                                         0 /* tc */,
+                                                         1 /* rd (ask for recursion) */,
+                                                         0 /* ra */,
+                                                         0 /* ad */,
+                                                         dnssec_checking_disabled /* cd */,
+                                                         0 /* rcode */));
+        }
+}
+
+int dns_packet_new_query(DnsPacket **ret, DnsProtocol protocol, size_t min_alloc_dsize, bool dnssec_checking_disabled) {
+        DnsPacket *p;
+        int r;
+
+        assert(ret);
+
+        r = dns_packet_new(&p, protocol, min_alloc_dsize, DNS_PACKET_SIZE_MAX);
+        if (r < 0)
+                return r;
+
+        /* Always set the TC bit to 0 initially.
+         * If there are multiple packets later, we'll update the bit shortly before sending.
+         */
+        dns_packet_set_flags(p, dnssec_checking_disabled, false);
+
+        *ret = p;
+        return 0;
+}
+
+int dns_packet_dup(DnsPacket **ret, DnsPacket *p) {
+        DnsPacket *c;
+        int r;
+
+        assert(ret);
+        assert(p);
+
+        r = dns_packet_validate(p);
+        if (r < 0)
+                return r;
+
+        c = malloc(ALIGN(sizeof(DnsPacket)) + p->size);
+        if (!c)
+                return -ENOMEM;
+
+        *c = (DnsPacket) {
+                .n_ref = 1,
+                .protocol = p->protocol,
+                .size = p->size,
+                .rindex = DNS_PACKET_HEADER_SIZE,
+                .allocated = p->size,
+                .max_size = p->max_size,
+                .opt_start = SIZE_MAX,
+                .opt_size = SIZE_MAX,
+        };
+
+        memcpy(DNS_PACKET_DATA(c), DNS_PACKET_DATA(p), p->size);
+
+        *ret = c;
+        return 0;
+}
+
+DnsPacket *dns_packet_ref(DnsPacket *p) {
+
+        if (!p)
+                return NULL;
+
+        assert(!p->on_stack);
+
+        assert(p->n_ref > 0);
+        p->n_ref++;
+        return p;
+}
+
+static void dns_packet_free(DnsPacket *p) {
+        char *s;
+
+        assert(p);
+
+        dns_question_unref(p->question);
+        dns_answer_unref(p->answer);
+        dns_resource_record_unref(p->opt);
+
+        while ((s = hashmap_steal_first_key(p->names)))
+                free(s);
+        hashmap_free(p->names);
+
+        free(p->_data);
+
+        if (!p->on_stack)
+                free(p);
+}
+
+DnsPacket *dns_packet_unref(DnsPacket *p) {
+        if (!p)
+                return NULL;
+
+        assert(p->n_ref > 0);
+
+        dns_packet_unref(p->more);
+
+        if (p->n_ref == 1)
+                dns_packet_free(p);
+        else
+                p->n_ref--;
+
+        return NULL;
+}
+
+int dns_packet_validate(DnsPacket *p) {
+        assert(p);
+
+        if (p->size < DNS_PACKET_HEADER_SIZE)
+                return -EBADMSG;
+
+        if (p->size > DNS_PACKET_SIZE_MAX)
+                return -EBADMSG;
+
+        return 1;
+}
+
+int dns_packet_validate_reply(DnsPacket *p) {
+        int r;
+
+        assert(p);
+
+        r = dns_packet_validate(p);
+        if (r < 0)
+                return r;
+
+        if (DNS_PACKET_QR(p) != 1)
+                return 0;
+
+        if (DNS_PACKET_OPCODE(p) != 0)
+                return -EBADMSG;
+
+        switch (p->protocol) {
+
+        case DNS_PROTOCOL_LLMNR:
+                /* RFC 4795, Section 2.1.1. says to discard all replies with QDCOUNT != 1 */
+                if (DNS_PACKET_QDCOUNT(p) != 1)
+                        return -EBADMSG;
+
+                break;
+
+        case DNS_PROTOCOL_MDNS:
+                /* RFC 6762, Section 18 */
+                if (DNS_PACKET_RCODE(p) != 0)
+                        return -EBADMSG;
+
+                break;
+
+        default:
+                break;
+        }
+
+        return 1;
+}
+
+int dns_packet_validate_query(DnsPacket *p) {
+        int r;
+
+        assert(p);
+
+        r = dns_packet_validate(p);
+        if (r < 0)
+                return r;
+
+        if (DNS_PACKET_QR(p) != 0)
+                return 0;
+
+        if (DNS_PACKET_OPCODE(p) != 0)
+                return -EBADMSG;
+
+        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 specifies that messages with non-zero RCODE
+                 * must be silently ignored, and that we must ignore the values of
+                 * AA, RD, RA, AD, and CD bits. */
+                if (DNS_PACKET_RCODE(p) != 0)
+                        return -EBADMSG;
+
+                break;
+
+        default:
+                break;
+        }
+
+        return 1;
+}
+
+static int dns_packet_extend(DnsPacket *p, size_t add, void **ret, size_t *start) {
+        assert(p);
+
+        if (p->size + add > p->allocated) {
+                size_t a, ms;
+
+                a = PAGE_ALIGN((p->size + add) * 2);
+
+                ms = dns_packet_size_max(p);
+                if (a > ms)
+                        a = ms;
+
+                if (p->size + add > a)
+                        return -EMSGSIZE;
+
+                if (p->_data) {
+                        void *d;
+
+                        d = realloc(p->_data, a);
+                        if (!d)
+                                return -ENOMEM;
+
+                        p->_data = d;
+                } else {
+                        p->_data = malloc(a);
+                        if (!p->_data)
+                                return -ENOMEM;
+
+                        memcpy(p->_data, (uint8_t*) p + ALIGN(sizeof(DnsPacket)), p->size);
+                        memzero((uint8_t*) p->_data + p->size, a - p->size);
+                }
+
+                p->allocated = a;
+        }
+
+        if (start)
+                *start = p->size;
+
+        if (ret)
+                *ret = (uint8_t*) DNS_PACKET_DATA(p) + p->size;
+
+        p->size += add;
+        return 0;
+}
+
+void dns_packet_truncate(DnsPacket *p, size_t sz) {
+        char *s;
+        void *n;
+
+        assert(p);
+
+        if (p->size <= sz)
+                return;
+
+        HASHMAP_FOREACH_KEY(n, s, p->names) {
+
+                if (PTR_TO_SIZE(n) < sz)
+                        continue;
+
+                hashmap_remove(p->names, s);
+                free(s);
+        }
+
+        p->size = sz;
+}
+
+int dns_packet_append_blob(DnsPacket *p, const void *d, size_t l, size_t *start) {
+        void *q;
+        int r;
+
+        assert(p);
+
+        r = dns_packet_extend(p, l, &q, start);
+        if (r < 0)
+                return r;
+
+        memcpy_safe(q, d, l);
+        return 0;
+}
+
+int dns_packet_append_uint8(DnsPacket *p, uint8_t v, size_t *start) {
+        void *d;
+        int r;
+
+        assert(p);
+
+        r = dns_packet_extend(p, sizeof(uint8_t), &d, start);
+        if (r < 0)
+                return r;
+
+        ((uint8_t*) d)[0] = v;
+
+        return 0;
+}
+
+int dns_packet_append_uint16(DnsPacket *p, uint16_t v, size_t *start) {
+        void *d;
+        int r;
+
+        assert(p);
+
+        r = dns_packet_extend(p, sizeof(uint16_t), &d, start);
+        if (r < 0)
+                return r;
+
+        unaligned_write_be16(d, v);
+
+        return 0;
+}
+
+int dns_packet_append_uint32(DnsPacket *p, uint32_t v, size_t *start) {
+        void *d;
+        int r;
+
+        assert(p);
+
+        r = dns_packet_extend(p, sizeof(uint32_t), &d, start);
+        if (r < 0)
+                return r;
+
+        unaligned_write_be32(d, v);
+
+        return 0;
+}
+
+int dns_packet_append_string(DnsPacket *p, const char *s, size_t *start) {
+        assert(p);
+        assert(s);
+
+        return dns_packet_append_raw_string(p, s, strlen(s), start);
+}
+
+int dns_packet_append_raw_string(DnsPacket *p, const void *s, size_t size, size_t *start) {
+        void *d;
+        int r;
+
+        assert(p);
+        assert(s || size == 0);
+
+        if (size > 255)
+                return -E2BIG;
+
+        r = dns_packet_extend(p, 1 + size, &d, start);
+        if (r < 0)
+                return r;
+
+        ((uint8_t*) d)[0] = (uint8_t) size;
+
+        memcpy_safe(((uint8_t*) d) + 1, s, size);
+
+        return 0;
+}
+
+int dns_packet_append_label(DnsPacket *p, const char *d, size_t l, bool canonical_candidate, size_t *start) {
+        uint8_t *w;
+        int r;
+
+        /* Append a label to a packet. Optionally, does this in DNSSEC
+         * canonical form, if this label is marked as a candidate for
+         * it, and the canonical form logic is enabled for the
+         * packet */
+
+        assert(p);
+        assert(d);
+
+        if (l > DNS_LABEL_MAX)
+                return -E2BIG;
+
+        r = dns_packet_extend(p, 1 + l, (void**) &w, start);
+        if (r < 0)
+                return r;
+
+        *(w++) = (uint8_t) l;
+
+        if (p->canonical_form && canonical_candidate) {
+                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_put(&p->names, &dns_name_hash_ops, s, SIZE_TO_PTR(n));
+                        if (r < 0)
+                                goto fail;
+
+                        TAKE_PTR(s);
+                }
+        }
+
+        r = dns_packet_append_uint8(p, 0, NULL);
+        if (r < 0)
+                return r;
+
+done:
+        if (start)
+                *start = saved_size;
+
+        return 0;
+
+fail:
+        dns_packet_truncate(p, saved_size);
+        return r;
+}
+
+int dns_packet_append_key(DnsPacket *p, const DnsResourceKey *k, const DnsAnswerFlags flags, size_t *start) {
+        size_t saved_size;
+        uint16_t class;
+        int r;
+
+        assert(p);
+        assert(k);
+
+        saved_size = p->size;
+
+        r = dns_packet_append_name(p, dns_resource_key_name(k), true, true, NULL);
+        if (r < 0)
+                goto fail;
+
+        r = dns_packet_append_uint16(p, k->type, NULL);
+        if (r < 0)
+                goto fail;
+
+        class = flags & DNS_ANSWER_CACHE_FLUSH ? k->class | MDNS_RR_CACHE_FLUSH_OR_QU : k->class;
+        r = dns_packet_append_uint16(p, class, NULL);
+        if (r < 0)
+                goto fail;
+
+        if (start)
+                *start = saved_size;
+
+        return 0;
+
+fail:
+        dns_packet_truncate(p, saved_size);
+        return r;
+}
+
+static int dns_packet_append_type_window(DnsPacket *p, uint8_t window, uint8_t length, const uint8_t *types, size_t *start) {
+        size_t saved_size;
+        int r;
+
+        assert(p);
+        assert(types);
+        assert(length > 0);
+
+        saved_size = p->size;
+
+        r = dns_packet_append_uint8(p, window, NULL);
+        if (r < 0)
+                goto fail;
+
+        r = dns_packet_append_uint8(p, length, NULL);
+        if (r < 0)
+                goto fail;
+
+        r = dns_packet_append_blob(p, types, length, NULL);
+        if (r < 0)
+                goto fail;
+
+        if (start)
+                *start = saved_size;
+
+        return 0;
+fail:
+        dns_packet_truncate(p, saved_size);
+        return r;
+}
+
+static int dns_packet_append_types(DnsPacket *p, Bitmap *types, size_t *start) {
+        uint8_t window = 0;
+        uint8_t entry = 0;
+        uint8_t bitmaps[32] = {};
+        unsigned n;
+        size_t saved_size;
+        int r;
+
+        assert(p);
+
+        saved_size = p->size;
+
+        BITMAP_FOREACH(n, types) {
+                assert(n <= 0xffff);
+
+                if ((n >> 8) != window && bitmaps[entry / 8] != 0) {
+                        r = dns_packet_append_type_window(p, window, entry / 8 + 1, bitmaps, NULL);
+                        if (r < 0)
+                                goto fail;
+
+                        zero(bitmaps);
+                }
+
+                window = n >> 8;
+                entry = n & 255;
+
+                bitmaps[entry / 8] |= 1 << (7 - (entry % 8));
+        }
+
+        if (bitmaps[entry / 8] != 0) {
+                r = dns_packet_append_type_window(p, window, entry / 8 + 1, bitmaps, NULL);
+                if (r < 0)
+                        goto fail;
+        }
+
+        if (start)
+                *start = saved_size;
+
+        return 0;
+fail:
+        dns_packet_truncate(p, saved_size);
+        return r;
+}
+
+/* Append the OPT pseudo-RR described in RFC6891 */
+int dns_packet_append_opt(
+                DnsPacket *p,
+                uint16_t max_udp_size,
+                bool edns0_do,
+                bool include_rfc6975,
+                const char *nsid,
+                int rcode,
+                size_t *ret_start) {
+
+        size_t saved_size;
+        int r;
+
+        assert(p);
+        /* we must never advertise supported packet size smaller than the legacy max */
+        assert(max_udp_size >= DNS_PACKET_UNICAST_SIZE_MAX);
+        assert(rcode >= 0);
+        assert(rcode <= _DNS_RCODE_MAX);
+
+        if (p->opt_start != SIZE_MAX)
+                return -EBUSY;
+
+        assert(p->opt_size == SIZE_MAX);
+
+        saved_size = p->size;
+
+        /* empty name */
+        r = dns_packet_append_uint8(p, 0, NULL);
+        if (r < 0)
+                return r;
+
+        /* type */
+        r = dns_packet_append_uint16(p, DNS_TYPE_OPT, NULL);
+        if (r < 0)
+                goto fail;
+
+        /* class: maximum udp packet that can be received */
+        r = dns_packet_append_uint16(p, max_udp_size, NULL);
+        if (r < 0)
+                goto fail;
+
+        /* extended RCODE and VERSION */
+        r = dns_packet_append_uint16(p, ((uint16_t) rcode & 0x0FF0) << 4, NULL);
+        if (r < 0)
+                goto fail;
+
+        /* flags: DNSSEC OK (DO), see RFC3225 */
+        r = dns_packet_append_uint16(p, edns0_do ? EDNS0_OPT_DO : 0, NULL);
+        if (r < 0)
+                goto fail;
+
+        if (edns0_do && include_rfc6975) {
+                /* If DO is on and this is requested, also append RFC6975 Algorithm data. This is supposed to
+                 * be done on queries, not on replies, hencer callers should turn this off when finishing off
+                 * replies. */
+
+                static const uint8_t rfc6975[] = {
+
+                        0, 5, /* OPTION_CODE: DAU */
+#if 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); /* RDLENGTH */
+                if (r < 0)
+                        goto fail;
+
+                r = dns_packet_append_blob(p, rfc6975, sizeof(rfc6975), NULL); /* the payload, as defined above */
+
+        } else if (nsid) {
+
+                if (strlen(nsid) > UINT16_MAX - 4) {
+                        r = -E2BIG;
+                        goto fail;
+                }
+
+                r = dns_packet_append_uint16(p, 4 + strlen(nsid), NULL); /* RDLENGTH */
+                if (r < 0)
+                        goto fail;
+
+                r = dns_packet_append_uint16(p, 3, NULL); /* OPTION-CODE: NSID */
+                if (r < 0)
+                        goto fail;
+
+                r = dns_packet_append_uint16(p, strlen(nsid), NULL); /* OPTION-LENGTH */
+                if (r < 0)
+                        goto fail;
+
+                r = dns_packet_append_blob(p, nsid, strlen(nsid), NULL);
+        } else
+                r = dns_packet_append_uint16(p, 0, NULL);
+        if (r < 0)
+                goto fail;
+
+        DNS_PACKET_HEADER(p)->arcount = htobe16(DNS_PACKET_ARCOUNT(p) + 1);
+
+        p->opt_start = saved_size;
+        p->opt_size = p->size - saved_size;
+
+        if (ret_start)
+                *ret_start = saved_size;
+
+        return 0;
+
+fail:
+        dns_packet_truncate(p, saved_size);
+        return r;
+}
+
+int dns_packet_truncate_opt(DnsPacket *p) {
+        assert(p);
+
+        if (p->opt_start == SIZE_MAX) {
+                assert(p->opt_size == SIZE_MAX);
+                return 0;
+        }
+
+        assert(p->opt_size != SIZE_MAX);
+        assert(DNS_PACKET_ARCOUNT(p) > 0);
+
+        if (p->opt_start + p->opt_size != p->size)
+                return -EBUSY;
+
+        dns_packet_truncate(p, p->opt_start);
+        DNS_PACKET_HEADER(p)->arcount = htobe16(DNS_PACKET_ARCOUNT(p) - 1);
+        p->opt_start = p->opt_size = SIZE_MAX;
+
+        return 1;
+}
+
+int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, const DnsAnswerFlags flags, size_t *start, size_t *rdata_start) {
+
+        size_t saved_size, rdlength_offset, end, rdlength, rds;
+        uint32_t ttl;
+        int r;
+
+        assert(p);
+        assert(rr);
+
+        saved_size = p->size;
+
+        r = dns_packet_append_key(p, rr->key, flags, NULL);
+        if (r < 0)
+                goto fail;
+
+        ttl = flags & DNS_ANSWER_GOODBYE ? 0 : rr->ttl;
+        r = dns_packet_append_uint32(p, ttl, NULL);
+        if (r < 0)
+                goto fail;
+
+        /* Initially we write 0 here */
+        r = dns_packet_append_uint16(p, 0, &rdlength_offset);
+        if (r < 0)
+                goto fail;
+
+        rds = p->size - saved_size;
+
+        switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) {
+
+        case DNS_TYPE_SRV:
+                r = dns_packet_append_uint16(p, rr->srv.priority, NULL);
+                if (r < 0)
+                        goto fail;
+
+                r = dns_packet_append_uint16(p, rr->srv.weight, NULL);
+                if (r < 0)
+                        goto fail;
+
+                r = dns_packet_append_uint16(p, rr->srv.port, NULL);
+                if (r < 0)
+                        goto fail;
+
+                /* RFC 2782 states "Unless and until permitted by future standards
+                 * action, name compression is not to be used for this field." */
+                r = dns_packet_append_name(p, rr->srv.name, false, true, NULL);
+                break;
+
+        case DNS_TYPE_PTR:
+        case DNS_TYPE_NS:
+        case DNS_TYPE_CNAME:
+        case DNS_TYPE_DNAME:
+                r = dns_packet_append_name(p, rr->ptr.name, true, true, NULL);
+                break;
+
+        case DNS_TYPE_HINFO:
+                r = dns_packet_append_string(p, rr->hinfo.cpu, NULL);
+                if (r < 0)
+                        goto fail;
+
+                r = dns_packet_append_string(p, rr->hinfo.os, NULL);
+                break;
+
+        case DNS_TYPE_SPF: /* exactly the same as TXT */
+        case DNS_TYPE_TXT:
+
+                if (!rr->txt.items) {
+                        /* RFC 6763, section 6.1 suggests to generate
+                         * single empty string for an empty array. */
+
+                        r = dns_packet_append_raw_string(p, NULL, 0, NULL);
+                        if (r < 0)
+                                goto fail;
+                } else {
+                        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, unsigned *completed) {
+        DnsResourceRecord *rr;
+        DnsAnswerFlags flags;
+        int r;
+
+        assert(p);
+
+        DNS_ANSWER_FOREACH_FLAGS(rr, flags, a) {
+                r = dns_packet_append_rr(p, rr, flags, NULL, NULL);
+                if (r < 0)
+                        return r;
+
+                if (completed)
+                        (*completed)++;
+        }
+
+        return 0;
+}
+
+int dns_packet_read(DnsPacket *p, size_t sz, const void **ret, size_t *start) {
+        assert(p);
+
+        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;
+
+        if (ret)
+                *ret = unaligned_read_be16(d);
+
+        return 0;
+}
+
+int dns_packet_read_uint32(DnsPacket *p, uint32_t *ret, size_t *start) {
+        const void *d;
+        int r;
+
+        assert(p);
+
+        r = dns_packet_read(p, sizeof(uint32_t), &d, start);
+        if (r < 0)
+                return r;
+
+        *ret = unaligned_read_be32(d);
+
+        return 0;
+}
+
+int dns_packet_read_string(DnsPacket *p, char **ret, size_t *start) {
+        _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 *ret_start) {
+
+        _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+        size_t after_rindex = 0, jump_barrier;
+        _cleanup_free_ char *name = NULL;
+        bool first = true;
+        size_t n = 0;
+        int r;
+
+        assert(p);
+
+        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(name, n + !first + DNS_LABEL_ESCAPED_MAX))
+                                return -ENOMEM;
+
+                        if (first)
+                                first = false;
+                        else
+                                name[n++] = '.';
+
+                        r = dns_label_escape(label, c, name + n, DNS_LABEL_ESCAPED_MAX);
+                        if (r < 0)
+                                return r;
+
+                        n += r;
+                        continue;
+                } else if (allow_compression && FLAGS_SET(c, 0xc0)) {
+                        uint16_t ptr;
+
+                        /* Pointer */
+                        r = dns_packet_read_uint8(p, &d, NULL);
+                        if (r < 0)
+                                return r;
+
+                        ptr = (uint16_t) (c & ~0xc0) << 8 | (uint16_t) d;
+                        if (ptr < DNS_PACKET_HEADER_SIZE || ptr >= jump_barrier)
+                                return -EBADMSG;
+
+                        if (after_rindex == 0)
+                                after_rindex = p->rindex;
+
+                        /* Jumps are limited to a "prior occurrence" (RFC-1035 4.1.4) */
+                        jump_barrier = ptr;
+                        p->rindex = ptr;
+                } else
+                        return -EBADMSG;
+        }
+
+        if (!GREEDY_REALLOC(name, n + 1))
+                return -ENOMEM;
+
+        name[n] = 0;
+
+        if (after_rindex != 0)
+                p->rindex= after_rindex;
+
+        if (ret)
+                *ret = TAKE_PTR(name);
+        if (ret_start)
+                *ret_start = rewinder.saved_rindex;
+
+        CANCEL_REWINDER(rewinder);
+
+        return 0;
+}
+
+static int dns_packet_read_type_window(DnsPacket *p, Bitmap **types, size_t *start) {
+        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 = systemd_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 = systemd_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_or_qu,
+                size_t *ret_start) {
+
+        _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+        _cleanup_free_ char *name = NULL;
+        bool cache_flush_or_qu = false;
+        uint16_t class, type;
+        int r;
+
+        assert(p);
+        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, sections 5.4 and 10.2 */
+
+                if (type != DNS_TYPE_OPT && (class & MDNS_RR_CACHE_FLUSH_OR_QU)) {
+                        class &= ~MDNS_RR_CACHE_FLUSH_OR_QU;
+                        cache_flush_or_qu = true;
+                }
+        }
+
+        if (ret) {
+                DnsResourceKey *key;
+
+                key = dns_resource_key_new_consume(class, type, name);
+                if (!key)
+                        return -ENOMEM;
+
+                TAKE_PTR(name);
+                *ret = key;
+        }
+
+        if (ret_cache_flush_or_qu)
+                *ret_cache_flush_or_qu = cache_flush_or_qu;
+        if (ret_start)
+                *ret_start = rewinder.saved_rindex;
+
+        CANCEL_REWINDER(rewinder);
+        return 0;
+}
+
+static bool loc_size_ok(uint8_t size) {
+        uint8_t m = size >> 4, e = size & 0xF;
+
+        return m <= 9 && e <= 9 && (m > 0 || e == 0);
+}
+
+int dns_packet_read_rr(
+                DnsPacket *p,
+                DnsResourceRecord **ret,
+                bool *ret_cache_flush,
+                size_t *ret_start) {
+
+        _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);
+
+        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;
+
+        if (ret)
+                *ret = TAKE_PTR(rr);
+        if (ret_cache_flush)
+                *ret_cache_flush = cache_flush;
+        if (ret_start)
+                *ret_start = rewinder.saved_rindex;
+
+        CANCEL_REWINDER(rewinder);
+        return 0;
+}
+
+static bool opt_is_good(DnsResourceRecord *rr, bool *rfc6975) {
+        const uint8_t* p;
+        bool found_dau_dhu_n3u = false;
+        size_t l;
+
+        /* Checks whether the specified OPT RR is well-formed and whether it contains RFC6975 data (which is not OK in
+         * a reply). */
+
+        assert(rr);
+        assert(rr->key->type == DNS_TYPE_OPT);
+
+        /* Check that the version is 0 */
+        if (((rr->ttl >> 16) & UINT32_C(0xFF)) != 0) {
+                *rfc6975 = false;
+                return true; /* if it's not version 0, it's OK, but we will ignore the OPT field contents */
+        }
+
+        p = rr->opt.data;
+        l = rr->opt.data_size;
+        while (l > 0) {
+                uint16_t option_code, option_length;
+
+                /* At least four bytes for OPTION-CODE and OPTION-LENGTH are required */
+                if (l < 4U)
+                        return false;
+
+                option_code = unaligned_read_be16(p);
+                option_length = unaligned_read_be16(p + 2);
+
+                if (l < option_length + 4U)
+                        return false;
+
+                /* RFC 6975 DAU, DHU or N3U fields found. */
+                if (IN_SET(option_code, 5, 6, 7))
+                        found_dau_dhu_n3u = true;
+
+                p += option_length + 4U;
+                l -= option_length + 4U;
+        }
+
+        *rfc6975 = found_dau_dhu_n3u;
+        return true;
+}
+
+static int dns_packet_extract_question(DnsPacket *p, DnsQuestion **ret_question) {
+        _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
+        unsigned n, 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 off after 2. */
+                if (r < 0)
+                        return r;
+
+                for (i = 0; i < n; i++) {
+                        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+                        bool qu;
+
+                        r = dns_packet_read_key(p, &key, &qu, NULL);
+                        if (r < 0)
+                                return r;
+
+                        if (!dns_type_is_valid_query(key->type))
+                                return -EBADMSG;
+
+                        r = set_put(keys, key);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                /* Already in the Question, let's skip */
+                                continue;
+
+                        r = dns_question_add_raw(question, key, qu ? DNS_QUESTION_WANTS_UNICAST_REPLY : 0);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        *ret_question = TAKE_PTR(question);
+
+        return 0;
+}
+
+static int dns_packet_extract_answer(DnsPacket *p, DnsAnswer **ret_answer) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        unsigned n, 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;
+                size_t start;
+
+                if (p->rindex == p->size && p->opt) {
+                        /* If we reached the end of the packet already, but there are still more RRs
+                         * declared, then that's a corrupt packet. Let's accept the packet anyway, since it's
+                         * apparently a common bug in routers. Let's however suppress OPT support in this
+                         * case, so that we force the rest of the logic into lowest DNS baseline support. Or
+                         * to say this differently: if the DNS server doesn't even get the RR counts right,
+                         * it's highly unlikely it gets EDNS right. */
+                        log_debug("More resource records declared in packet than included, suppressing OPT.");
+                        bad_opt = true;
+                        break;
+                }
+
+                r = dns_packet_read_rr(p, &rr, &cache_flush, &start);
+                if (r < 0)
+                        return r;
+
+                /* Try to reduce memory usage a bit */
+                if (previous)
+                        dns_resource_key_reduce(&rr->key, &previous->key);
+
+                if (rr->key->type == DNS_TYPE_OPT) {
+                        bool has_rfc6975;
+
+                        if (p->opt || bad_opt) {
+                                /* Multiple OPT RRs? if so, let's ignore all, because there's
+                                 * something wrong with the server, and if one is valid we wouldn't
+                                 * know which one. */
+                                log_debug("Multiple OPT RRs detected, ignoring all.");
+                                bad_opt = true;
+                                continue;
+                        }
+
+                        if (!dns_name_is_root(dns_resource_key_name(rr->key))) {
+                                /* If the OPT RR is not owned by the root domain, then it is bad,
+                                 * let's ignore it. */
+                                log_debug("OPT RR is not owned by root domain, ignoring.");
+                                bad_opt = true;
+                                continue;
+                        }
+
+                        if (i < DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)) {
+                                /* OPT RR is in the wrong section? Some Belkin routers do this. This
+                                 * is a hint the EDNS implementation is borked, like the Belkin one
+                                 * is, hence ignore it. */
+                                log_debug("OPT RR in wrong section, ignoring.");
+                                bad_opt = true;
+                                continue;
+                        }
+
+                        if (!opt_is_good(rr, &has_rfc6975)) {
+                                log_debug("Malformed OPT RR, ignoring.");
+                                bad_opt = true;
+                                continue;
+                        }
+
+                        if (DNS_PACKET_QR(p)) {
+                                /* Additional checks for responses */
+
+                                if (!DNS_RESOURCE_RECORD_OPT_VERSION_SUPPORTED(rr))
+                                        /* If this is a reply and we don't know the EDNS version
+                                         * then something is weird... */
+                                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
+                                                               "EDNS version newer that our request, bad server.");
+
+                                if (has_rfc6975) {
+                                        /* If the OPT RR contains RFC6975 algorithm data, then this
+                                         * is indication that the server just copied the OPT it got
+                                         * from us (which contained that data) back into the reply.
+                                         * If so, then it doesn't properly support EDNS, as RFC6975
+                                         * makes it very clear that the algorithm data should only
+                                         * be contained in questions, never in replies. Crappy
+                                         * Belkin routers copy the OPT data for example, hence let's
+                                         * detect this so that we downgrade early. */
+                                        log_debug("OPT RR contains RFC6975 data, ignoring.");
+                                        bad_opt = true;
+                                        continue;
+                                }
+                        }
+
+                        p->opt = dns_resource_record_ref(rr);
+                        p->opt_start = start;
+                        assert(p->rindex >= start);
+                        p->opt_size = p->rindex - start;
+                } else {
+                        DnsAnswerFlags flags = 0;
+
+                        if (p->protocol == DNS_PROTOCOL_MDNS && !cache_flush)
+                                flags |= DNS_ANSWER_SHARED_OWNER;
+
+                        /* According to RFC 4795, section 2.9. only the RRs from the Answer section shall be
+                         * cached. Hence mark only those RRs as cacheable by default, but not the ones from
+                         * the Additional or Authority sections. */
+                        if (i < DNS_PACKET_ANCOUNT(p))
+                                flags |= DNS_ANSWER_CACHEABLE|DNS_ANSWER_SECTION_ANSWER;
+                        else if (i < DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p))
+                                flags |= DNS_ANSWER_SECTION_AUTHORITY;
+                        else
+                                flags |= DNS_ANSWER_SECTION_ADDITIONAL;
+
+                        r = dns_answer_add(answer, rr, p->ifindex, flags, NULL);
+                        if (r < 0)
+                                return r;
+                }
+
+                /* Remember this RR, so that we can potentially merge its ->key object with the
+                 * next RR. Note that we only do this if we actually decided to keep the RR around.
+                 */
+                dns_resource_record_unref(previous);
+                previous = dns_resource_record_ref(rr);
+        }
+
+        if (bad_opt) {
+                p->opt = dns_resource_record_unref(p->opt);
+                p->opt_start = p->opt_size = SIZE_MAX;
+        }
+
+        *ret_answer = TAKE_PTR(answer);
+
+        return 0;
+}
+
+int dns_packet_extract(DnsPacket *p) {
+        _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;
+
+        if (p->rindex < p->size)  {
+                log_debug("Trailing garbage in packet, suppressing OPT.");
+                p->opt = dns_resource_record_unref(p->opt);
+                p->opt_start = p->opt_size = SIZE_MAX;
+        }
+
+        p->question = TAKE_PTR(question);
+        p->answer = TAKE_PTR(answer);
+
+        p->extracted = true;
+
+        /* no CANCEL, always rewind */
+        return 0;
+}
+
+int dns_packet_is_reply_for(DnsPacket *p, const DnsResourceKey *key) {
+        int r;
+
+        assert(p);
+        assert(key);
+
+        /* Checks if the specified packet is a reply for the specified
+         * key and the specified key is the only one in the question
+         * section. */
+
+        if (DNS_PACKET_QR(p) != 1)
+                return 0;
+
+        /* Let's unpack the packet, if that hasn't happened yet. */
+        r = dns_packet_extract(p);
+        if (r < 0)
+                return r;
+
+        if (!p->question)
+                return 0;
+
+        if (p->question->n_keys != 1)
+                return 0;
+
+        return dns_resource_key_equal(dns_question_first_key(p->question), key);
+}
+
+int dns_packet_patch_max_udp_size(DnsPacket *p, uint16_t max_udp_size) {
+        assert(p);
+        assert(max_udp_size >= DNS_PACKET_UNICAST_SIZE_MAX);
+
+        if (p->opt_start == SIZE_MAX) /* No OPT section, nothing to patch */
+                return 0;
+
+        assert(p->opt_size != SIZE_MAX);
+        assert(p->opt_size >= 5);
+
+        unaligned_write_be16(DNS_PACKET_DATA(p) + p->opt_start + 3, max_udp_size);
+        return 1;
+}
+
+static int patch_rr(DnsPacket *p, usec_t age) {
+        _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder;
+        size_t ttl_index;
+        uint32_t ttl;
+        uint16_t type, rdlength;
+        int r;
+
+        INIT_REWINDER(rewinder, p);
+
+        /* Patches the RR at the current rindex, subtracts the specified time from the TTL */
+
+        r = dns_packet_read_name(p, NULL, true, NULL);
+        if (r < 0)
+                return r;
+
+        r = dns_packet_read_uint16(p, &type, NULL);
+        if (r < 0)
+                return r;
+
+        r = dns_packet_read_uint16(p, NULL, NULL);
+        if (r < 0)
+                return r;
+
+        r = dns_packet_read_uint32(p, &ttl, &ttl_index);
+        if (r < 0)
+                return r;
+
+        if (type != DNS_TYPE_OPT) { /* The TTL of the OPT field is not actually a TTL, skip it */
+                ttl = LESS_BY(ttl * USEC_PER_SEC, age) / USEC_PER_SEC;
+                unaligned_write_be32(DNS_PACKET_DATA(p) + ttl_index, ttl);
+        }
+
+        r = dns_packet_read_uint16(p, &rdlength, NULL);
+        if (r < 0)
+                return r;
+
+        r = dns_packet_read(p, rdlength, NULL, NULL);
+        if (r < 0)
+                return r;
+
+        CANCEL_REWINDER(rewinder);
+        return 0;
+}
+
+int dns_packet_patch_ttls(DnsPacket *p, usec_t timestamp) {
+        _cleanup_(rewind_dns_packet) DnsPacketRewinder rewinder = {};
+        unsigned i, n;
+        usec_t k;
+        int r;
+
+        assert(p);
+        assert(timestamp_is_set(timestamp));
+
+        /* Adjusts all TTLs in the packet by subtracting the time difference between now and the specified timestamp */
+
+        k = now(clock_boottime_or_monotonic());
+        assert(k >= timestamp);
+        k -= timestamp;
+
+        INIT_REWINDER(rewinder, p);
+
+        dns_packet_rewind(p, DNS_PACKET_HEADER_SIZE);
+
+        n = DNS_PACKET_QDCOUNT(p);
+        for (i = 0; i < n; i++) {
+                r = dns_packet_read_key(p, NULL, NULL, NULL);
+                if (r < 0)
+                        return r;
+        }
+
+        n = DNS_PACKET_RRCOUNT(p);
+        for (i = 0; i < n; i++) {
+
+                /* DNS servers suck, hence the RR count is in many servers off. If we reached the end
+                 * prematurely, accept that, exit early */
+                if (p->rindex == p->size)
+                        break;
+
+                r = patch_rr(p, k);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static void dns_packet_hash_func(const DnsPacket *s, struct siphash *state) {
+        assert(s);
+
+        siphash24_compress(&s->size, sizeof(s->size), state);
+        siphash24_compress(DNS_PACKET_DATA((DnsPacket*) s), s->size, state);
+}
+
+static int dns_packet_compare_func(const DnsPacket *x, const DnsPacket *y) {
+        int r;
+
+        r = CMP(x->size, y->size);
+        if (r != 0)
+                return r;
+
+        return memcmp(DNS_PACKET_DATA((DnsPacket*) x), DNS_PACKET_DATA((DnsPacket*) y), x->size);
+}
+
+DEFINE_HASH_OPS(dns_packet_hash_ops, DnsPacket, dns_packet_hash_func, dns_packet_compare_func);
+
+bool dns_packet_equal(const DnsPacket *a, const DnsPacket *b) {
+        return dns_packet_compare_func(a, b) == 0;
+}
+
+int dns_packet_has_nsid_request(DnsPacket *p) {
+        bool has_nsid = false;
+        const uint8_t *d;
+        size_t l;
+
+        assert(p);
+
+        if (!p->opt)
+                return false;
+
+        d = p->opt->opt.data;
+        l = p->opt->opt.data_size;
+
+        while (l > 0) {
+                uint16_t code, length;
+
+                if (l < 4U)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
+                                               "EDNS0 variable part has invalid size.");
+
+                code = unaligned_read_be16(d);
+                length = unaligned_read_be16(d + 2);
+
+                if (l < 4U + length)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
+                                               "Truncated option in EDNS0 variable part.");
+
+                if (code == 3) {
+                        if (has_nsid)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
+                                                       "Duplicate NSID option in EDNS0 variable part.");
+
+                        if (length != 0)
+                                return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
+                                                       "Non-empty NSID option in DNS request.");
+
+                        has_nsid = true;
+                }
+
+                d += 4U + length;
+                l -= 4U + length;
+        }
+
+        return has_nsid;
+}
+
+size_t dns_packet_size_unfragmented(DnsPacket *p) {
+        assert(p);
+
+        if (p->fragsize == 0) /* Wasn't fragmented */
+                return p->size;
+
+        /* The fragment size (p->fragsize) covers the whole (fragmented) IP packet, while the regular packet
+         * size (p->size) only covers the DNS part. Thus, subtract the UDP header from the largest fragment
+         * size, in order to determine which size of DNS packet would have gone through without
+         * fragmenting. */
+
+        return LESS_BY(p->fragsize, udp_header_size(p->family));
+}
+
+static const char* const dns_rcode_table[_DNS_RCODE_MAX_DEFINED] = {
+        [DNS_RCODE_SUCCESS]   = "SUCCESS",
+        [DNS_RCODE_FORMERR]   = "FORMERR",
+        [DNS_RCODE_SERVFAIL]  = "SERVFAIL",
+        [DNS_RCODE_NXDOMAIN]  = "NXDOMAIN",
+        [DNS_RCODE_NOTIMP]    = "NOTIMP",
+        [DNS_RCODE_REFUSED]   = "REFUSED",
+        [DNS_RCODE_YXDOMAIN]  = "YXDOMAIN",
+        [DNS_RCODE_YXRRSET]   = "YRRSET",
+        [DNS_RCODE_NXRRSET]   = "NXRRSET",
+        [DNS_RCODE_NOTAUTH]   = "NOTAUTH",
+        [DNS_RCODE_NOTZONE]   = "NOTZONE",
+        [DNS_RCODE_BADVERS]   = "BADVERS",
+        [DNS_RCODE_BADKEY]    = "BADKEY",
+        [DNS_RCODE_BADTIME]   = "BADTIME",
+        [DNS_RCODE_BADMODE]   = "BADMODE",
+        [DNS_RCODE_BADNAME]   = "BADNAME",
+        [DNS_RCODE_BADALG]    = "BADALG",
+        [DNS_RCODE_BADTRUNC]  = "BADTRUNC",
+        [DNS_RCODE_BADCOOKIE] = "BADCOOKIE",
+};
+DEFINE_STRING_TABLE_LOOKUP(dns_rcode, int);
+
+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..6e7175c
--- /dev/null
+++ b/src/resolve/resolved-dns-packet.h
@@ -0,0 +1,339 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/udp.h>
+
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "macro.h"
+#include "sparse-endian.h"
+
+typedef struct DnsPacketHeader DnsPacketHeader;
+typedef struct DnsPacket DnsPacket;
+
+#include "resolved-def.h"
+#include "resolved-dns-answer.h"
+#include "resolved-dns-question.h"
+#include "resolved-dns-rr.h"
+
+typedef enum DnsProtocol {
+        DNS_PROTOCOL_DNS,
+        DNS_PROTOCOL_MDNS,
+        DNS_PROTOCOL_LLMNR,
+        _DNS_PROTOCOL_MAX,
+        _DNS_PROTOCOL_INVALID = -EINVAL,
+} DnsProtocol;
+
+struct DnsPacketHeader {
+        uint16_t id;
+        be16_t flags;
+        be16_t qdcount;
+        be16_t ancount;
+        be16_t nscount;
+        be16_t arcount;
+} _packed_;
+
+#define DNS_PACKET_HEADER_SIZE sizeof(DnsPacketHeader)
+#define UDP4_PACKET_HEADER_SIZE (sizeof(struct iphdr) + sizeof(struct udphdr))
+#define UDP6_PACKET_HEADER_SIZE (sizeof(struct ip6_hdr) + sizeof(struct udphdr))
+
+assert_cc(sizeof(struct ip6_hdr) == 40);
+assert_cc(sizeof(struct iphdr) == 20);
+assert_cc(sizeof(struct udphdr) == 8);
+assert_cc(sizeof(DnsPacketHeader) == 12);
+
+/* The various DNS protocols deviate in how large a packet can grow, but the TCP transport has a 16bit size
+ * field, hence that appears to be the absolute maximum. */
+#define DNS_PACKET_SIZE_MAX 0xFFFFu
+
+/* The default size to use for allocation when we don't know how large
+ * the packet will turn out to be. */
+#define DNS_PACKET_SIZE_START 512u
+
+/* RFC 1035 say 512 is the maximum, for classic unicast DNS */
+#define DNS_PACKET_UNICAST_SIZE_MAX 512u
+
+/* With EDNS0 we can use larger packets, default to 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, fragsize;
+        void *_data; /* don't access directly, use DNS_PACKET_DATA()! */
+        Hashmap *names; /* For name compression */
+        size_t opt_start, opt_size;
+
+        /* Parsed data */
+        DnsQuestion *question;
+        DnsAnswer *answer;
+        DnsResourceRecord *opt;
+
+        /* For support of truncated packets */
+        DnsPacket *more;
+
+        /* Packet reception metadata */
+        usec_t timestamp; /* CLOCK_BOOTTIME (or CLOCK_MONOTONIC if the former doesn't exist) */
+        int ifindex;
+        int family, ipproto;
+        union in_addr_union sender, destination;
+        uint16_t sender_port, destination_port;
+        uint32_t ttl;
+
+        bool on_stack;
+        bool extracted;
+        bool refuse_compression;
+        bool canonical_form;
+
+        /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */
+};
+
+static inline uint8_t* DNS_PACKET_DATA(const DnsPacket *p) {
+        if (_unlikely_(!p))
+                return NULL;
+
+        if (p->_data)
+                return p->_data;
+
+        return ((uint8_t*) p) + ALIGN(sizeof(DnsPacket));
+}
+
+#define DNS_PACKET_HEADER(p) ((DnsPacketHeader*) DNS_PACKET_DATA(p))
+#define DNS_PACKET_ID(p) DNS_PACKET_HEADER(p)->id
+#define DNS_PACKET_QR(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 15) & 1)
+#define DNS_PACKET_OPCODE(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 11) & 15)
+#define DNS_PACKET_AA(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 10) & 1)
+#define DNS_PACKET_TC(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 9) & 1)
+#define DNS_PACKET_RD(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 8) & 1)
+#define DNS_PACKET_RA(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 7) & 1)
+#define DNS_PACKET_AD(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 5) & 1)
+#define DNS_PACKET_CD(p) ((be16toh(DNS_PACKET_HEADER(p)->flags) >> 4) & 1)
+
+#define DNS_PACKET_FLAG_TC (UINT16_C(1) << 9)
+
+static inline uint16_t DNS_PACKET_RCODE(DnsPacket *p) {
+        uint16_t rcode;
+
+        if (p->opt)
+                rcode = (uint16_t) (p->opt->ttl >> 24);
+        else
+                rcode = 0;
+
+        return rcode | (be16toh(DNS_PACKET_HEADER(p)->flags) & 0xF);
+}
+
+static inline uint16_t DNS_PACKET_PAYLOAD_SIZE_MAX(DnsPacket *p) {
+
+        /* Returns the advertised maximum size for replies, or the DNS default if there's nothing defined. */
+
+        if (p->ipproto == IPPROTO_TCP) /* we ignore EDNS(0) size data on TCP, like everybody else */
+                return DNS_PACKET_SIZE_MAX;
+
+        if (p->opt)
+                return MAX(DNS_PACKET_UNICAST_SIZE_MAX, p->opt->key->class);
+
+        return DNS_PACKET_UNICAST_SIZE_MAX;
+}
+
+static inline bool DNS_PACKET_DO(DnsPacket *p) {
+        if (!p->opt)
+                return false;
+
+        return !!(p->opt->ttl & (1U << 15));
+}
+
+static inline bool DNS_PACKET_VERSION_SUPPORTED(DnsPacket *p) {
+        /* Returns true if this packet is in a version we support. Which means either non-EDNS or EDNS(0), but not EDNS
+         * of any newer versions */
+
+        if (!p->opt)
+                return true;
+
+        return DNS_RESOURCE_RECORD_OPT_VERSION_SUPPORTED(p->opt);
+}
+
+static inline bool DNS_PACKET_IS_FRAGMENTED(DnsPacket *p) {
+        assert(p);
+
+        /* For ingress packets: was this packet fragmented according to our knowledge? */
+
+        return p->fragsize != 0;
+}
+
+/* LLMNR defines some bits differently */
+#define DNS_PACKET_LLMNR_C(p) DNS_PACKET_AA(p)
+#define DNS_PACKET_LLMNR_T(p) DNS_PACKET_RD(p)
+
+#define DNS_PACKET_QDCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->qdcount)
+#define DNS_PACKET_ANCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->ancount)
+#define DNS_PACKET_NSCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->nscount)
+#define DNS_PACKET_ARCOUNT(p) be16toh(DNS_PACKET_HEADER(p)->arcount)
+
+#define DNS_PACKET_MAKE_FLAGS(qr, opcode, aa, tc, rd, ra, ad, cd, rcode) \
+        (((uint16_t) !!(qr) << 15) |                                    \
+         ((uint16_t) ((opcode) & 15) << 11) |                           \
+         ((uint16_t) !!(aa) << 10) |                /* on LLMNR: c */   \
+         ((uint16_t) !!(tc) << 9) |                                     \
+         ((uint16_t) !!(rd) << 8) |                 /* on LLMNR: t */   \
+         ((uint16_t) !!(ra) << 7) |                                     \
+         ((uint16_t) !!(ad) << 5) |                                     \
+         ((uint16_t) !!(cd) << 4) |                                     \
+         ((uint16_t) ((rcode) & 15)))
+
+static inline unsigned DNS_PACKET_RRCOUNT(DnsPacket *p) {
+        return
+                (unsigned) DNS_PACKET_ANCOUNT(p) +
+                (unsigned) DNS_PACKET_NSCOUNT(p) +
+                (unsigned) DNS_PACKET_ARCOUNT(p);
+}
+
+int dns_packet_new(DnsPacket **p, DnsProtocol protocol, size_t min_alloc_dsize, size_t max_size);
+int dns_packet_new_query(DnsPacket **p, DnsProtocol protocol, size_t min_alloc_dsize, bool dnssec_checking_disabled);
+
+int dns_packet_dup(DnsPacket **ret, DnsPacket *p);
+
+void dns_packet_set_flags(DnsPacket *p, bool dnssec_checking_disabled, bool truncated);
+
+DnsPacket *dns_packet_ref(DnsPacket *p);
+DnsPacket *dns_packet_unref(DnsPacket *p);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsPacket*, dns_packet_unref);
+
+int dns_packet_validate(DnsPacket *p);
+int dns_packet_validate_reply(DnsPacket *p);
+int dns_packet_validate_query(DnsPacket *p);
+
+int dns_packet_is_reply_for(DnsPacket *p, const DnsResourceKey *key);
+
+int dns_packet_append_blob(DnsPacket *p, const void *d, size_t sz, size_t *start);
+int dns_packet_append_uint8(DnsPacket *p, uint8_t v, size_t *start);
+int dns_packet_append_uint16(DnsPacket *p, uint16_t v, size_t *start);
+int dns_packet_append_uint32(DnsPacket *p, uint32_t v, size_t *start);
+int dns_packet_append_string(DnsPacket *p, const char *s, size_t *start);
+int dns_packet_append_raw_string(DnsPacket *p, const void *s, size_t size, size_t *start);
+int dns_packet_append_label(DnsPacket *p, const char *s, size_t l, bool canonical_candidate, size_t *start);
+int dns_packet_append_name(DnsPacket *p, const char *name, bool allow_compression, bool canonical_candidate, size_t *start);
+int dns_packet_append_key(DnsPacket *p, const DnsResourceKey *key, const DnsAnswerFlags flags, size_t *start);
+int dns_packet_append_rr(DnsPacket *p, const DnsResourceRecord *rr, const DnsAnswerFlags flags, size_t *start, size_t *rdata_start);
+int dns_packet_append_opt(DnsPacket *p, uint16_t max_udp_size, bool edns0_do, bool include_rfc6975, const char *nsid, int rcode, size_t *ret_start);
+int dns_packet_append_question(DnsPacket *p, DnsQuestion *q);
+int dns_packet_append_answer(DnsPacket *p, DnsAnswer *a, unsigned *completed);
+
+int dns_packet_patch_max_udp_size(DnsPacket *p, uint16_t max_udp_size);
+int dns_packet_patch_ttls(DnsPacket *p, usec_t timestamp);
+
+void dns_packet_truncate(DnsPacket *p, size_t sz);
+int dns_packet_truncate_opt(DnsPacket *p);
+
+int dns_packet_read(DnsPacket *p, size_t sz, const void **ret, size_t *start);
+int dns_packet_read_blob(DnsPacket *p, void *d, size_t sz, size_t *start);
+int dns_packet_read_uint8(DnsPacket *p, uint8_t *ret, size_t *start);
+int dns_packet_read_uint16(DnsPacket *p, uint16_t *ret, size_t *start);
+int dns_packet_read_uint32(DnsPacket *p, uint32_t *ret, size_t *start);
+int dns_packet_read_string(DnsPacket *p, char **ret, size_t *start);
+int dns_packet_read_raw_string(DnsPacket *p, const void **ret, size_t *size, size_t *start);
+int dns_packet_read_name(DnsPacket *p, char **ret, bool allow_compression, size_t *start);
+int dns_packet_read_key(DnsPacket *p, DnsResourceKey **ret, bool *ret_cache_flush_or_qu, size_t *start);
+int dns_packet_read_rr(DnsPacket *p, DnsResourceRecord **ret, bool *ret_cache_flush, size_t *start);
+
+void dns_packet_rewind(DnsPacket *p, size_t idx);
+
+int dns_packet_skip_question(DnsPacket *p);
+int dns_packet_extract(DnsPacket *p);
+
+bool dns_packet_equal(const DnsPacket *a, const DnsPacket *b);
+
+int dns_packet_has_nsid_request(DnsPacket *p);
+
+/* https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-6 */
+enum {
+        DNS_RCODE_SUCCESS = 0,
+        DNS_RCODE_FORMERR = 1,
+        DNS_RCODE_SERVFAIL = 2,
+        DNS_RCODE_NXDOMAIN = 3,
+        DNS_RCODE_NOTIMP = 4,
+        DNS_RCODE_REFUSED = 5,
+        DNS_RCODE_YXDOMAIN = 6,
+        DNS_RCODE_YXRRSET = 7,
+        DNS_RCODE_NXRRSET = 8,
+        DNS_RCODE_NOTAUTH = 9,
+        DNS_RCODE_NOTZONE = 10,
+        DNS_RCODE_BADVERS = 16,
+        DNS_RCODE_BADSIG = 16, /* duplicate value! */
+        DNS_RCODE_BADKEY = 17,
+        DNS_RCODE_BADTIME = 18,
+        DNS_RCODE_BADMODE = 19,
+        DNS_RCODE_BADNAME = 20,
+        DNS_RCODE_BADALG = 21,
+        DNS_RCODE_BADTRUNC = 22,
+        DNS_RCODE_BADCOOKIE = 23,
+        _DNS_RCODE_MAX_DEFINED,
+        _DNS_RCODE_MAX = 4095 /* 4 bit rcode in the header plus 8 bit rcode in OPT, makes 12 bit */
+};
+
+const char* dns_rcode_to_string(int i) _const_;
+int dns_rcode_from_string(const char *s) _pure_;
+
+const char* dns_protocol_to_string(DnsProtocol p) _const_;
+DnsProtocol dns_protocol_from_string(const char *s) _pure_;
+
+#define LLMNR_MULTICAST_IPV4_ADDRESS ((struct in_addr) { .s_addr = htobe32(224U << 24 | 252U) })
+#define LLMNR_MULTICAST_IPV6_ADDRESS ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03 } })
+
+#define MDNS_MULTICAST_IPV4_ADDRESS  ((struct in_addr) { .s_addr = htobe32(224U << 24 | 251U) })
+#define MDNS_MULTICAST_IPV6_ADDRESS  ((struct in6_addr) { .s6_addr = { 0xFF, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfb } })
+
+extern const struct hash_ops dns_packet_hash_ops;
+
+static inline uint64_t SD_RESOLVED_FLAGS_MAKE(
+                DnsProtocol protocol,
+                int family,
+                bool authenticated,
+                bool confidential) {
+        uint64_t f;
+
+        /* Converts a protocol + family into a flags field as used in queries and responses */
+
+        f = (authenticated ? SD_RESOLVED_AUTHENTICATED : 0) |
+                (confidential ? SD_RESOLVED_CONFIDENTIAL : 0);
+
+        switch (protocol) {
+        case DNS_PROTOCOL_DNS:
+                return f|SD_RESOLVED_DNS;
+
+        case DNS_PROTOCOL_LLMNR:
+                return f|(family == AF_INET6 ? SD_RESOLVED_LLMNR_IPV6 : SD_RESOLVED_LLMNR_IPV4);
+
+        case DNS_PROTOCOL_MDNS:
+                return f|(family == AF_INET6 ? SD_RESOLVED_MDNS_IPV6 : SD_RESOLVED_MDNS_IPV4);
+
+        default:
+                return f;
+        }
+}
+
+static inline size_t dns_packet_size_max(DnsPacket *p) {
+        assert(p);
+
+        /* Why not insist on a fully initialized max_size during DnsPacket construction? Well, this way it's easy to
+         * allocate a transient, throw-away DnsPacket on the stack by simple zero initialization, without having to
+         * deal with explicit field initialization. */
+
+        return p->max_size != 0 ? p->max_size : DNS_PACKET_SIZE_MAX;
+}
+
+static inline size_t udp_header_size(int af) {
+
+        switch (af) {
+        case AF_INET:
+                return UDP4_PACKET_HEADER_SIZE;
+        case AF_INET6:
+                return UDP6_PACKET_HEADER_SIZE;
+        default:
+                assert_not_reached("Unexpected address family");
+        }
+}
+
+size_t dns_packet_size_unfragmented(DnsPacket *p);
diff --git a/src/resolve/resolved-dns-query.c b/src/resolve/resolved-dns-query.c
new file mode 100644
index 0000000..24cd7cd
--- /dev/null
+++ b/src/resolve/resolved-dns-query.c
@@ -0,0 +1,1271 @@
+/* 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 QUERIES_MAX 2048
+#define AUXILIARY_QUERIES_MAX 64
+#define CNAME_REDIRECTS_MAX 16
+
+assert_cc(AUXILIARY_QUERIES_MAX < UINT8_MAX);
+assert_cc(CNAME_REDIRECTS_MAX < UINT8_MAX);
+
+static int dns_query_candidate_new(DnsQueryCandidate **ret, DnsQuery *q, DnsScope *s) {
+        DnsQueryCandidate *c;
+
+        assert(ret);
+        assert(q);
+        assert(s);
+
+        c = new(DnsQueryCandidate, 1);
+        if (!c)
+                return -ENOMEM;
+
+        *c = (DnsQueryCandidate) {
+                .n_ref = 1,
+                .query = q,
+                .scope = s,
+        };
+
+        LIST_PREPEND(candidates_by_query, q->candidates, c);
+        LIST_PREPEND(candidates_by_scope, s->query_candidates, c);
+
+        *ret = c;
+        return 0;
+}
+
+static void dns_query_candidate_stop(DnsQueryCandidate *c) {
+        DnsTransaction *t;
+
+        assert(c);
+
+        /* Detach all the DnsTransactions attached to this query */
+
+        while ((t = set_steal_first(c->transactions))) {
+                set_remove(t->notify_query_candidates, c);
+                set_remove(t->notify_query_candidates_done, c);
+                dns_transaction_gc(t);
+        }
+}
+
+static DnsQueryCandidate* dns_query_candidate_unlink(DnsQueryCandidate *c) {
+        assert(c);
+
+        /* Detach this DnsQueryCandidate from the Query and Scope objects */
+
+        if (c->query) {
+                LIST_REMOVE(candidates_by_query, c->query->candidates, c);
+                c->query = NULL;
+        }
+
+        if (c->scope) {
+                LIST_REMOVE(candidates_by_scope, c->scope->query_candidates, c);
+                c->scope = NULL;
+        }
+
+        return c;
+}
+
+static DnsQueryCandidate* dns_query_candidate_free(DnsQueryCandidate *c) {
+        if (!c)
+                return NULL;
+
+        dns_query_candidate_stop(c);
+        dns_query_candidate_unlink(c);
+
+        set_free(c->transactions);
+        dns_search_domain_unref(c->search_domain);
+
+        return mfree(c);
+}
+
+DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(DnsQueryCandidate, dns_query_candidate, dns_query_candidate_free);
+
+static int dns_query_candidate_next_search_domain(DnsQueryCandidate *c) {
+        DnsSearchDomain *next;
+
+        assert(c);
+
+        if (c->search_domain && c->search_domain->linked)
+                next = c->search_domain->domains_next;
+        else
+                next = dns_scope_get_search_domains(c->scope);
+
+        for (;;) {
+                if (!next) /* We hit the end of the list */
+                        return 0;
+
+                if (!next->route_only)
+                        break;
+
+                /* Skip over route-only domains */
+                next = next->domains_next;
+        }
+
+        dns_search_domain_unref(c->search_domain);
+        c->search_domain = dns_search_domain_ref(next);
+
+        return 1;
+}
+
+static int dns_query_candidate_add_transaction(
+                DnsQueryCandidate *c,
+                DnsResourceKey *key,
+                DnsPacket *bypass) {
+
+        _cleanup_(dns_transaction_gcp) DnsTransaction *t = NULL;
+        int r;
+
+        assert(c);
+        assert(c->query); /* We shan't add transactions to a candidate that has been detached already */
+
+        if (key) {
+                /* Regular lookup with a resource key */
+                assert(!bypass);
+
+                t = dns_scope_find_transaction(c->scope, key, c->query->flags);
+                if (!t) {
+                        r = dns_transaction_new(&t, c->scope, key, NULL, c->query->flags);
+                        if (r < 0)
+                                return r;
+                } else if (set_contains(c->transactions, t))
+                        return 0;
+        } else {
+                /* "Bypass" lookup with a query packet */
+                assert(bypass);
+
+                r = dns_transaction_new(&t, c->scope, NULL, bypass, c->query->flags);
+                if (r < 0)
+                        return r;
+        }
+
+        r = set_ensure_allocated(&t->notify_query_candidates_done, NULL);
+        if (r < 0)
+                return r;
+
+        r = set_ensure_put(&t->notify_query_candidates, NULL, c);
+        if (r < 0)
+                return r;
+
+        r = set_ensure_put(&c->transactions, NULL, t);
+        if (r < 0) {
+                (void) set_remove(t->notify_query_candidates, c);
+                return r;
+        }
+
+        TAKE_PTR(t);
+        return 1;
+}
+
+static int dns_query_candidate_go(DnsQueryCandidate *c) {
+        _unused_ _cleanup_(dns_query_candidate_unrefp) DnsQueryCandidate *keep_c = NULL;
+        DnsTransaction *t;
+        int r;
+        unsigned n = 0;
+
+        assert(c);
+
+        /* Let's keep a reference to the query while we're operating */
+        keep_c = dns_query_candidate_ref(c);
+
+        /* Start the transactions that are not started yet */
+        SET_FOREACH(t, c->transactions) {
+                if (t->state != DNS_TRANSACTION_NULL)
+                        continue;
+
+                r = dns_transaction_go(t);
+                if (r < 0)
+                        return r;
+
+                n++;
+        }
+
+        /* If there was nothing to start, then let's proceed immediately */
+        if (n == 0)
+                dns_query_candidate_notify(c);
+
+        return 0;
+}
+
+static DnsTransactionState dns_query_candidate_state(DnsQueryCandidate *c) {
+        DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS;
+        DnsTransaction *t;
+
+        assert(c);
+
+        if (c->error_code != 0)
+                return DNS_TRANSACTION_ERRNO;
+
+        SET_FOREACH(t, c->transactions)
+
+                switch (t->state) {
+
+                case DNS_TRANSACTION_NULL:
+                        /* If there's a NULL transaction pending, then
+                         * this means not all transactions where
+                         * started yet, and we were called from within
+                         * the stackframe that is supposed to start
+                         * remaining transactions. In this case,
+                         * simply claim the candidate is pending. */
+
+                case DNS_TRANSACTION_PENDING:
+                case DNS_TRANSACTION_VALIDATING:
+                        /* If there's one transaction currently in
+                         * VALIDATING state, then this means there's
+                         * also one in PENDING state, hence we can
+                         * return PENDING immediately. */
+                        return DNS_TRANSACTION_PENDING;
+
+                case DNS_TRANSACTION_SUCCESS:
+                        state = t->state;
+                        break;
+
+                default:
+                        if (state != DNS_TRANSACTION_SUCCESS)
+                                state = t->state;
+
+                        break;
+                }
+
+        return state;
+}
+
+static int dns_query_candidate_setup_transactions(DnsQueryCandidate *c) {
+        DnsQuestion *question;
+        DnsResourceKey *key;
+        int n = 0, r;
+
+        assert(c);
+        assert(c->query); /* We shan't add transactions to a candidate that has been detached already */
+
+        dns_query_candidate_stop(c);
+
+        if (c->query->question_bypass) {
+                /* If this is a bypass query, then pass the original query packet along to the transaction */
+
+                assert(dns_question_size(c->query->question_bypass->question) == 1);
+
+                if (!dns_scope_good_key(c->scope, dns_question_first_key(c->query->question_bypass->question)))
+                        return 0;
+
+                r = dns_query_candidate_add_transaction(c, NULL, c->query->question_bypass);
+                if (r < 0)
+                        goto fail;
+
+                return 1;
+        }
+
+        question = dns_query_question_for_protocol(c->query, c->scope->protocol);
+
+        /* Create one transaction per question key */
+        DNS_QUESTION_FOREACH(key, question) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *new_key = NULL;
+                DnsResourceKey *qkey;
+
+                if (c->search_domain) {
+                        r = dns_resource_key_new_append_suffix(&new_key, key, c->search_domain->name);
+                        if (r < 0)
+                                goto fail;
+
+                        qkey = new_key;
+                } else
+                        qkey = key;
+
+                if (!dns_scope_good_key(c->scope, qkey))
+                        continue;
+
+                r = dns_query_candidate_add_transaction(c, qkey, NULL);
+                if (r < 0)
+                        goto fail;
+
+                n++;
+        }
+
+        return n;
+
+fail:
+        dns_query_candidate_stop(c);
+        return r;
+}
+
+void dns_query_candidate_notify(DnsQueryCandidate *c) {
+        DnsTransactionState state;
+        int r;
+
+        assert(c);
+
+        if (!c->query) /* This candidate has been abandoned, do nothing. */
+                return;
+
+        state = dns_query_candidate_state(c);
+
+        if (DNS_TRANSACTION_IS_LIVE(state))
+                return;
+
+        if (state != DNS_TRANSACTION_SUCCESS && c->search_domain) {
+
+                r = dns_query_candidate_next_search_domain(c);
+                if (r < 0)
+                        goto fail;
+
+                if (r > 0) {
+                        /* OK, there's another search domain to try, let's do so. */
+
+                        r = dns_query_candidate_setup_transactions(c);
+                        if (r < 0)
+                                goto fail;
+
+                        if (r > 0) {
+                                /* New transactions where queued. Start them and wait */
+
+                                r = dns_query_candidate_go(c);
+                                if (r < 0)
+                                        goto fail;
+
+                                return;
+                        }
+                }
+
+        }
+
+        dns_query_ready(c->query);
+        return;
+
+fail:
+        c->error_code = log_warning_errno(r, "Failed to follow search domains: %m");
+        dns_query_ready(c->query);
+}
+
+static void dns_query_stop(DnsQuery *q) {
+        DnsQueryCandidate *c;
+
+        assert(q);
+
+        q->timeout_event_source = sd_event_source_disable_unref(q->timeout_event_source);
+
+        LIST_FOREACH(candidates_by_query, c, q->candidates)
+                dns_query_candidate_stop(c);
+}
+
+static void dns_query_unlink_candidates(DnsQuery *q) {
+        assert(q);
+
+        while (q->candidates)
+                /* Here we drop *our* references to each of the candidates. If we had the only reference, the
+                 * DnsQueryCandidate object will be freed. */
+                dns_query_candidate_unref(dns_query_candidate_unlink(q->candidates));
+}
+
+static void dns_query_reset_answer(DnsQuery *q) {
+        assert(q);
+
+        q->answer = dns_answer_unref(q->answer);
+        q->answer_rcode = 0;
+        q->answer_dnssec_result = _DNSSEC_RESULT_INVALID;
+        q->answer_errno = 0;
+        q->answer_query_flags = 0;
+        q->answer_protocol = _DNS_PROTOCOL_INVALID;
+        q->answer_family = AF_UNSPEC;
+        q->answer_search_domain = dns_search_domain_unref(q->answer_search_domain);
+        q->answer_full_packet = dns_packet_unref(q->answer_full_packet);
+}
+
+DnsQuery *dns_query_free(DnsQuery *q) {
+        if (!q)
+                return NULL;
+
+        q->timeout_event_source = sd_event_source_disable_unref(q->timeout_event_source);
+
+        while (q->auxiliary_queries)
+                dns_query_free(q->auxiliary_queries);
+
+        if (q->auxiliary_for) {
+                assert(q->auxiliary_for->n_auxiliary_queries > 0);
+                q->auxiliary_for->n_auxiliary_queries--;
+                LIST_REMOVE(auxiliary_queries, q->auxiliary_for->auxiliary_queries, q);
+        }
+
+        dns_query_unlink_candidates(q);
+
+        dns_question_unref(q->question_idna);
+        dns_question_unref(q->question_utf8);
+        dns_packet_unref(q->question_bypass);
+
+        dns_query_reset_answer(q);
+
+        sd_bus_message_unref(q->bus_request);
+        sd_bus_track_unref(q->bus_track);
+
+        if (q->varlink_request) {
+                varlink_set_userdata(q->varlink_request, NULL);
+                varlink_unref(q->varlink_request);
+        }
+
+        if (q->request_packet)
+                hashmap_remove_value(q->stub_listener_extra ?
+                                     q->stub_listener_extra->queries_by_packet :
+                                     q->manager->stub_queries_by_packet,
+                                     q->request_packet,
+                                     q);
+
+        dns_packet_unref(q->request_packet);
+        dns_answer_unref(q->reply_answer);
+        dns_answer_unref(q->reply_authoritative);
+        dns_answer_unref(q->reply_additional);
+
+        if (q->request_stream) {
+                /* Detach the stream from our query, in case something else keeps a reference to it. */
+                (void) set_remove(q->request_stream->queries, q);
+                q->request_stream = dns_stream_unref(q->request_stream);
+        }
+
+        free(q->request_address_string);
+
+        if (q->manager) {
+                LIST_REMOVE(queries, q->manager->dns_queries, q);
+                q->manager->n_dns_queries--;
+        }
+
+        return mfree(q);
+}
+
+int dns_query_new(
+                Manager *m,
+                DnsQuery **ret,
+                DnsQuestion *question_utf8,
+                DnsQuestion *question_idna,
+                DnsPacket *question_bypass,
+                int ifindex,
+                uint64_t flags) {
+
+        _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+        DnsResourceKey *key;
+        int r;
+
+        assert(m);
+
+        if (question_bypass) {
+                /* It's either a "bypass" query, or a regular one, but can't be both. */
+                if (question_utf8 || question_idna)
+                        return -EINVAL;
+
+        } else {
+                bool good = false;
+
+                /* This (primarily) checks two things:
+                 *
+                 * 1. That the question is not empty
+                 * 2. That all RR keys in the question objects are for the same domain
+                 *
+                 * Or in other words, a single DnsQuery object may be used to look up A+AAAA combination for
+                 * the same domain name, or SRV+TXT (for DNS-SD services), but not for unrelated lookups. */
+
+                if (dns_question_size(question_utf8) > 0) {
+                        r = dns_question_is_valid_for_query(question_utf8);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                return -EINVAL;
+
+                        good = true;
+                }
+
+                /* If the IDNA and UTF8 questions are the same, merge their references */
+                r = dns_question_is_equal(question_idna, question_utf8);
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        question_idna = question_utf8;
+                else {
+                        if (dns_question_size(question_idna) > 0) {
+                                r = dns_question_is_valid_for_query(question_idna);
+                                if (r < 0)
+                                        return r;
+                                if (r == 0)
+                                        return -EINVAL;
+
+                                good = true;
+                        }
+                }
+
+                if (!good) /* don't allow empty queries */
+                        return -EINVAL;
+        }
+
+        if (m->n_dns_queries >= QUERIES_MAX)
+                return -EBUSY;
+
+        q = new(DnsQuery, 1);
+        if (!q)
+                return -ENOMEM;
+
+        *q = (DnsQuery) {
+                .question_utf8 = dns_question_ref(question_utf8),
+                .question_idna = dns_question_ref(question_idna),
+                .question_bypass = dns_packet_ref(question_bypass),
+                .ifindex = ifindex,
+                .flags = flags,
+                .answer_dnssec_result = _DNSSEC_RESULT_INVALID,
+                .answer_protocol = _DNS_PROTOCOL_INVALID,
+                .answer_family = AF_UNSPEC,
+        };
+
+        if (question_bypass) {
+                DNS_QUESTION_FOREACH(key, question_bypass->question)
+                        log_debug("Looking up bypass packet for %s.",
+                                  dns_resource_key_to_string(key, key_str, sizeof key_str));
+        } else {
+                /* First dump UTF8 question */
+                DNS_QUESTION_FOREACH(key, question_utf8)
+                        log_debug("Looking up RR for %s.",
+                                  dns_resource_key_to_string(key, key_str, sizeof key_str));
+
+                /* And then dump the IDNA question, but only what hasn't been dumped already through the UTF8 question. */
+                DNS_QUESTION_FOREACH(key, question_idna) {
+                        r = dns_question_contains_key(question_utf8, key);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                continue;
+
+                        log_debug("Looking up IDNA RR for %s.",
+                                  dns_resource_key_to_string(key, key_str, sizeof key_str));
+                }
+        }
+
+        LIST_PREPEND(queries, m->dns_queries, q);
+        m->n_dns_queries++;
+        q->manager = m;
+
+        if (ret)
+                *ret = q;
+
+        TAKE_PTR(q);
+        return 0;
+}
+
+int dns_query_make_auxiliary(DnsQuery *q, DnsQuery *auxiliary_for) {
+        assert(q);
+        assert(auxiliary_for);
+
+        /* Ensure that the query is not auxiliary yet, and
+         * nothing else is auxiliary to it either */
+        assert(!q->auxiliary_for);
+        assert(!q->auxiliary_queries);
+
+        /* Ensure that the unit we shall be made auxiliary for isn't
+         * auxiliary itself */
+        assert(!auxiliary_for->auxiliary_for);
+
+        if (auxiliary_for->n_auxiliary_queries >= AUXILIARY_QUERIES_MAX)
+                return -EAGAIN;
+
+        LIST_PREPEND(auxiliary_queries, auxiliary_for->auxiliary_queries, q);
+        q->auxiliary_for = auxiliary_for;
+
+        auxiliary_for->n_auxiliary_queries++;
+        return 0;
+}
+
+void dns_query_complete(DnsQuery *q, DnsTransactionState state) {
+        assert(q);
+        assert(!DNS_TRANSACTION_IS_LIVE(state));
+        assert(DNS_TRANSACTION_IS_LIVE(q->state));
+
+        /* Note that this call might invalidate the query. Callers should hence not attempt to access the
+         * query or transaction after calling this function. */
+
+        q->state = state;
+
+        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;
+
+        if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE))
+                return 0;
+
+        r = dns_synthesize_answer(
+                        q->manager,
+                        q->question_bypass ? q->question_bypass->question : q->question_utf8,
+                        q->ifindex,
+                        &answer);
+        if (r == -ENXIO) {
+                /* If we get ENXIO this tells us to generate NXDOMAIN unconditionally. */
+
+                dns_query_reset_answer(q);
+                q->answer_rcode = DNS_RCODE_NXDOMAIN;
+                q->answer_protocol = dns_synthesize_protocol(q->flags);
+                q->answer_family = dns_synthesize_family(q->flags);
+                q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
+                *state = DNS_TRANSACTION_RCODE_FAILURE;
+
+                return 0;
+        }
+        if (r <= 0)
+                return r;
+
+        dns_query_reset_answer(q);
+
+        q->answer = TAKE_PTR(answer);
+        q->answer_rcode = DNS_RCODE_SUCCESS;
+        q->answer_protocol = dns_synthesize_protocol(q->flags);
+        q->answer_family = dns_synthesize_family(q->flags);
+        q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
+
+        *state = DNS_TRANSACTION_SUCCESS;
+
+        return 1;
+}
+
+static int dns_query_try_etc_hosts(DnsQuery *q) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        int r;
+
+        assert(q);
+
+        /* Looks in /etc/hosts for matching entries. Note that this is done *before* the normal lookup is
+         * done. The data from /etc/hosts hence takes precedence over the network. */
+
+        if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE))
+                return 0;
+
+        r = manager_etc_hosts_lookup(
+                        q->manager,
+                        q->question_bypass ? q->question_bypass->question : q->question_utf8,
+                        &answer);
+        if (r <= 0)
+                return r;
+
+        dns_query_reset_answer(q);
+
+        q->answer = TAKE_PTR(answer);
+        q->answer_rcode = DNS_RCODE_SUCCESS;
+        q->answer_protocol = dns_synthesize_protocol(q->flags);
+        q->answer_family = dns_synthesize_family(q->flags);
+        q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
+
+        return 1;
+}
+
+int dns_query_go(DnsQuery *q) {
+        DnsScopeMatch found = DNS_SCOPE_NO;
+        DnsScope *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;
+
+                match = dns_scope_good_domain(s, q);
+                assert(match >= 0);
+                if (match > found) { /* Does this match better? If so, remember how well it matched, and the first one
+                                      * that matches this well */
+                        found = match;
+                        first = s;
+                }
+        }
+
+        if (found == DNS_SCOPE_NO) {
+                DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS;
+
+                r = dns_query_synthesize_reply(q, &state);
+                if (r < 0)
+                        return r;
+
+                dns_query_complete(q, state);
+                return 1;
+        }
+
+        r = dns_query_add_candidate(q, first);
+        if (r < 0)
+                goto fail;
+
+        LIST_FOREACH(scopes, s, first->scopes_next) {
+                DnsScopeMatch match;
+
+                match = dns_scope_good_domain(s, q);
+                assert(match >= 0);
+                if (match < found)
+                        continue;
+
+                r = dns_query_add_candidate(q, s);
+                if (r < 0)
+                        goto fail;
+        }
+
+        dns_query_reset_answer(q);
+
+        r = 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, has_confidential = false, has_non_confidential = false;
+        DnssecResult dnssec_result_authenticated = _DNSSEC_RESULT_INVALID, dnssec_result_non_authenticated = _DNSSEC_RESULT_INVALID;
+        DnsTransaction *t;
+        int r;
+
+        assert(q);
+
+        if (!c) {
+                r = dns_query_synthesize_reply(q, &state);
+                if (r < 0)
+                        goto fail;
+
+                dns_query_complete(q, state);
+                return;
+        }
+
+        if (c->error_code != 0) {
+                /* If the candidate had an error condition of its own, start with that. */
+                state = DNS_TRANSACTION_ERRNO;
+                q->answer = dns_answer_unref(q->answer);
+                q->answer_rcode = 0;
+                q->answer_dnssec_result = _DNSSEC_RESULT_INVALID;
+                q->answer_query_flags = 0;
+                q->answer_errno = c->error_code;
+                q->answer_full_packet = dns_packet_unref(q->answer_full_packet);
+        }
+
+        SET_FOREACH(t, c->transactions) {
+
+                switch (t->state) {
+
+                case DNS_TRANSACTION_SUCCESS: {
+                        /* We found a successful reply, merge it into the answer */
+
+                        if (state == DNS_TRANSACTION_SUCCESS) {
+                                r = dns_answer_extend(&q->answer, t->answer);
+                                if (r < 0)
+                                        goto fail;
+
+                                q->answer_query_flags |= dns_transaction_source_to_query_flags(t->answer_source);
+                        } else {
+                                /* Override non-successful previous answers */
+                                dns_answer_unref(q->answer);
+                                q->answer = dns_answer_ref(t->answer);
+
+                                q->answer_query_flags = dns_transaction_source_to_query_flags(t->answer_source);
+                        }
+
+                        q->answer_rcode = t->answer_rcode;
+                        q->answer_errno = 0;
+
+                        dns_packet_unref(q->answer_full_packet);
+                        q->answer_full_packet = dns_packet_ref(t->received);
+
+                        if (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) {
+                                has_authenticated = true;
+                                dnssec_result_authenticated = t->answer_dnssec_result;
+                        } else {
+                                has_non_authenticated = true;
+                                dnssec_result_non_authenticated = t->answer_dnssec_result;
+                        }
+
+                        if (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL))
+                                has_confidential = true;
+                        else
+                                has_non_confidential = true;
+
+                        state = DNS_TRANSACTION_SUCCESS;
+                        break;
+                }
+
+                case DNS_TRANSACTION_NULL:
+                case DNS_TRANSACTION_PENDING:
+                case DNS_TRANSACTION_VALIDATING:
+                case DNS_TRANSACTION_ABORTED:
+                        /* Ignore transactions that didn't complete */
+                        continue;
+
+                default:
+                        /* Any kind of failure? Store the data away, if there's nothing stored yet. */
+                        if (state == DNS_TRANSACTION_SUCCESS)
+                                continue;
+
+                        /* If there's already an authenticated negative reply stored, then prefer that over any unauthenticated one */
+                        if (FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) &&
+                            !FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
+                                continue;
+
+                        dns_answer_unref(q->answer);
+                        q->answer = dns_answer_ref(t->answer);
+                        q->answer_rcode = t->answer_rcode;
+                        q->answer_dnssec_result = t->answer_dnssec_result;
+                        q->answer_query_flags = t->answer_query_flags | dns_transaction_source_to_query_flags(t->answer_source);
+                        q->answer_errno = t->answer_errno;
+                        dns_packet_unref(q->answer_full_packet);
+                        q->answer_full_packet = dns_packet_ref(t->received);
+
+                        state = t->state;
+                        break;
+                }
+        }
+
+        if (state == DNS_TRANSACTION_SUCCESS) {
+                SET_FLAG(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED, has_authenticated && !has_non_authenticated);
+                SET_FLAG(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL, has_confidential && !has_non_confidential);
+                q->answer_dnssec_result = FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) ? dnssec_result_authenticated : dnssec_result_non_authenticated;
+        }
+
+        q->answer_protocol = c->scope->protocol;
+        q->answer_family = c->scope->family;
+
+        dns_search_domain_unref(q->answer_search_domain);
+        q->answer_search_domain = dns_search_domain_ref(c->search_domain);
+
+        r = dns_query_synthesize_reply(q, &state);
+        if (r < 0)
+                goto fail;
+
+        dns_query_complete(q, state);
+        return;
+
+fail:
+        q->answer_errno = -r;
+        dns_query_complete(q, DNS_TRANSACTION_ERRNO);
+}
+
+void dns_query_ready(DnsQuery *q) {
+
+        DnsQueryCandidate *bad = NULL, *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);
+
+        if (q->n_cname_redirects >= CNAME_REDIRECTS_MAX)
+                return -ELOOP;
+        q->n_cname_redirects++;
+
+        r = dns_question_cname_redirect(q->question_idna, cname, &nq_idna);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                log_debug("Following CNAME/DNAME %s → %s.", 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 k;
+        if (k > 0) {
+                /* Same question? Shortcut new question generation */
+                nq_utf8 = dns_question_ref(nq_idna);
+                k = r;
+        } else {
+                k = dns_question_cname_redirect(q->question_utf8, cname, &nq_utf8);
+                if (k < 0)
+                        return k;
+                if (k > 0)
+                        log_debug("Following UTF8 CNAME/DNAME %s → %s.", 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_unlink_candidates(q);
+
+        /* Note that we do *not* reset the answer here, because the answer we previously got might already
+         * include everything we need, let's check that first */
+
+        q->state = DNS_TRANSACTION_NULL;
+
+        return 0;
+}
+
+int dns_query_process_cname_one(DnsQuery *q) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *cname = NULL;
+        DnsQuestion *question;
+        DnsResourceRecord *rr;
+        bool full_match = true;
+        DnsResourceKey *k;
+        int r;
+
+        assert(q);
+
+        /* Processes a CNAME redirect if there's one. Returns one of three values:
+         *
+         * CNAME_QUERY_MATCH   → direct RR match, caller should just use the RRs in this answer (and not
+         *                       bother with any CNAME/DNAME stuff)
+         *
+         * CNAME_QUERY_NOMATCH → no match at all, neither direct nor CNAME/DNAME, caller might decide to
+         *                       restart query or take things as NODATA reply.
+         *
+         * CNAME_QUERY_CNAME   → no direct RR match, but a CNAME/DNAME match that we now followed for one step.
+         *
+         * The function might also return a failure, in particular -ELOOP if we encountered too many
+         * CNAMEs/DNAMEs in a chain or if following CNAMEs/DNAMEs was turned off.
+         *
+         * Note that this function doesn't actually restart the query. The caller can decide to do that in
+         * case of CNAME_QUERY_CNAME, though. */
+
+        if (!IN_SET(q->state, DNS_TRANSACTION_SUCCESS, DNS_TRANSACTION_NULL))
+                return DNS_QUERY_NOMATCH;
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        /* Small reminder: our question will consist of one or more RR keys that match in name, but not in
+         * record type. Specifically, when we do an address lookup the question will typically consist of one
+         * A and one AAAA key lookup for the same domain name. When we get a response from a server we need
+         * to check if the answer answers all our questions to use it. Note that a response of CNAME/DNAME
+         * can answer both an A and the AAAA question for us, but an A/AAAA response only the relevant
+         * type.
+         *
+         * Hence we first check of the answers we collected are sufficient to answer all our questions
+         * directly. If one question wasn't answered we go on, waiting for more replies. However, if there's
+         * a CNAME/DNAME response we use it, and redirect to it, regardless if it was a response to the A or
+         * the AAAA query. */
+
+        DNS_QUESTION_FOREACH(k, question) {
+                bool match = false;
+
+                DNS_ANSWER_FOREACH(rr, q->answer) {
+                        r = dns_resource_key_match_rr(k, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                match = true; /* Yay, we found an RR that matches the key we are looking for */
+                                break;
+                        }
+                }
+
+                if (!match) {
+                        /* Hmm. :-( there's no response for this key. This doesn't match. */
+                        full_match = false;
+                        break;
+                }
+        }
+
+        if (full_match)
+                return DNS_QUERY_MATCH; /* The answer can answer our question in full, no need to follow CNAMEs/DNAMEs */
+
+        /* Let's see if there is a CNAME/DNAME to match. This case is simpler: we accept the CNAME/DNAME that
+         * matches any of our questions. */
+        DNS_ANSWER_FOREACH(rr, q->answer) {
+                r = dns_question_matches_cname_or_dname(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
+                if (r < 0)
+                        return r;
+                if (r > 0 && !cname)
+                        cname = dns_resource_record_ref(rr);
+        }
+
+        if (!cname)
+                return DNS_QUERY_NOMATCH; /* No match and no CNAME/DNAME to follow */
+
+        if (q->flags & SD_RESOLVED_NO_CNAME)
+                return -ELOOP;
+
+        if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
+                q->previous_redirect_unauthenticated = true;
+        if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL))
+                q->previous_redirect_non_confidential = true;
+        if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_SYNTHETIC))
+                q->previous_redirect_non_synthetic = true;
+
+        /* OK, let's actually follow the CNAME */
+        r = dns_query_cname_redirect(q, cname);
+        if (r < 0)
+                return r;
+
+        return DNS_QUERY_CNAME; /* Tell caller that we did a single CNAME/DNAME redirection step */
+}
+
+int dns_query_process_cname_many(DnsQuery *q) {
+        int r;
+
+        assert(q);
+
+        /* Follows CNAMEs through the current packet: as long as the current packet can fulfill our
+         * redirected CNAME queries we keep going, and restart the query once the current packet isn't good
+         * enough anymore. It's a wrapper around dns_query_process_cname_one() and returns the same values,
+         * but with extended semantics. Specifically:
+         *
+         * DNS_QUERY_MATCH   → as above
+         *
+         * DNS_QUERY_CNAME   → we ran into a CNAME/DNAME redirect that we could not answer from the current
+         *                     message, and thus restarted the query to resolve it.
+         *
+         * DNS_QUERY_NOMATCH → we reached the end of CNAME/DNAME chain, and there are no direct matches nor a
+         *                     CNAME/DNAME match. i.e. this is a NODATA case.
+         *
+         * Note that this function will restart the query for the caller if needed, and that's the case
+         * DNS_QUERY_CNAME is returned.
+         */
+
+        r = dns_query_process_cname_one(q);
+        if (r != DNS_QUERY_CNAME)
+                return r; /* The first redirect is special: if it doesn't answer the question that's no
+                           * reason to restart the query, we just accept this as a NODATA answer. */
+
+        for (;;) {
+                r = dns_query_process_cname_one(q);
+                if (r < 0 || r == DNS_QUERY_MATCH)
+                        return r;
+                if (r == DNS_QUERY_NOMATCH) {
+                        /* OK, so we followed one or more CNAME/DNAME RR but the existing packet can't answer
+                         * this. Let's restart the query hence, with the new question. Why the different
+                         * handling than the first chain element? Because if the server answers a direct
+                         * question with an empty answer then this is a NODATA response. But if it responds
+                         * with a CNAME chain that ultimately is incomplete (i.e. a non-empty but truncated
+                         * CNAME chain) then we better follow up ourselves and ask for the rest of the
+                         * chain. This is particular relevant since our cache will store CNAME/DNAME
+                         * redirects that we learnt about for lookups of certain DNS types, but later on we
+                         * can reuse this data even for other DNS types, but in that case need to follow up
+                         * with the final lookup of the chain ourselves with the RR type we ourselves are
+                         * interested in. */
+                        r = dns_query_go(q);
+                        if (r < 0)
+                                return r;
+
+                        return DNS_QUERY_CNAME;
+                }
+
+                /* So we found a CNAME that the existing packet already answers, again via a CNAME, let's
+                 * continue going then. */
+                assert(r == DNS_QUERY_CNAME);
+        }
+}
+
+DnsQuestion* dns_query_question_for_protocol(DnsQuery *q, DnsProtocol protocol) {
+        assert(q);
+
+        if (q->question_bypass)
+                return q->question_bypass->question;
+
+        switch (protocol) {
+
+        case DNS_PROTOCOL_DNS:
+                return q->question_idna;
+
+        case DNS_PROTOCOL_MDNS:
+        case DNS_PROTOCOL_LLMNR:
+                return q->question_utf8;
+
+        default:
+                return NULL;
+        }
+}
+
+const char *dns_query_string(DnsQuery *q) {
+        const char *name;
+        int r;
+
+        /* Returns a somewhat useful human-readable lookup key string for this query */
+
+        if (q->question_bypass)
+                return dns_question_first_name(q->question_bypass->question);
+
+        if (q->request_address_string)
+                return q->request_address_string;
+
+        if (q->request_address_valid) {
+                r = in_addr_to_string(q->request_family, &q->request_address, &q->request_address_string);
+                if (r >= 0)
+                        return q->request_address_string;
+        }
+
+        name = dns_question_first_name(q->question_utf8);
+        if (name)
+                return name;
+
+        return dns_question_first_name(q->question_idna);
+}
+
+bool dns_query_fully_authenticated(DnsQuery *q) {
+        assert(q);
+
+        return FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) && !q->previous_redirect_unauthenticated;
+}
+
+bool dns_query_fully_confidential(DnsQuery *q) {
+        assert(q);
+
+        return FLAGS_SET(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL) && !q->previous_redirect_non_confidential;
+}
+
+bool dns_query_fully_authoritative(DnsQuery *q) {
+        assert(q);
+
+        /* We are authoritative for everything synthetic (except if a previous CNAME/DNAME) wasn't
+         * synthetic. (Note: SD_RESOLVED_SYNTHETIC is reset on each CNAME/DNAME, hence the explicit check for
+         * previous synthetic DNAME/CNAME redirections.) */
+        if ((q->answer_query_flags & SD_RESOLVED_SYNTHETIC) && !q->previous_redirect_non_synthetic)
+                return true;
+
+        /* We are also authoritative for everything coming only from the trust anchor and the local
+         * zones. (Note: the SD_RESOLVED_FROM_xyz flags we merge on each redirect, hence no need to
+         * explicitly check previous redirects here.) */
+        return (q->answer_query_flags & SD_RESOLVED_FROM_MASK & ~(SD_RESOLVED_FROM_TRUST_ANCHOR | SD_RESOLVED_FROM_ZONE)) == 0;
+}
diff --git a/src/resolve/resolved-dns-query.h b/src/resolve/resolved-dns-query.h
new file mode 100644
index 0000000..43a833a
--- /dev/null
+++ b/src/resolve/resolved-dns-query.h
@@ -0,0 +1,161 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-bus.h"
+
+#include "set.h"
+#include "varlink.h"
+
+typedef struct DnsQueryCandidate DnsQueryCandidate;
+typedef struct DnsQuery DnsQuery;
+typedef struct DnsStubListenerExtra DnsStubListenerExtra;
+
+#include "resolved-dns-answer.h"
+#include "resolved-dns-question.h"
+#include "resolved-dns-search-domain.h"
+#include "resolved-dns-transaction.h"
+
+struct DnsQueryCandidate {
+        unsigned n_ref;
+        int error_code;
+
+        DnsQuery *query;
+        DnsScope *scope;
+
+        DnsSearchDomain *search_domain;
+
+        Set *transactions;
+
+        LIST_FIELDS(DnsQueryCandidate, candidates_by_query);
+        LIST_FIELDS(DnsQueryCandidate, candidates_by_scope);
+};
+
+struct DnsQuery {
+        Manager *manager;
+
+        /* The question, formatted in IDNA for use on classic DNS, and as UTF8 for use in LLMNR or mDNS. Note
+         * that even on classic DNS some labels might use UTF8 encoding. Specifically, DNS-SD service names
+         * (in contrast to their domain suffixes) use UTF-8 encoding even on DNS. Thus, the difference
+         * between these two fields is mostly relevant only for explicit *hostname* lookups as well as the
+         * domain suffixes of service lookups.
+         *
+         * Note that questions may consist of multiple RR keys at once, but they must be for the same domain
+         * name. This is used for A+AAAA and TXT+SRV lookups: we'll allocate a single DnsQuery object for
+         * them instead of two separate ones. That allows us minor optimizations with response handling:
+         * CNAME/DNAMEs of the first reply we get can already be used to follow the CNAME/DNAME chain for
+         * both, and we can take benefit of server replies that oftentimes put A responses into AAAA queries
+         * and vice versa (in the additional section). */
+        DnsQuestion *question_idna;
+        DnsQuestion *question_utf8;
+
+        /* If this is not a question by ourselves, but a "bypass" request, we propagate the original packet
+         * here, and use that instead. */
+        DnsPacket *question_bypass;
+
+        uint64_t flags;
+        int ifindex;
+
+        /* When resolving a service, we first create a TXT+SRV query, and then for the hostnames we discover
+         * auxiliary A+AAAA queries. This pointer always points from the auxiliary queries back to the
+         * TXT+SRV query. */
+        int auxiliary_result;
+        DnsQuery *auxiliary_for;
+        LIST_HEAD(DnsQuery, auxiliary_queries);
+
+        LIST_HEAD(DnsQueryCandidate, candidates);
+        sd_event_source *timeout_event_source;
+
+        /* Discovered data */
+        DnsAnswer *answer;
+        int answer_rcode;
+        DnssecResult answer_dnssec_result;
+        uint64_t answer_query_flags;
+        DnsProtocol answer_protocol;
+        int answer_family;
+        DnsPacket *answer_full_packet;
+        DnsSearchDomain *answer_search_domain;
+
+        DnsTransactionState state;
+        int answer_errno; /* if state is DNS_TRANSACTION_ERRNO */
+
+        unsigned block_ready;
+
+        uint8_t n_auxiliary_queries;
+        uint8_t n_cname_redirects;
+
+        bool previous_redirect_unauthenticated:1;
+        bool previous_redirect_non_confidential:1;
+        bool previous_redirect_non_synthetic:1;
+        bool request_address_valid:1;
+
+        /* Bus + Varlink client information */
+        sd_bus_message *bus_request;
+        Varlink *varlink_request;
+        int request_family;
+        union in_addr_union request_address;
+        unsigned block_all_complete;
+        char *request_address_string;
+
+        /* DNS stub information */
+        DnsPacket *request_packet;
+        DnsStream *request_stream;
+        DnsAnswer *reply_answer;
+        DnsAnswer *reply_authoritative;
+        DnsAnswer *reply_additional;
+        DnsStubListenerExtra *stub_listener_extra;
+
+        /* Completion callback */
+        void (*complete)(DnsQuery* q);
+
+        sd_bus_track *bus_track;
+
+        LIST_FIELDS(DnsQuery, queries);
+        LIST_FIELDS(DnsQuery, auxiliary_queries);
+
+        /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */
+};
+
+enum {
+        DNS_QUERY_MATCH,
+        DNS_QUERY_NOMATCH,
+        DNS_QUERY_CNAME,
+};
+
+DnsQueryCandidate* dns_query_candidate_ref(DnsQueryCandidate*);
+DnsQueryCandidate* dns_query_candidate_unref(DnsQueryCandidate*);
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQueryCandidate*, dns_query_candidate_unref);
+
+void dns_query_candidate_notify(DnsQueryCandidate *c);
+
+int dns_query_new(Manager *m, DnsQuery **q, DnsQuestion *question_utf8, DnsQuestion *question_idna, DnsPacket *question_bypass, int family, uint64_t flags);
+DnsQuery *dns_query_free(DnsQuery *q);
+
+int dns_query_make_auxiliary(DnsQuery *q, DnsQuery *auxiliary_for);
+
+int dns_query_go(DnsQuery *q);
+void dns_query_ready(DnsQuery *q);
+
+int dns_query_process_cname_one(DnsQuery *q);
+int dns_query_process_cname_many(DnsQuery *q);
+
+void dns_query_complete(DnsQuery *q, DnsTransactionState state);
+
+DnsQuestion* dns_query_question_for_protocol(DnsQuery *q, DnsProtocol protocol);
+
+const char *dns_query_string(DnsQuery *q);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuery*, dns_query_free);
+
+bool dns_query_fully_authenticated(DnsQuery *q);
+bool dns_query_fully_confidential(DnsQuery *q);
+bool dns_query_fully_authoritative(DnsQuery *q);
+
+static inline uint64_t dns_query_reply_flags_make(DnsQuery *q) {
+        assert(q);
+
+        return SD_RESOLVED_FLAGS_MAKE(q->answer_protocol,
+                                      q->answer_family,
+                                      dns_query_fully_authenticated(q),
+                                      dns_query_fully_confidential(q)) |
+                (q->answer_query_flags & (SD_RESOLVED_FROM_MASK|SD_RESOLVED_SYNTHETIC));
+}
diff --git a/src/resolve/resolved-dns-question.c b/src/resolve/resolved-dns-question.c
new file mode 100644
index 0000000..aefdaa0
--- /dev/null
+++ b/src/resolve/resolved-dns-question.c
@@ -0,0 +1,488 @@
+/* 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, items) + sizeof(DnsQuestionItem) * n);
+        if (!q)
+                return NULL;
+
+        q->n_ref = 1;
+        q->n_allocated = n;
+
+        return q;
+}
+
+static DnsQuestion *dns_question_free(DnsQuestion *q) {
+        DnsResourceKey *key;
+
+        assert(q);
+
+        DNS_QUESTION_FOREACH(key, q)
+                dns_resource_key_unref(key);
+
+        return mfree(q);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(DnsQuestion, dns_question, dns_question_free);
+
+int dns_question_add_raw(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags) {
+        /* Insert without checking for duplicates. */
+
+        assert(key);
+        assert(q);
+
+        if (q->n_keys >= q->n_allocated)
+                return -ENOSPC;
+
+        q->items[q->n_keys++] = (DnsQuestionItem) {
+                .key = dns_resource_key_ref(key),
+                .flags = flags,
+        };
+        return 0;
+}
+
+int dns_question_add(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags) {
+        DnsQuestionItem *item;
+        int r;
+
+        assert(key);
+
+        if (!q)
+                return -ENOSPC;
+
+
+        DNS_QUESTION_FOREACH_ITEM(item, q) {
+                r = dns_resource_key_equal(item->key, key);
+                if (r < 0)
+                        return r;
+                if (r > 0 && item->flags == flags)
+                        return 0;
+        }
+
+        return dns_question_add_raw(q, key, flags);
+}
+
+int dns_question_matches_rr(DnsQuestion *q, DnsResourceRecord *rr, const char *search_domain) {
+        DnsResourceKey *key;
+        int r;
+
+        assert(rr);
+
+        if (!q)
+                return 0;
+
+        DNS_QUESTION_FOREACH(key, q) {
+                r = dns_resource_key_match_rr(key, rr, search_domain);
+                if (r != 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int dns_question_matches_cname_or_dname(DnsQuestion *q, DnsResourceRecord *rr, const char *search_domain) {
+        DnsResourceKey *key;
+        int r;
+
+        assert(rr);
+
+        if (!q)
+                return 0;
+
+        if (!IN_SET(rr->key->type, DNS_TYPE_CNAME, DNS_TYPE_DNAME))
+                return 0;
+
+        DNS_QUESTION_FOREACH(key, q) {
+                /* For a {C,D}NAME record we can never find a matching {C,D}NAME record */
+                if (!dns_type_may_redirect(key->type))
+                        return 0;
+
+                r = dns_resource_key_match_cname_or_dname(key, rr->key, search_domain);
+                if (r != 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+int dns_question_is_valid_for_query(DnsQuestion *q) {
+        const char *name;
+        size_t i;
+        int r;
+
+        if (!q)
+                return 0;
+
+        if (q->n_keys <= 0)
+                return 0;
+
+        if (q->n_keys > 65535)
+                return 0;
+
+        name = dns_resource_key_name(q->items[0].key);
+        if (!name)
+                return 0;
+
+        /* Check that all keys in this question bear the same name */
+        for (i = 0; i < q->n_keys; i++) {
+                assert(q->items[i].key);
+
+                if (i > 0) {
+                        r = dns_name_equal(dns_resource_key_name(q->items[i].key), name);
+                        if (r <= 0)
+                                return r;
+                }
+
+                if (!dns_type_is_valid_query(q->items[i].key->type))
+                        return 0;
+        }
+
+        return 1;
+}
+
+int dns_question_contains_key(DnsQuestion *q, const DnsResourceKey *k) {
+        size_t j;
+        int r;
+
+        assert(k);
+
+        if (!q)
+                return 0;
+
+
+        for (j = 0; j < q->n_keys; j++) {
+                r = dns_resource_key_equal(q->items[j].key, k);
+                if (r != 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_question_contains_item(DnsQuestion *q, const DnsQuestionItem *i) {
+        DnsQuestionItem *item;
+        int r;
+
+        assert(i);
+
+        DNS_QUESTION_FOREACH_ITEM(item, q) {
+                if (item->flags != i->flags)
+                        continue;
+                r = dns_resource_key_equal(item->key, i->key);
+                if (r != 0)
+                        return r;
+        }
+
+        return false;
+}
+
+int dns_question_is_equal(DnsQuestion *a, DnsQuestion *b) {
+        DnsQuestionItem *item;
+        int r;
+
+        if (a == b)
+                return 1;
+
+        if (!a)
+                return !b || b->n_keys == 0;
+        if (!b)
+                return a->n_keys == 0;
+
+        /* Checks if all items in a are also contained b, and vice versa */
+
+        DNS_QUESTION_FOREACH_ITEM(item, a) {
+                r = dns_question_contains_item(b, item);
+                if (r <= 0)
+                        return r;
+        }
+        DNS_QUESTION_FOREACH_ITEM(item, b) {
+                r = dns_question_contains_item(a, item);
+                if (r <= 0)
+                        return r;
+        }
+
+        return 1;
+}
+
+int dns_question_cname_redirect(DnsQuestion *q, const DnsResourceRecord *cname, DnsQuestion **ret) {
+        _cleanup_(dns_question_unrefp) DnsQuestion *n = NULL;
+        DnsResourceKey *key;
+        bool same = true;
+        int r;
+
+        assert(cname);
+        assert(ret);
+        assert(IN_SET(cname->key->type, DNS_TYPE_CNAME, DNS_TYPE_DNAME));
+
+        if (dns_question_size(q) <= 0) {
+                *ret = NULL;
+                return 0;
+        }
+
+        DNS_QUESTION_FOREACH(key, q) {
+                _cleanup_free_ char *destination = NULL;
+                const char *d;
+
+                if (cname->key->type == DNS_TYPE_CNAME)
+                        d = cname->cname.name;
+                else {
+                        r = dns_name_change_suffix(dns_resource_key_name(key), dns_resource_key_name(cname->key), cname->dname.name, &destination);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        d = destination;
+                }
+
+                r = dns_name_equal(dns_resource_key_name(key), d);
+                if (r < 0)
+                        return r;
+
+                if (r == 0) {
+                        same = false;
+                        break;
+                }
+        }
+
+        /* Fully the same, indicate we didn't do a thing */
+        if (same) {
+                *ret = NULL;
+                return 0;
+        }
+
+        n = dns_question_new(q->n_keys);
+        if (!n)
+                return -ENOMEM;
+
+        /* Create a new question, and patch in the new name */
+        DNS_QUESTION_FOREACH(key, q) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *k = NULL;
+
+                k = dns_resource_key_new_redirect(key, cname);
+                if (!k)
+                        return -ENOMEM;
+
+                r = dns_question_add(n, k, 0);
+                if (r < 0)
+                        return r;
+        }
+
+        *ret = TAKE_PTR(n);
+
+        return 1;
+}
+
+const char *dns_question_first_name(DnsQuestion *q) {
+
+        if (!q)
+                return NULL;
+
+        if (q->n_keys < 1)
+                return NULL;
+
+        return dns_resource_key_name(q->items[0].key);
+}
+
+int dns_question_new_address(DnsQuestion **ret, int family, const char *name, bool convert_idna) {
+        _cleanup_(dns_question_unrefp) DnsQuestion *q = NULL;
+        _cleanup_free_ char *buf = NULL;
+        int r;
+
+        assert(ret);
+        assert(name);
+
+        if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC))
+                return -EAFNOSUPPORT;
+
+        if (convert_idna) {
+                r = dns_name_apply_idna(name, &buf);
+                if (r < 0)
+                        return r;
+                if (r > 0 && !streq(name, buf))
+                        name = buf;
+                else
+                        /* We did not manage to create convert the idna name, or it's
+                         * the same as the original name. We assume the caller already
+                         * created an unconverted question, so let's not repeat work
+                         * unnecessarily. */
+                        return -EALREADY;
+        }
+
+        q = dns_question_new(family == AF_UNSPEC ? 2 : 1);
+        if (!q)
+                return -ENOMEM;
+
+        if (family != AF_INET6) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+
+                key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, name);
+                if (!key)
+                        return -ENOMEM;
+
+                r = dns_question_add(q, key, 0);
+                if (r < 0)
+                        return r;
+        }
+
+        if (family != AF_INET) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+
+                key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_AAAA, name);
+                if (!key)
+                        return -ENOMEM;
+
+                r = dns_question_add(q, key, 0);
+                if (r < 0)
+                        return r;
+        }
+
+        *ret = TAKE_PTR(q);
+
+        return 0;
+}
+
+int dns_question_new_reverse(DnsQuestion **ret, int family, const union in_addr_union *a) {
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+        _cleanup_(dns_question_unrefp) DnsQuestion *q = NULL;
+        _cleanup_free_ char *reverse = NULL;
+        int r;
+
+        assert(ret);
+        assert(a);
+
+        if (!IN_SET(family, AF_INET, AF_INET6, AF_UNSPEC))
+                return -EAFNOSUPPORT;
+
+        r = dns_name_reverse(family, a, &reverse);
+        if (r < 0)
+                return r;
+
+        q = dns_question_new(1);
+        if (!q)
+                return -ENOMEM;
+
+        key = dns_resource_key_new_consume(DNS_CLASS_IN, DNS_TYPE_PTR, reverse);
+        if (!key)
+                return -ENOMEM;
+
+        reverse = NULL;
+
+        r = dns_question_add(q, key, 0);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(q);
+
+        return 0;
+}
+
+int dns_question_new_service(
+                DnsQuestion **ret,
+                const char *service,
+                const char *type,
+                const char *domain,
+                bool with_txt,
+                bool convert_idna) {
+
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+        _cleanup_(dns_question_unrefp) DnsQuestion *q = NULL;
+        _cleanup_free_ char *buf = NULL, *joined = NULL;
+        const char *name;
+        int r;
+
+        assert(ret);
+
+        /* We support three modes of invocation:
+         *
+         * 1. Only a domain is specified, in which case we assume a properly encoded SRV RR name, including service
+         *    type and possibly a service name. If specified in this way we assume it's already IDNA converted if
+         *    that's necessary.
+         *
+         * 2. Both service type and a domain specified, in which case a normal SRV RR is assumed, without a DNS-SD
+         *    style prefix. In this case we'll IDNA convert the domain, if that's requested.
+         *
+         * 3. All three of service name, type and domain are specified, in which case a DNS-SD service is put
+         *    together. The service name is never IDNA converted, and the domain is if requested.
+         *
+         * It's not supported to specify a service name without a type, or no domain name.
+         */
+
+        if (!domain)
+                return -EINVAL;
+
+        if (type) {
+                if (convert_idna) {
+                        r = dns_name_apply_idna(domain, &buf);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                domain = buf;
+                }
+
+                r = dns_service_join(service, type, domain, &joined);
+                if (r < 0)
+                        return r;
+
+                name = joined;
+        } else {
+                if (service)
+                        return -EINVAL;
+
+                name = domain;
+        }
+
+        q = dns_question_new(1 + with_txt);
+        if (!q)
+                return -ENOMEM;
+
+        key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_SRV, name);
+        if (!key)
+                return -ENOMEM;
+
+        r = dns_question_add(q, key, 0);
+        if (r < 0)
+                return r;
+
+        if (with_txt) {
+                dns_resource_key_unref(key);
+                key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_TXT, name);
+                if (!key)
+                        return -ENOMEM;
+
+                r = dns_question_add(q, key, 0);
+                if (r < 0)
+                        return r;
+        }
+
+        *ret = TAKE_PTR(q);
+
+        return 0;
+}
+
+/*
+ * This function is not used in the code base, but is useful when debugging. Do not delete.
+ */
+void dns_question_dump(DnsQuestion *question, FILE *f) {
+        DnsResourceKey *k;
+
+        if (!f)
+                f = stdout;
+
+        DNS_QUESTION_FOREACH(k, question) {
+                char buf[DNS_RESOURCE_KEY_STRING_MAX];
+
+                fputc('\t', f);
+                fputs(dns_resource_key_to_string(k, buf, sizeof(buf)), f);
+                fputc('\n', f);
+        }
+}
diff --git a/src/resolve/resolved-dns-question.h b/src/resolve/resolved-dns-question.h
new file mode 100644
index 0000000..31b8a2e
--- /dev/null
+++ b/src/resolve/resolved-dns-question.h
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+typedef struct DnsQuestion DnsQuestion;
+typedef struct DnsQuestionItem DnsQuestionItem;
+
+#include "macro.h"
+#include "resolved-dns-rr.h"
+
+/* A simple array of resource keys */
+
+typedef enum DnsQuestionFlags {
+        DNS_QUESTION_WANTS_UNICAST_REPLY = 1 << 0, /* For mDNS: sender is willing to accept unicast replies */
+} DnsQuestionFlags;
+
+struct DnsQuestionItem {
+        DnsResourceKey *key;
+        DnsQuestionFlags flags;
+};
+
+struct DnsQuestion {
+        unsigned n_ref;
+        size_t n_keys, n_allocated;
+        DnsQuestionItem items[0];
+};
+
+DnsQuestion *dns_question_new(size_t n);
+DnsQuestion *dns_question_ref(DnsQuestion *q);
+DnsQuestion *dns_question_unref(DnsQuestion *q);
+
+int dns_question_new_address(DnsQuestion **ret, int family, const char *name, bool convert_idna);
+int dns_question_new_reverse(DnsQuestion **ret, int family, const union in_addr_union *a);
+int dns_question_new_service(DnsQuestion **ret, const char *service, const char *type, const char *domain, bool with_txt, bool convert_idna);
+
+int dns_question_add_raw(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags);
+int dns_question_add(DnsQuestion *q, DnsResourceKey *key, DnsQuestionFlags flags);
+
+int dns_question_matches_rr(DnsQuestion *q, DnsResourceRecord *rr, const char *search_domain);
+int dns_question_matches_cname_or_dname(DnsQuestion *q, DnsResourceRecord *rr, const char* search_domain);
+int dns_question_is_valid_for_query(DnsQuestion *q);
+int dns_question_contains_key(DnsQuestion *q, const DnsResourceKey *k);
+int dns_question_is_equal(DnsQuestion *a, DnsQuestion *b);
+
+int dns_question_cname_redirect(DnsQuestion *q, const DnsResourceRecord *cname, DnsQuestion **ret);
+
+void dns_question_dump(DnsQuestion *q, FILE *f);
+
+const char *dns_question_first_name(DnsQuestion *q);
+
+static inline DnsResourceKey *dns_question_first_key(DnsQuestion *q) {
+        return (q && q->n_keys > 0) ? q->items[0].key : NULL;
+}
+
+static inline size_t dns_question_size(DnsQuestion *q) {
+        return q ? q->n_keys : 0;
+}
+
+static inline bool dns_question_isempty(DnsQuestion *q) {
+        return dns_question_size(q) <= 0;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuestion*, dns_question_unref);
+
+#define _DNS_QUESTION_FOREACH(u, k, q)                                     \
+        for (size_t UNIQ_T(i, u) = ({                                      \
+                                (k) = ((q) && (q)->n_keys > 0) ? (q)->items[0].key : NULL; \
+                                0;                                         \
+                        });                                                \
+             (q) && (UNIQ_T(i, u) < (q)->n_keys);                          \
+             UNIQ_T(i, u)++, (k) = (UNIQ_T(i, u) < (q)->n_keys ? (q)->items[UNIQ_T(i, u)].key : NULL))
+
+#define DNS_QUESTION_FOREACH(key, q) _DNS_QUESTION_FOREACH(UNIQ, key, q)
+
+#define _DNS_QUESTION_FOREACH_ITEM(u, item, q)                             \
+        for (size_t UNIQ_T(i, u) = ({                                      \
+                     (item) = dns_question_isempty(q) ? NULL : (q)->items; \
+                     0;                                                    \
+             });                                                           \
+             UNIQ_T(i, u) < dns_question_size(q);                          \
+             UNIQ_T(i, u)++, (item) = (UNIQ_T(i, u) < dns_question_size(q) ? (q)->items + UNIQ_T(i, u) : NULL))
+
+#define DNS_QUESTION_FOREACH_ITEM(item, q) _DNS_QUESTION_FOREACH_ITEM(UNIQ, item, q)
diff --git a/src/resolve/resolved-dns-rr.c b/src/resolve/resolved-dns-rr.c
new file mode 100644
index 0000000..3fa59e2
--- /dev/null
+++ b/src/resolve/resolved-dns-rr.c
@@ -0,0 +1,1878 @@
+/* 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 != UINT_MAX);
+
+        assert(k->n_ref > 0);
+        k->n_ref++;
+
+        return k;
+}
+
+DnsResourceKey* dns_resource_key_unref(DnsResourceKey *k) {
+        if (!k)
+                return NULL;
+
+        assert(k->n_ref != UINT_MAX);
+        assert(k->n_ref > 0);
+
+        if (k->n_ref == 1) {
+                free(k->_name);
+                free(k);
+        } else
+                k->n_ref--;
+
+        return NULL;
+}
+
+const char* dns_resource_key_name(const DnsResourceKey *key) {
+        const char *name;
+
+        if (!key)
+                return NULL;
+
+        if (key->_name)
+                name = key->_name;
+        else
+                name = (char*) key + sizeof(DnsResourceKey);
+
+        if (dns_name_is_root(name))
+                return ".";
+        else
+                return name;
+}
+
+bool dns_resource_key_is_address(const DnsResourceKey *key) {
+        assert(key);
+
+        /* Check if this is an A or AAAA resource key */
+
+        return key->class == DNS_CLASS_IN && IN_SET(key->type, DNS_TYPE_A, DNS_TYPE_AAAA);
+}
+
+bool dns_resource_key_is_dnssd_ptr(const DnsResourceKey *key) {
+        assert(key);
+
+        /* Check if this is a PTR resource key used in
+           Service Instance Enumeration as described in RFC6763 p4.1. */
+
+        if (key->type != DNS_TYPE_PTR)
+                return false;
+
+        return dns_name_endswith(dns_resource_key_name(key), "_tcp.local") ||
+                dns_name_endswith(dns_resource_key_name(key), "_udp.local");
+}
+
+int dns_resource_key_equal(const DnsResourceKey *a, const DnsResourceKey *b) {
+        int r;
+
+        if (a == b)
+                return 1;
+
+        r = dns_name_equal(dns_resource_key_name(a), dns_resource_key_name(b));
+        if (r <= 0)
+                return r;
+
+        if (a->class != b->class)
+                return 0;
+
+        if (a->type != b->type)
+                return 0;
+
+        return 1;
+}
+
+int dns_resource_key_match_rr(const DnsResourceKey *key, DnsResourceRecord *rr, const char *search_domain) {
+        int r;
+
+        assert(key);
+        assert(rr);
+
+        if (key == rr->key)
+                return 1;
+
+        /* Checks if an rr matches the specified key. If a search
+         * domain is specified, it will also be checked if the key
+         * with the search domain suffixed might match the RR. */
+
+        if (rr->key->class != key->class && key->class != DNS_CLASS_ANY)
+                return 0;
+
+        if (rr->key->type != key->type && key->type != DNS_TYPE_ANY)
+                return 0;
+
+        r = dns_name_equal(dns_resource_key_name(rr->key), dns_resource_key_name(key));
+        if (r != 0)
+                return r;
+
+        if (search_domain) {
+                _cleanup_free_ char *joined = NULL;
+
+                r = dns_name_concat(dns_resource_key_name(key), search_domain, 0, &joined);
+                if (r < 0)
+                        return r;
+
+                return dns_name_equal(dns_resource_key_name(rr->key), joined);
+        }
+
+        return 0;
+}
+
+int dns_resource_key_match_cname_or_dname(const DnsResourceKey *key, const DnsResourceKey *cname, const char *search_domain) {
+        int r;
+
+        assert(key);
+        assert(cname);
+
+        if (cname->class != key->class && key->class != DNS_CLASS_ANY)
+                return 0;
+
+        if (!dns_type_may_redirect(key->type))
+                return 0;
+
+        if (cname->type == DNS_TYPE_CNAME)
+                r = dns_name_equal(dns_resource_key_name(key), dns_resource_key_name(cname));
+        else if (cname->type == DNS_TYPE_DNAME)
+                r = dns_name_endswith(dns_resource_key_name(key), dns_resource_key_name(cname));
+        else
+                return 0;
+
+        if (r != 0)
+                return r;
+
+        if (search_domain) {
+                _cleanup_free_ char *joined = NULL;
+
+                r = dns_name_concat(dns_resource_key_name(key), search_domain, 0, &joined);
+                if (r < 0)
+                        return r;
+
+                if (cname->type == DNS_TYPE_CNAME)
+                        return dns_name_equal(joined, dns_resource_key_name(cname));
+                else if (cname->type == DNS_TYPE_DNAME)
+                        return dns_name_endswith(joined, dns_resource_key_name(cname));
+        }
+
+        return 0;
+}
+
+int dns_resource_key_match_soa(const DnsResourceKey *key, const DnsResourceKey *soa) {
+        assert(soa);
+        assert(key);
+
+        /* Checks whether 'soa' is a SOA record for the specified key. */
+
+        if (soa->class != key->class)
+                return 0;
+
+        if (soa->type != DNS_TYPE_SOA)
+                return 0;
+
+        return dns_name_endswith(dns_resource_key_name(key), dns_resource_key_name(soa));
+}
+
+static void dns_resource_key_hash_func(const DnsResourceKey *k, struct siphash *state) {
+        assert(k);
+
+        dns_name_hash_func(dns_resource_key_name(k), state);
+        siphash24_compress(&k->class, sizeof(k->class), state);
+        siphash24_compress(&k->type, sizeof(k->type), state);
+}
+
+static int dns_resource_key_compare_func(const DnsResourceKey *x, const DnsResourceKey *y) {
+        int r;
+
+        r = dns_name_compare_func(dns_resource_key_name(x), dns_resource_key_name(y));
+        if (r != 0)
+                return r;
+
+        r = CMP(x->type, y->type);
+        if (r != 0)
+                return r;
+
+        return CMP(x->class, y->class);
+}
+
+DEFINE_HASH_OPS(dns_resource_key_hash_ops, DnsResourceKey, dns_resource_key_hash_func, dns_resource_key_compare_func);
+
+char* dns_resource_key_to_string(const DnsResourceKey *key, char *buf, size_t buf_size) {
+        const char *c, *t;
+        char *ans = buf;
+
+        /* If we cannot convert the CLASS/TYPE into a known string,
+           use the format recommended by RFC 3597, Section 5. */
+
+        c = dns_class_to_string(key->class);
+        t = dns_type_to_string(key->type);
+
+        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 == UINT_MAX)
+                return false;
+        if ((*b)->n_ref == UINT_MAX)
+                return false;
+
+        /* Already the same? */
+        if (*a == *b)
+                return true;
+
+        /* Are they really identical? */
+        if (dns_resource_key_equal(*a, *b) <= 0)
+                return false;
+
+        /* Keep the one which already has more references. */
+        if ((*a)->n_ref > (*b)->n_ref) {
+                dns_resource_key_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 = UINT8_MAX,
+                .n_skip_labels_source = UINT8_MAX,
+        };
+
+        return rr;
+}
+
+DnsResourceRecord* dns_resource_record_new_full(uint16_t class, uint16_t type, const char *name) {
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+
+        key = dns_resource_key_new(class, type, name);
+        if (!key)
+                return NULL;
+
+        return dns_resource_record_new(key);
+}
+
+static DnsResourceRecord* dns_resource_record_free(DnsResourceRecord *rr) {
+        assert(rr);
+
+        if (rr->key) {
+                switch(rr->key->type) {
+
+                case DNS_TYPE_SRV:
+                        free(rr->srv.name);
+                        break;
+
+                case DNS_TYPE_PTR:
+                case DNS_TYPE_NS:
+                case DNS_TYPE_CNAME:
+                case DNS_TYPE_DNAME:
+                        free(rr->ptr.name);
+                        break;
+
+                case DNS_TYPE_HINFO:
+                        free(rr->hinfo.cpu);
+                        free(rr->hinfo.os);
+                        break;
+
+                case DNS_TYPE_TXT:
+                case DNS_TYPE_SPF:
+                        dns_txt_item_free_all(rr->txt.items);
+                        break;
+
+                case DNS_TYPE_SOA:
+                        free(rr->soa.mname);
+                        free(rr->soa.rname);
+                        break;
+
+                case DNS_TYPE_MX:
+                        free(rr->mx.exchange);
+                        break;
+
+                case DNS_TYPE_DS:
+                        free(rr->ds.digest);
+                        break;
+
+                case DNS_TYPE_SSHFP:
+                        free(rr->sshfp.fingerprint);
+                        break;
+
+                case DNS_TYPE_DNSKEY:
+                        free(rr->dnskey.key);
+                        break;
+
+                case DNS_TYPE_RRSIG:
+                        free(rr->rrsig.signer);
+                        free(rr->rrsig.signature);
+                        break;
+
+                case DNS_TYPE_NSEC:
+                        free(rr->nsec.next_domain_name);
+                        systemd_bitmap_free(rr->nsec.types);
+                        break;
+
+                case DNS_TYPE_NSEC3:
+                        free(rr->nsec3.next_hashed_name);
+                        free(rr->nsec3.salt);
+                        systemd_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) &&
+                       systemd_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) &&
+                       systemd_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) {
+                if (i != first)
+                        *(p++) = ' ';
+
+                *(p++) = '"';
+
+                for (size_t j = 0; j < i->length; j++) {
+                        if (i->data[j] < ' ' || i->data[j] == '"' || i->data[j] >= 127) {
+                                *(p++) = '\\';
+                                *(p++) = '0' + (i->data[j] / 100);
+                                *(p++) = '0' + ((i->data[j] / 10) % 10);
+                                *(p++) = '0' + (i->data[j] % 10);
+                        } else
+                                *(p++) = i->data[j];
+                }
+
+                *(p++) = '"';
+        }
+
+        *p = 0;
+        return s;
+}
+
+const char *dns_resource_record_to_string(DnsResourceRecord *rr) {
+        _cleanup_free_ char *s = NULL, *t = NULL;
+        char k[DNS_RESOURCE_KEY_STRING_MAX];
+        int r;
+
+        assert(rr);
+
+        if (rr->to_string)
+                return rr->to_string;
+
+        dns_resource_key_to_string(rr->key, k, sizeof(k));
+
+        switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) {
+
+        case DNS_TYPE_SRV:
+                r = asprintf(&s, "%s %u %u %u %s",
+                             k,
+                             rr->srv.priority,
+                             rr->srv.weight,
+                             rr->srv.port,
+                             strna(rr->srv.name));
+                if (r < 0)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_PTR:
+        case DNS_TYPE_NS:
+        case DNS_TYPE_CNAME:
+        case DNS_TYPE_DNAME:
+                s = strjoin(k, " ", rr->ptr.name);
+                if (!s)
+                        return NULL;
+
+                break;
+
+        case DNS_TYPE_HINFO:
+                s = strjoin(k, " ", rr->hinfo.cpu, " ", rr->hinfo.os);
+                if (!s)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_SPF: /* exactly the same as TXT */
+        case DNS_TYPE_TXT:
+                t = format_txt(rr->txt.items);
+                if (!t)
+                        return NULL;
+
+                s = strjoin(k, " ", t);
+                if (!s)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_A:
+                r = in_addr_to_string(AF_INET, (const union in_addr_union*) &rr->a.in_addr, &t);
+                if (r < 0)
+                        return NULL;
+
+                s = strjoin(k, " ", t);
+                if (!s)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_AAAA:
+                r = in_addr_to_string(AF_INET6, (const union in_addr_union*) &rr->aaaa.in6_addr, &t);
+                if (r < 0)
+                        return NULL;
+
+                s = strjoin(k, " ", t);
+                if (!s)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_SOA:
+                r = asprintf(&s, "%s %s %s %u %u %u %u %u",
+                             k,
+                             strna(rr->soa.mname),
+                             strna(rr->soa.rname),
+                             rr->soa.serial,
+                             rr->soa.refresh,
+                             rr->soa.retry,
+                             rr->soa.expire,
+                             rr->soa.minimum);
+                if (r < 0)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_MX:
+                r = asprintf(&s, "%s %u %s",
+                             k,
+                             rr->mx.priority,
+                             rr->mx.exchange);
+                if (r < 0)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_LOC:
+                assert(rr->loc.version == 0);
+
+                t = format_location(rr->loc.latitude,
+                                    rr->loc.longitude,
+                                    rr->loc.altitude,
+                                    rr->loc.size,
+                                    rr->loc.horiz_pre,
+                                    rr->loc.vert_pre);
+                if (!t)
+                        return NULL;
+
+                s = strjoin(k, " ", t);
+                if (!s)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_DS:
+                t = hexmem(rr->ds.digest, rr->ds.digest_size);
+                if (!t)
+                        return NULL;
+
+                r = asprintf(&s, "%s %u %u %u %s",
+                             k,
+                             rr->ds.key_tag,
+                             rr->ds.algorithm,
+                             rr->ds.digest_type,
+                             t);
+                if (r < 0)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_SSHFP:
+                t = hexmem(rr->sshfp.fingerprint, rr->sshfp.fingerprint_size);
+                if (!t)
+                        return NULL;
+
+                r = asprintf(&s, "%s %u %u %s",
+                             k,
+                             rr->sshfp.algorithm,
+                             rr->sshfp.fptype,
+                             t);
+                if (r < 0)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_DNSKEY: {
+                _cleanup_free_ char *alg = NULL;
+                uint16_t key_tag;
+
+                key_tag = dnssec_keytag(rr, true);
+
+                r = dnssec_algorithm_to_string_alloc(rr->dnskey.algorithm, &alg);
+                if (r < 0)
+                        return NULL;
+
+                r = asprintf(&t, "%s %u %u %s",
+                             k,
+                             rr->dnskey.flags,
+                             rr->dnskey.protocol,
+                             alg);
+                if (r < 0)
+                        return NULL;
+
+                r = base64_append(&t, r,
+                                  rr->dnskey.key, rr->dnskey.key_size,
+                                  8, columns());
+                if (r < 0)
+                        return NULL;
+
+                r = asprintf(&s, "%s\n"
+                             "        -- Flags:%s%s%s\n"
+                             "        -- Key tag: %u",
+                             t,
+                             rr->dnskey.flags & DNSKEY_FLAG_SEP ? " SEP" : "",
+                             rr->dnskey.flags & DNSKEY_FLAG_REVOKE ? " REVOKE" : "",
+                             rr->dnskey.flags & DNSKEY_FLAG_ZONE_KEY ? " ZONE_KEY" : "",
+                             key_tag);
+                if (r < 0)
+                        return NULL;
+
+                break;
+        }
+
+        case DNS_TYPE_RRSIG: {
+                _cleanup_free_ char *alg = NULL;
+                char expiration[STRLEN("YYYYMMDDHHmmSS") + 1], inception[STRLEN("YYYYMMDDHHmmSS") + 1];
+                const char *type;
+
+                type = dns_type_to_string(rr->rrsig.type_covered);
+
+                r = dnssec_algorithm_to_string_alloc(rr->rrsig.algorithm, &alg);
+                if (r < 0)
+                        return NULL;
+
+                r = format_timestamp_dns(expiration, sizeof(expiration), rr->rrsig.expiration);
+                if (r < 0)
+                        return NULL;
+
+                r = format_timestamp_dns(inception, sizeof(inception), rr->rrsig.inception);
+                if (r < 0)
+                        return NULL;
+
+                /* TYPE?? follows
+                 * http://tools.ietf.org/html/rfc3597#section-5 */
+
+                r = asprintf(&s, "%s %s%.*u %s %u %u %s %s %u %s",
+                             k,
+                             type ?: "TYPE",
+                             type ? 0 : 1, type ? 0u : (unsigned) rr->rrsig.type_covered,
+                             alg,
+                             rr->rrsig.labels,
+                             rr->rrsig.original_ttl,
+                             expiration,
+                             inception,
+                             rr->rrsig.key_tag,
+                             rr->rrsig.signer);
+                if (r < 0)
+                        return NULL;
+
+                r = base64_append(&s, r,
+                                  rr->rrsig.signature, rr->rrsig.signature_size,
+                                  8, columns());
+                if (r < 0)
+                        return NULL;
+
+                break;
+        }
+
+        case DNS_TYPE_NSEC:
+                t = format_types(rr->nsec.types);
+                if (!t)
+                        return NULL;
+
+                r = asprintf(&s, "%s %s %s",
+                             k,
+                             rr->nsec.next_domain_name,
+                             t);
+                if (r < 0)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_NSEC3: {
+                _cleanup_free_ char *salt = NULL, *hash = NULL;
+
+                if (rr->nsec3.salt_size > 0) {
+                        salt = hexmem(rr->nsec3.salt, rr->nsec3.salt_size);
+                        if (!salt)
+                                return NULL;
+                }
+
+                hash = base32hexmem(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size, false);
+                if (!hash)
+                        return NULL;
+
+                t = format_types(rr->nsec3.types);
+                if (!t)
+                        return NULL;
+
+                r = asprintf(&s, "%s %"PRIu8" %"PRIu8" %"PRIu16" %s %s %s",
+                             k,
+                             rr->nsec3.algorithm,
+                             rr->nsec3.flags,
+                             rr->nsec3.iterations,
+                             rr->nsec3.salt_size > 0 ? salt : "-",
+                             hash,
+                             t);
+                if (r < 0)
+                        return NULL;
+
+                break;
+        }
+
+        case DNS_TYPE_TLSA: {
+                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:
+                t = octescape(rr->caa.value, rr->caa.value_size);
+                if (!t)
+                        return NULL;
+
+                r = asprintf(&s, "%s %u %s \"%s\"%s%s%s%.0u",
+                             k,
+                             rr->caa.flags,
+                             rr->caa.tag,
+                             t,
+                             rr->caa.flags ? "\n        -- Flags:" : "",
+                             rr->caa.flags & CAA_FLAG_CRITICAL ? " critical" : "",
+                             rr->caa.flags & ~CAA_FLAG_CRITICAL ? " " : "",
+                             rr->caa.flags & ~CAA_FLAG_CRITICAL);
+                if (r < 0)
+                        return NULL;
+
+                break;
+
+        case DNS_TYPE_OPENPGPKEY:
+                r = asprintf(&s, "%s", k);
+                if (r < 0)
+                        return NULL;
+
+                r = base64_append(&s, r,
+                                  rr->generic.data, rr->generic.data_size,
+                                  8, columns());
+                if (r < 0)
+                        return NULL;
+                break;
+
+        default:
+                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 TAKE_PTR(s);
+}
+
+ssize_t dns_resource_record_payload(DnsResourceRecord *rr, void **out) {
+        assert(rr);
+        assert(out);
+
+        switch(rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) {
+        case DNS_TYPE_SRV:
+        case DNS_TYPE_PTR:
+        case DNS_TYPE_NS:
+        case DNS_TYPE_CNAME:
+        case DNS_TYPE_DNAME:
+        case DNS_TYPE_HINFO:
+        case DNS_TYPE_SPF:
+        case DNS_TYPE_TXT:
+        case DNS_TYPE_A:
+        case DNS_TYPE_AAAA:
+        case DNS_TYPE_SOA:
+        case DNS_TYPE_MX:
+        case DNS_TYPE_LOC:
+        case DNS_TYPE_DS:
+        case DNS_TYPE_DNSKEY:
+        case DNS_TYPE_RRSIG:
+        case DNS_TYPE_NSEC:
+        case DNS_TYPE_NSEC3:
+                return -EINVAL;
+
+        case DNS_TYPE_SSHFP:
+                *out = rr->sshfp.fingerprint;
+                return rr->sshfp.fingerprint_size;
+
+        case DNS_TYPE_TLSA:
+                *out = rr->tlsa.data;
+                return rr->tlsa.data_size;
+
+        case DNS_TYPE_OPENPGPKEY:
+        default:
+                *out = rr->generic.data;
+                return rr->generic.data_size;
+        }
+}
+
+int dns_resource_record_to_wire_format(DnsResourceRecord *rr, bool canonical) {
+
+        DnsPacket packet = {
+                .n_ref = 1,
+                .protocol = DNS_PROTOCOL_DNS,
+                .on_stack = true,
+                .refuse_compression = true,
+                .canonical_form = canonical,
+        };
+
+        size_t start, rds;
+        int r;
+
+        assert(rr);
+
+        /* Generates the RR in wire-format, optionally in the
+         * canonical form as discussed in the DNSSEC RFC 4034, Section
+         * 6.2. We allocate a throw-away DnsPacket object on the stack
+         * here, because we need some book-keeping for memory
+         * management, and can reuse the DnsPacket serializer, that
+         * can generate the canonical form, too, but also knows label
+         * compression and suchlike. */
+
+        if (rr->wire_format && rr->wire_format_canonical == canonical)
+                return 0;
+
+        r = dns_packet_append_rr(&packet, rr, 0, &start, &rds);
+        if (r < 0)
+                return r;
+
+        assert(start == 0);
+        assert(packet._data);
+
+        free(rr->wire_format);
+        rr->wire_format = packet._data;
+        rr->wire_format_size = packet.size;
+        rr->wire_format_rdata_offset = rds;
+        rr->wire_format_canonical = canonical;
+
+        packet._data = NULL;
+        dns_packet_unref(&packet);
+
+        return 0;
+}
+
+int dns_resource_record_signer(DnsResourceRecord *rr, const char **ret) {
+        const char *n;
+        int r;
+
+        assert(rr);
+        assert(ret);
+
+        /* Returns the RRset's signer, if it is known. */
+
+        if (rr->n_skip_labels_signer == UINT8_MAX)
+                return -ENODATA;
+
+        n = dns_resource_key_name(rr->key);
+        r = dns_name_skip(n, rr->n_skip_labels_signer, &n);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -EINVAL;
+
+        *ret = n;
+        return 0;
+}
+
+int dns_resource_record_source(DnsResourceRecord *rr, const char **ret) {
+        const char *n;
+        int r;
+
+        assert(rr);
+        assert(ret);
+
+        /* Returns the RRset's synthesizing source, if it is known. */
+
+        if (rr->n_skip_labels_source == UINT8_MAX)
+                return -ENODATA;
+
+        n = dns_resource_key_name(rr->key);
+        r = dns_name_skip(n, rr->n_skip_labels_source, &n);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return -EINVAL;
+
+        *ret = n;
+        return 0;
+}
+
+int dns_resource_record_is_signer(DnsResourceRecord *rr, const char *zone) {
+        const char *signer;
+        int r;
+
+        assert(rr);
+
+        r = dns_resource_record_signer(rr, &signer);
+        if (r < 0)
+                return r;
+
+        return dns_name_equal(zone, signer);
+}
+
+int dns_resource_record_is_synthetic(DnsResourceRecord *rr) {
+        int r;
+
+        assert(rr);
+
+        /* Returns > 0 if the RR is generated from a wildcard, and is not the asterisk name itself */
+
+        if (rr->n_skip_labels_source == UINT8_MAX)
+                return -ENODATA;
+
+        if (rr->n_skip_labels_source == 0)
+                return 0;
+
+        if (rr->n_skip_labels_source > 1)
+                return 1;
+
+        r = dns_name_startswith(dns_resource_key_name(rr->key), "*");
+        if (r < 0)
+                return r;
+
+        return !r;
+}
+
+void dns_resource_record_hash_func(const DnsResourceRecord *rr, struct siphash *state) {
+        assert(rr);
+
+        dns_resource_key_hash_func(rr->key, state);
+
+        switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) {
+
+        case DNS_TYPE_SRV:
+                siphash24_compress(&rr->srv.priority, sizeof(rr->srv.priority), state);
+                siphash24_compress(&rr->srv.weight, sizeof(rr->srv.weight), state);
+                siphash24_compress(&rr->srv.port, sizeof(rr->srv.port), state);
+                dns_name_hash_func(rr->srv.name, state);
+                break;
+
+        case DNS_TYPE_PTR:
+        case DNS_TYPE_NS:
+        case DNS_TYPE_CNAME:
+        case DNS_TYPE_DNAME:
+                dns_name_hash_func(rr->ptr.name, state);
+                break;
+
+        case DNS_TYPE_HINFO:
+                string_hash_func(rr->hinfo.cpu, state);
+                string_hash_func(rr->hinfo.os, state);
+                break;
+
+        case DNS_TYPE_TXT:
+        case DNS_TYPE_SPF: {
+                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;
+        }
+}
+
+int dns_resource_record_compare_func(const DnsResourceRecord *x, const DnsResourceRecord *y) {
+        int r;
+
+        r = dns_resource_key_compare_func(x->key, y->key);
+        if (r != 0)
+                return r;
+
+        if (dns_resource_record_payload_equal(x, y) > 0)
+                return 0;
+
+        /* We still use CMP() here, even though don't implement proper
+         * ordering, since the hashtable doesn't need ordering anyway. */
+        return CMP(x, y);
+}
+
+DEFINE_HASH_OPS(dns_resource_record_hash_ops, DnsResourceRecord, dns_resource_record_hash_func, dns_resource_record_compare_func);
+
+DnsResourceRecord *dns_resource_record_copy(DnsResourceRecord *rr) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *copy = NULL;
+        DnsResourceRecord *t;
+
+        assert(rr);
+
+        copy = dns_resource_record_new(rr->key);
+        if (!copy)
+                return NULL;
+
+        copy->ttl = rr->ttl;
+        copy->expiry = rr->expiry;
+        copy->n_skip_labels_signer = rr->n_skip_labels_signer;
+        copy->n_skip_labels_source = rr->n_skip_labels_source;
+        copy->unparsable = rr->unparsable;
+
+        switch (rr->unparsable ? _DNS_TYPE_INVALID : rr->key->type) {
+
+        case DNS_TYPE_SRV:
+                copy->srv.priority = rr->srv.priority;
+                copy->srv.weight = rr->srv.weight;
+                copy->srv.port = rr->srv.port;
+                copy->srv.name = strdup(rr->srv.name);
+                if (!copy->srv.name)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_PTR:
+        case DNS_TYPE_NS:
+        case DNS_TYPE_CNAME:
+        case DNS_TYPE_DNAME:
+                copy->ptr.name = strdup(rr->ptr.name);
+                if (!copy->ptr.name)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_HINFO:
+                copy->hinfo.cpu = strdup(rr->hinfo.cpu);
+                if (!copy->hinfo.cpu)
+                        return NULL;
+
+                copy->hinfo.os = strdup(rr->hinfo.os);
+                if (!copy->hinfo.os)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_TXT:
+        case DNS_TYPE_SPF:
+                copy->txt.items = dns_txt_item_copy(rr->txt.items);
+                if (!copy->txt.items)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_A:
+                copy->a = rr->a;
+                break;
+
+        case DNS_TYPE_AAAA:
+                copy->aaaa = rr->aaaa;
+                break;
+
+        case DNS_TYPE_SOA:
+                copy->soa.mname = strdup(rr->soa.mname);
+                if (!copy->soa.mname)
+                        return NULL;
+                copy->soa.rname = strdup(rr->soa.rname);
+                if (!copy->soa.rname)
+                        return NULL;
+                copy->soa.serial = rr->soa.serial;
+                copy->soa.refresh = rr->soa.refresh;
+                copy->soa.retry = rr->soa.retry;
+                copy->soa.expire = rr->soa.expire;
+                copy->soa.minimum = rr->soa.minimum;
+                break;
+
+        case DNS_TYPE_MX:
+                copy->mx.priority = rr->mx.priority;
+                copy->mx.exchange = strdup(rr->mx.exchange);
+                if (!copy->mx.exchange)
+                        return NULL;
+                break;
+
+        case DNS_TYPE_LOC:
+                copy->loc = rr->loc;
+                break;
+
+        case DNS_TYPE_SSHFP:
+                copy->sshfp.algorithm = rr->sshfp.algorithm;
+                copy->sshfp.fptype = rr->sshfp.fptype;
+                copy->sshfp.fingerprint = memdup(rr->sshfp.fingerprint, rr->sshfp.fingerprint_size);
+                if (!copy->sshfp.fingerprint)
+                        return NULL;
+                copy->sshfp.fingerprint_size = rr->sshfp.fingerprint_size;
+                break;
+
+        case DNS_TYPE_DNSKEY:
+                copy->dnskey.flags = rr->dnskey.flags;
+                copy->dnskey.protocol = rr->dnskey.protocol;
+                copy->dnskey.algorithm = rr->dnskey.algorithm;
+                copy->dnskey.key = memdup(rr->dnskey.key, rr->dnskey.key_size);
+                if (!copy->dnskey.key)
+                        return NULL;
+                copy->dnskey.key_size = rr->dnskey.key_size;
+                break;
+
+        case DNS_TYPE_RRSIG:
+                copy->rrsig.type_covered = rr->rrsig.type_covered;
+                copy->rrsig.algorithm = rr->rrsig.algorithm;
+                copy->rrsig.labels = rr->rrsig.labels;
+                copy->rrsig.original_ttl = rr->rrsig.original_ttl;
+                copy->rrsig.expiration = rr->rrsig.expiration;
+                copy->rrsig.inception = rr->rrsig.inception;
+                copy->rrsig.key_tag = rr->rrsig.key_tag;
+                copy->rrsig.signer = strdup(rr->rrsig.signer);
+                if (!copy->rrsig.signer)
+                        return NULL;
+                copy->rrsig.signature = memdup(rr->rrsig.signature, rr->rrsig.signature_size);
+                if (!copy->rrsig.signature)
+                        return NULL;
+                copy->rrsig.signature_size = rr->rrsig.signature_size;
+                break;
+
+        case DNS_TYPE_NSEC:
+                copy->nsec.next_domain_name = strdup(rr->nsec.next_domain_name);
+                if (!copy->nsec.next_domain_name)
+                        return NULL;
+                if (rr->nsec.types) {
+                        copy->nsec.types = systemd_bitmap_copy(rr->nsec.types);
+                        if (!copy->nsec.types)
+                                return NULL;
+                }
+                break;
+
+        case DNS_TYPE_DS:
+                copy->ds.key_tag = rr->ds.key_tag;
+                copy->ds.algorithm = rr->ds.algorithm;
+                copy->ds.digest_type = rr->ds.digest_type;
+                copy->ds.digest = memdup(rr->ds.digest, rr->ds.digest_size);
+                if (!copy->ds.digest)
+                        return NULL;
+                copy->ds.digest_size = rr->ds.digest_size;
+                break;
+
+        case DNS_TYPE_NSEC3:
+                copy->nsec3.algorithm = rr->nsec3.algorithm;
+                copy->nsec3.flags = rr->nsec3.flags;
+                copy->nsec3.iterations = rr->nsec3.iterations;
+                copy->nsec3.salt = memdup(rr->nsec3.salt, rr->nsec3.salt_size);
+                if (!copy->nsec3.salt)
+                        return NULL;
+                copy->nsec3.salt_size = rr->nsec3.salt_size;
+                copy->nsec3.next_hashed_name = memdup(rr->nsec3.next_hashed_name, rr->nsec3.next_hashed_name_size);
+                if (!copy->nsec3.next_hashed_name)
+                        return NULL;
+                copy->nsec3.next_hashed_name_size = rr->nsec3.next_hashed_name_size;
+                if (rr->nsec3.types) {
+                        copy->nsec3.types = systemd_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;
+}
+
+bool dns_resource_record_is_link_local_address(DnsResourceRecord *rr) {
+        assert(rr);
+
+        if (rr->key->class != DNS_CLASS_IN)
+                return false;
+
+        if (rr->key->type == DNS_TYPE_A)
+                return in4_addr_is_link_local(&rr->a.in_addr);
+
+        if (rr->key->type == DNS_TYPE_AAAA)
+                return in6_addr_is_link_local(&rr->aaaa.in6_addr);
+
+        return false;
+}
+
+int dns_resource_record_get_cname_target(DnsResourceKey *key, DnsResourceRecord *cname, char **ret) {
+        _cleanup_free_ char *d = NULL;
+        int r;
+
+        assert(key);
+        assert(cname);
+
+        /* Checks if the RR `cname` is a CNAME/DNAME RR that matches the specified `key`. If so, returns the
+         * target domain. If not, returns -EUNATCH */
+
+        if (key->class != cname->key->class && key->class != DNS_CLASS_ANY)
+                return -EUNATCH;
+
+        if (!dns_type_may_redirect(key->type)) /* This key type is not subject to CNAME/DNAME redirection?
+                                                * Then let's refuse right-away */
+                return -EUNATCH;
+
+        if (cname->key->type == DNS_TYPE_CNAME) {
+                r = dns_name_equal(dns_resource_key_name(key),
+                                   dns_resource_key_name(cname->key));
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return -EUNATCH; /* CNAME RR key doesn't actually match the original key */
+
+                d = strdup(cname->cname.name);
+                if (!d)
+                        return -ENOMEM;
+
+        } else if (cname->key->type == DNS_TYPE_DNAME) {
+
+                r = dns_name_change_suffix(
+                                dns_resource_key_name(key),
+                                dns_resource_key_name(cname->key),
+                                cname->dname.name,
+                                &d);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return -EUNATCH; /* DNAME RR key doesn't actually match the original key */
+
+        } else
+                return -EUNATCH; /* Not a CNAME/DNAME RR, hence doesn't match the proposition either */
+
+        *ret = TAKE_PTR(d);
+        return 0;
+}
+
+DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *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..66aa10b
--- /dev/null
+++ b/src/resolve/resolved-dns-rr.h
@@ -0,0 +1,359 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <netinet/in.h>
+
+#include "bitmap.h"
+#include "dns-def.h"
+#include "dns-type.h"
+#include "hashmap.h"
+#include "in-addr-util.h"
+#include "list.h"
+#include "string-util.h"
+#include "time-util.h"
+
+typedef struct DnsResourceKey DnsResourceKey;
+typedef struct DnsResourceRecord DnsResourceRecord;
+typedef struct DnsTxtItem DnsTxtItem;
+
+/* DNSKEY RR flags */
+#define DNSKEY_FLAG_SEP            (UINT16_C(1) << 0)
+#define DNSKEY_FLAG_REVOKE         (UINT16_C(1) << 7)
+#define DNSKEY_FLAG_ZONE_KEY       (UINT16_C(1) << 8)
+
+/* mDNS RR flags */
+#define MDNS_RR_CACHE_FLUSH_OR_QU  (UINT16_C(1) << 15)
+
+/* DNSSEC algorithm identifiers, see
+ * http://tools.ietf.org/html/rfc4034#appendix-A.1 and
+ * https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml */
+enum {
+        DNSSEC_ALGORITHM_RSAMD5 = 1,
+        DNSSEC_ALGORITHM_DH,
+        DNSSEC_ALGORITHM_DSA,
+        DNSSEC_ALGORITHM_ECC,
+        DNSSEC_ALGORITHM_RSASHA1,
+        DNSSEC_ALGORITHM_DSA_NSEC3_SHA1,
+        DNSSEC_ALGORITHM_RSASHA1_NSEC3_SHA1,
+        DNSSEC_ALGORITHM_RSASHA256 = 8,        /* RFC 5702 */
+        DNSSEC_ALGORITHM_RSASHA512 = 10,       /* RFC 5702 */
+        DNSSEC_ALGORITHM_ECC_GOST = 12,        /* RFC 5933 */
+        DNSSEC_ALGORITHM_ECDSAP256SHA256 = 13, /* RFC 6605 */
+        DNSSEC_ALGORITHM_ECDSAP384SHA384 = 14, /* RFC 6605 */
+        DNSSEC_ALGORITHM_ED25519 = 15,         /* RFC 8080 */
+        DNSSEC_ALGORITHM_ED448 = 16,           /* RFC 8080 */
+        DNSSEC_ALGORITHM_INDIRECT = 252,
+        DNSSEC_ALGORITHM_PRIVATEDNS,
+        DNSSEC_ALGORITHM_PRIVATEOID,
+        _DNSSEC_ALGORITHM_MAX_DEFINED
+};
+
+/* DNSSEC digest identifiers, see
+ * https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml */
+enum {
+        DNSSEC_DIGEST_SHA1 = 1,
+        DNSSEC_DIGEST_SHA256 = 2,              /* RFC 4509 */
+        DNSSEC_DIGEST_GOST_R_34_11_94 = 3,     /* RFC 5933 */
+        DNSSEC_DIGEST_SHA384 = 4,              /* RFC 6605 */
+        _DNSSEC_DIGEST_MAX_DEFINED
+};
+
+/* DNSSEC NSEC3 hash algorithms, see
+ * https://www.iana.org/assignments/dnssec-nsec3-parameters/dnssec-nsec3-parameters.xhtml */
+enum {
+        NSEC3_ALGORITHM_SHA1 = 1,
+        _NSEC3_ALGORITHM_MAX_DEFINED
+};
+
+struct DnsResourceKey {
+        unsigned n_ref; /* (unsigned -1) for const keys, see below */
+        uint16_t class, type;
+        char *_name; /* don't access directly, use dns_resource_key_name()! */
+};
+
+/* Creates a temporary resource key. This is only useful to quickly
+ * look up something, without allocating a full DnsResourceKey object
+ * for it. Note that it is not OK to take references to this kind of
+ * resource key object. */
+#define DNS_RESOURCE_KEY_CONST(c, t, n)                 \
+        ((DnsResourceKey) {                             \
+                .n_ref = UINT_MAX,                      \
+                .class = c,                             \
+                .type = t,                              \
+                ._name = (char*) n,                     \
+        })
+
+struct DnsTxtItem {
+        size_t length;
+        LIST_FIELDS(DnsTxtItem, items);
+        uint8_t data[];
+};
+
+struct DnsResourceRecord {
+        unsigned n_ref;
+        uint32_t ttl;
+        usec_t expiry; /* RRSIG signature expiry */
+
+        DnsResourceKey *key;
+
+        char *to_string;
+
+        /* How many labels to strip to determine "signer" of the RRSIG (aka, the zone). -1 if not signed. */
+        uint8_t n_skip_labels_signer;
+        /* How many labels to strip to determine "synthesizing source" of this RR, i.e. the wildcard's immediate parent. -1 if not signed. */
+        uint8_t n_skip_labels_source;
+
+        bool unparsable;
+        bool wire_format_canonical;
+
+        void *wire_format;
+        size_t wire_format_size;
+        size_t wire_format_rdata_offset;
+
+        union {
+                struct {
+                        void *data;
+                        size_t data_size;
+                } generic, opt;
+
+                struct {
+                        char *name;
+                        uint16_t priority;
+                        uint16_t weight;
+                        uint16_t port;
+                } srv;
+
+                struct {
+                        char *name;
+                } ptr, ns, cname, dname;
+
+                struct {
+                        char *cpu;
+                        char *os;
+                } hinfo;
+
+                struct {
+                        DnsTxtItem *items;
+                } txt, spf;
+
+                struct {
+                        struct in_addr in_addr;
+                } a;
+
+                struct {
+                        struct in6_addr in6_addr;
+                } aaaa;
+
+                struct {
+                        char *mname;
+                        char *rname;
+                        uint32_t serial;
+                        uint32_t refresh;
+                        uint32_t retry;
+                        uint32_t expire;
+                        uint32_t minimum;
+                } soa;
+
+                struct {
+                        char *exchange;
+                        uint16_t priority;
+                } mx;
+
+                /* https://tools.ietf.org/html/rfc1876 */
+                struct {
+                        uint8_t version;
+                        uint8_t size;
+                        uint8_t horiz_pre;
+                        uint8_t vert_pre;
+                        uint32_t latitude;
+                        uint32_t longitude;
+                        uint32_t altitude;
+                } loc;
+
+                /* https://tools.ietf.org/html/rfc4255#section-3.1 */
+                struct {
+                        void *fingerprint;
+                        size_t fingerprint_size;
+
+                        uint8_t algorithm;
+                        uint8_t fptype;
+                } sshfp;
+
+                /* http://tools.ietf.org/html/rfc4034#section-2.1 */
+                struct {
+                        void* key;
+                        size_t key_size;
+
+                        uint16_t flags;
+                        uint8_t protocol;
+                        uint8_t algorithm;
+                } dnskey;
+
+                /* http://tools.ietf.org/html/rfc4034#section-3.1 */
+                struct {
+                        char *signer;
+                        void *signature;
+                        size_t signature_size;
+
+                        uint16_t type_covered;
+                        uint8_t algorithm;
+                        uint8_t labels;
+                        uint32_t original_ttl;
+                        uint32_t expiration;
+                        uint32_t inception;
+                        uint16_t key_tag;
+                } rrsig;
+
+                /* https://tools.ietf.org/html/rfc4034#section-4.1 */
+                struct {
+                        char *next_domain_name;
+                        Bitmap *types;
+                } nsec;
+
+                /* https://tools.ietf.org/html/rfc4034#section-5.1 */
+                struct {
+                        void *digest;
+                        size_t digest_size;
+
+                        uint16_t key_tag;
+                        uint8_t algorithm;
+                        uint8_t digest_type;
+                } ds;
+
+                struct {
+                        Bitmap *types;
+                        void *salt;
+                        size_t salt_size;
+                        void *next_hashed_name;
+                        size_t next_hashed_name_size;
+
+                        uint8_t algorithm;
+                        uint8_t flags;
+                        uint16_t iterations;
+                } nsec3;
+
+                /* https://tools.ietf.org/html/draft-ietf-dane-protocol-23 */
+                struct {
+                        void *data;
+                        size_t data_size;
+
+                        uint8_t cert_usage;
+                        uint8_t selector;
+                        uint8_t matching_type;
+                } tlsa;
+
+                /* https://tools.ietf.org/html/rfc6844 */
+                struct {
+                        char *tag;
+                        void *value;
+                        size_t value_size;
+
+                        uint8_t flags;
+                } caa;
+        };
+
+        /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */
+};
+
+/* We use uint8_t for label counts above, and UINT8_MAX/-1 has special meaning. */
+assert_cc(DNS_N_LABELS_MAX < UINT8_MAX);
+
+static inline const void* DNS_RESOURCE_RECORD_RDATA(const DnsResourceRecord *rr) {
+        if (!rr)
+                return NULL;
+
+        if (!rr->wire_format)
+                return NULL;
+
+        assert(rr->wire_format_rdata_offset <= rr->wire_format_size);
+        return (uint8_t*) rr->wire_format + rr->wire_format_rdata_offset;
+}
+
+static inline size_t DNS_RESOURCE_RECORD_RDATA_SIZE(const DnsResourceRecord *rr) {
+        if (!rr)
+                return 0;
+        if (!rr->wire_format)
+                return 0;
+
+        assert(rr->wire_format_rdata_offset <= rr->wire_format_size);
+        return rr->wire_format_size - rr->wire_format_rdata_offset;
+}
+
+static inline uint8_t DNS_RESOURCE_RECORD_OPT_VERSION_SUPPORTED(const DnsResourceRecord *rr) {
+        assert(rr);
+        assert(rr->key->type == DNS_TYPE_OPT);
+
+        return ((rr->ttl >> 16) & 0xFF) == 0;
+}
+
+DnsResourceKey* dns_resource_key_new(uint16_t class, uint16_t type, const char *name);
+DnsResourceKey* dns_resource_key_new_redirect(const DnsResourceKey *key, const DnsResourceRecord *cname);
+int dns_resource_key_new_append_suffix(DnsResourceKey **ret, DnsResourceKey *key, char *name);
+DnsResourceKey* dns_resource_key_new_consume(uint16_t class, uint16_t type, char *name);
+DnsResourceKey* dns_resource_key_ref(DnsResourceKey *key);
+DnsResourceKey* dns_resource_key_unref(DnsResourceKey *key);
+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);
+
+bool dns_resource_record_is_link_local_address(DnsResourceRecord *rr);
+
+int dns_resource_record_get_cname_target(DnsResourceKey *key, DnsResourceRecord *cname, char **ret);
+
+DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *i);
+bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b);
+DnsTxtItem *dns_txt_item_copy(DnsTxtItem *i);
+int dns_txt_item_new_empty(DnsTxtItem **ret);
+
+void dns_resource_record_hash_func(const DnsResourceRecord *i, struct siphash *state);
+int dns_resource_record_compare_func(const DnsResourceRecord *x, const DnsResourceRecord *y);
+
+extern const struct hash_ops dns_resource_key_hash_ops;
+extern const struct hash_ops dns_resource_record_hash_ops;
+
+int dnssec_algorithm_to_string_alloc(int i, char **ret);
+int dnssec_algorithm_from_string(const char *s) _pure_;
+
+int dnssec_digest_to_string_alloc(int i, char **ret);
+int dnssec_digest_from_string(const char *s) _pure_;
diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c
new file mode 100644
index 0000000..360ceec
--- /dev/null
+++ b/src/resolve/resolved-dns-scope.c
@@ -0,0 +1,1639 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/tcp.h>
+
+#include "af-list.h"
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "hostname-util.h"
+#include "missing_network.h"
+#include "random-util.h"
+#include "resolved-dnssd.h"
+#include "resolved-dns-scope.h"
+#include "resolved-dns-zone.h"
+#include "resolved-llmnr.h"
+#include "resolved-mdns.h"
+#include "socket-util.h"
+#include "strv.h"
+
+#define MULTICAST_RATELIMIT_INTERVAL_USEC (1*USEC_PER_SEC)
+#define MULTICAST_RATELIMIT_BURST 1000
+
+/* After how much time to repeat LLMNR requests, see RFC 4795 Section 7 */
+#define MULTICAST_RESEND_TIMEOUT_MIN_USEC (100 * USEC_PER_MSEC)
+#define MULTICAST_RESEND_TIMEOUT_MAX_USEC (1 * USEC_PER_SEC)
+
+int dns_scope_new(Manager *m, DnsScope **ret, Link *l, DnsProtocol protocol, int family) {
+        DnsScope *s;
+
+        assert(m);
+        assert(ret);
+
+        s = new(DnsScope, 1);
+        if (!s)
+                return -ENOMEM;
+
+        *s = (DnsScope) {
+                .manager = m,
+                .link = l,
+                .protocol = protocol,
+                .family = family,
+                .resend_timeout = MULTICAST_RESEND_TIMEOUT_MIN_USEC,
+        };
+
+        if (protocol == DNS_PROTOCOL_DNS) {
+                /* Copy DNSSEC mode from the link if it is set there,
+                 * otherwise take the manager's DNSSEC mode. Note that
+                 * we copy this only at scope creation time, and do
+                 * not update it from the on, even if the setting
+                 * changes. */
+
+                if (l) {
+                        s->dnssec_mode = link_get_dnssec_mode(l);
+                        s->dns_over_tls_mode = link_get_dns_over_tls_mode(l);
+                } else {
+                        s->dnssec_mode = manager_get_dnssec_mode(m);
+                        s->dns_over_tls_mode = manager_get_dns_over_tls_mode(m);
+                }
+
+        } else {
+                s->dnssec_mode = DNSSEC_NO;
+                s->dns_over_tls_mode = DNS_OVER_TLS_NO;
+        }
+
+        LIST_PREPEND(scopes, m->dns_scopes, s);
+
+        dns_scope_llmnr_membership(s, true);
+        dns_scope_mdns_membership(s, true);
+
+        log_debug("New scope on link %s, protocol %s, family %s", l ? l->ifname : "*", dns_protocol_to_string(protocol), family == AF_UNSPEC ? "*" : af_to_name(family));
+
+        /* Enforce ratelimiting for the multicast protocols */
+        s->ratelimit = (RateLimit) { MULTICAST_RATELIMIT_INTERVAL_USEC, MULTICAST_RATELIMIT_BURST };
+
+        *ret = s;
+        return 0;
+}
+
+static void dns_scope_abort_transactions(DnsScope *s) {
+        assert(s);
+
+        while (s->transactions) {
+                DnsTransaction *t = s->transactions;
+
+                /* Abort the transaction, but make sure it is not
+                 * freed while we still look at it */
+
+                t->block_gc++;
+                if (DNS_TRANSACTION_IS_LIVE(t->state))
+                        dns_transaction_complete(t, DNS_TRANSACTION_ABORTED);
+                t->block_gc--;
+
+                dns_transaction_free(t);
+        }
+}
+
+DnsScope* dns_scope_free(DnsScope *s) {
+        if (!s)
+                return NULL;
+
+        log_debug("Removing scope on link %s, protocol %s, family %s", s->link ? s->link->ifname : "*", dns_protocol_to_string(s->protocol), s->family == AF_UNSPEC ? "*" : af_to_name(s->family));
+
+        dns_scope_llmnr_membership(s, false);
+        dns_scope_mdns_membership(s, false);
+        dns_scope_abort_transactions(s);
+
+        while (s->query_candidates)
+                dns_query_candidate_unref(s->query_candidates);
+
+        hashmap_free(s->transactions_by_key);
+
+        ordered_hashmap_free_with_destructor(s->conflict_queue, dns_resource_record_unref);
+        sd_event_source_disable_unref(s->conflict_event_source);
+
+        sd_event_source_disable_unref(s->announce_event_source);
+
+        dns_cache_flush(&s->cache);
+        dns_zone_flush(&s->zone);
+
+        LIST_REMOVE(scopes, s->manager->dns_scopes, s);
+        return mfree(s);
+}
+
+DnsServer *dns_scope_get_dns_server(DnsScope *s) {
+        assert(s);
+
+        if (s->protocol != DNS_PROTOCOL_DNS)
+                return NULL;
+
+        if (s->link)
+                return link_get_dns_server(s->link);
+        else
+                return manager_get_dns_server(s->manager);
+}
+
+unsigned dns_scope_get_n_dns_servers(DnsScope *s) {
+        unsigned n = 0;
+        DnsServer *i;
+
+        assert(s);
+
+        if (s->protocol != DNS_PROTOCOL_DNS)
+                return 0;
+
+        if (s->link)
+                i = s->link->dns_servers;
+        else
+                i = s->manager->dns_servers;
+
+        for (; i; i = i->servers_next)
+                n++;
+
+        return n;
+}
+
+void dns_scope_next_dns_server(DnsScope *s, DnsServer *if_current) {
+        assert(s);
+
+        if (s->protocol != DNS_PROTOCOL_DNS)
+                return;
+
+        /* Changes to the next DNS server in the list. If 'if_current' is passed will do so only if the
+         * current DNS server still matches it. */
+
+        if (s->link)
+                link_next_dns_server(s->link, if_current);
+        else
+                manager_next_dns_server(s->manager, if_current);
+}
+
+void dns_scope_packet_received(DnsScope *s, usec_t rtt) {
+        assert(s);
+
+        if (rtt <= s->max_rtt)
+                return;
+
+        s->max_rtt = rtt;
+        s->resend_timeout = MIN(MAX(MULTICAST_RESEND_TIMEOUT_MIN_USEC, s->max_rtt * 2), MULTICAST_RESEND_TIMEOUT_MAX_USEC);
+}
+
+void dns_scope_packet_lost(DnsScope *s, usec_t usec) {
+        assert(s);
+
+        if (s->resend_timeout <= usec)
+                s->resend_timeout = MIN(s->resend_timeout * 2, MULTICAST_RESEND_TIMEOUT_MAX_USEC);
+}
+
+static int dns_scope_emit_one(DnsScope *s, int fd, int family, DnsPacket *p) {
+        int r;
+
+        assert(s);
+        assert(p);
+        assert(p->protocol == s->protocol);
+
+        if (family == AF_UNSPEC) {
+                if (s->family == AF_UNSPEC)
+                        return -EAFNOSUPPORT;
+
+                family = s->family;
+        }
+
+        switch (s->protocol) {
+
+        case DNS_PROTOCOL_DNS: {
+                size_t mtu, udp_size, min_mtu, socket_mtu = 0;
+
+                assert(fd >= 0);
+
+                if (DNS_PACKET_QDCOUNT(p) > 1) /* Classic DNS only allows one question per packet */
+                        return -EOPNOTSUPP;
+
+                if (p->size > DNS_PACKET_UNICAST_SIZE_MAX)
+                        return -EMSGSIZE;
+
+                /* Determine the local most accurate MTU */
+                if (s->link)
+                        mtu = s->link->mtu;
+                else
+                        mtu = manager_find_mtu(s->manager);
+
+                /* Acquire the socket's PMDU MTU */
+                r = socket_get_mtu(fd, family, &socket_mtu);
+                if (r < 0 && !ERRNO_IS_DISCONNECT(r)) /* Will return ENOTCONN if no information is available yet */
+                        return log_debug_errno(r, "Failed to read socket MTU: %m");
+
+                /* Determine the appropriate UDP header size */
+                udp_size = udp_header_size(family);
+                min_mtu = udp_size + DNS_PACKET_HEADER_SIZE;
+
+                log_debug("Emitting UDP, link MTU is %zu, socket MTU is %zu, minimal MTU is %zu",
+                          mtu, socket_mtu, min_mtu);
+
+                /* Clamp by the kernel's idea of the (path) MTU */
+                if (socket_mtu != 0 && socket_mtu < mtu)
+                        mtu = socket_mtu;
+
+                /* Put a lower limit, in case all MTU data we acquired was rubbish */
+                if (mtu < min_mtu)
+                        mtu = min_mtu;
+
+                /* Now check our packet size against the MTU we determined */
+                if (udp_size + p->size > mtu)
+                        return -EMSGSIZE; /* This means: try TCP instead */
+
+                r = manager_write(s->manager, fd, p);
+                if (r < 0)
+                        return r;
+
+                break;
+        }
+
+        case DNS_PROTOCOL_LLMNR: {
+                union in_addr_union addr;
+
+                assert(fd < 0);
+
+                if (DNS_PACKET_QDCOUNT(p) > 1)
+                        return -EOPNOTSUPP;
+
+                if (!ratelimit_below(&s->ratelimit))
+                        return -EBUSY;
+
+                if (family == AF_INET) {
+                        addr.in = LLMNR_MULTICAST_IPV4_ADDRESS;
+                        fd = manager_llmnr_ipv4_udp_fd(s->manager);
+                } else if (family == AF_INET6) {
+                        addr.in6 = LLMNR_MULTICAST_IPV6_ADDRESS;
+                        fd = manager_llmnr_ipv6_udp_fd(s->manager);
+                } else
+                        return -EAFNOSUPPORT;
+                if (fd < 0)
+                        return fd;
+
+                r = manager_send(s->manager, fd, s->link->ifindex, family, &addr, LLMNR_PORT, NULL, p);
+                if (r < 0)
+                        return r;
+
+                break;
+        }
+
+        case DNS_PROTOCOL_MDNS: {
+                union in_addr_union addr;
+                assert(fd < 0);
+
+                if (!ratelimit_below(&s->ratelimit))
+                        return -EBUSY;
+
+                if (family == AF_INET) {
+                        if (in4_addr_is_null(&p->destination.in))
+                                addr.in = MDNS_MULTICAST_IPV4_ADDRESS;
+                        else
+                                addr = p->destination;
+                        fd = manager_mdns_ipv4_fd(s->manager);
+                } else if (family == AF_INET6) {
+                        if (in6_addr_is_null(&p->destination.in6))
+                                addr.in6 = MDNS_MULTICAST_IPV6_ADDRESS;
+                        else
+                                addr = p->destination;
+                        fd = manager_mdns_ipv6_fd(s->manager);
+                } else
+                        return -EAFNOSUPPORT;
+                if (fd < 0)
+                        return fd;
+
+                r = manager_send(s->manager, fd, s->link->ifindex, family, &addr, p->destination_port ?: MDNS_PORT, NULL, p);
+                if (r < 0)
+                        return r;
+
+                break;
+        }
+
+        default:
+                return -EAFNOSUPPORT;
+        }
+
+        return 1;
+}
+
+int dns_scope_emit_udp(DnsScope *s, int fd, int af, DnsPacket *p) {
+        int r;
+
+        assert(s);
+        assert(p);
+        assert(p->protocol == s->protocol);
+        assert((s->protocol == DNS_PROTOCOL_DNS) == (fd >= 0));
+
+        do {
+                /* If there are multiple linked packets, set the TC bit in all but the last of them */
+                if (p->more) {
+                        assert(p->protocol == DNS_PROTOCOL_MDNS);
+                        dns_packet_set_flags(p, true, true);
+                }
+
+                r = dns_scope_emit_one(s, fd, af, p);
+                if (r < 0)
+                        return r;
+
+                p = p->more;
+        } while (p);
+
+        return 0;
+}
+
+static int dns_scope_socket(
+                DnsScope *s,
+                int type,
+                int family,
+                const union in_addr_union *address,
+                DnsServer *server,
+                uint16_t port,
+                union sockaddr_union *ret_socket_address) {
+
+        _cleanup_close_ int fd = -1;
+        union sockaddr_union sa;
+        socklen_t salen;
+        int r, ifindex;
+
+        assert(s);
+
+        if (server) {
+                assert(family == AF_UNSPEC);
+                assert(!address);
+
+                ifindex = dns_server_ifindex(server);
+
+                switch (server->family) {
+                case AF_INET:
+                        sa = (union sockaddr_union) {
+                                .in.sin_family = server->family,
+                                .in.sin_port = htobe16(port),
+                                .in.sin_addr = server->address.in,
+                        };
+                        salen = sizeof(sa.in);
+                        break;
+                case AF_INET6:
+                        sa = (union sockaddr_union) {
+                                .in6.sin6_family = server->family,
+                                .in6.sin6_port = htobe16(port),
+                                .in6.sin6_addr = server->address.in6,
+                                .in6.sin6_scope_id = ifindex,
+                        };
+                        salen = sizeof(sa.in6);
+                        break;
+                default:
+                        return -EAFNOSUPPORT;
+                }
+        } else {
+                assert(family != AF_UNSPEC);
+                assert(address);
+
+                ifindex = s->link ? s->link->ifindex : 0;
+
+                switch (family) {
+                case AF_INET:
+                        sa = (union sockaddr_union) {
+                                .in.sin_family = family,
+                                .in.sin_port = htobe16(port),
+                                .in.sin_addr = address->in,
+                        };
+                        salen = sizeof(sa.in);
+                        break;
+                case AF_INET6:
+                        sa = (union sockaddr_union) {
+                                .in6.sin6_family = family,
+                                .in6.sin6_port = htobe16(port),
+                                .in6.sin6_addr = address->in6,
+                                .in6.sin6_scope_id = ifindex,
+                        };
+                        salen = sizeof(sa.in6);
+                        break;
+                default:
+                        return -EAFNOSUPPORT;
+                }
+        }
+
+        fd = socket(sa.sa.sa_family, type|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (fd < 0)
+                return -errno;
+
+        if (type == SOCK_STREAM) {
+                r = setsockopt_int(fd, IPPROTO_TCP, TCP_NODELAY, true);
+                if (r < 0)
+                        return r;
+        }
+
+        if (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;
+
+                /* Turn of path MTU discovery for security reasons */
+                r = socket_disable_pmtud(fd, sa.sa.sa_family);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to disable UDP PMTUD, ignoring: %m");
+
+                /* Learn about fragmentation taking place */
+                r = socket_set_recvfragsize(fd, sa.sa.sa_family, true);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to enable fragment size reception, ignoring: %m");
+        }
+
+        if (ret_socket_address)
+                *ret_socket_address = sa;
+        else {
+                bool bound = false;
+
+                /* Let's temporarily bind the socket to the specified ifindex. The kernel currently takes
+                 * only the SO_BINDTODEVICE/SO_BINDTOINDEX ifindex into account when making routing decisions
+                 * in connect() — and not IP_UNICAST_IF. We don't really want any of the other semantics of
+                 * SO_BINDTODEVICE/SO_BINDTOINDEX, hence we immediately unbind the socket after the fact
+                 * again.
+                 *
+                 * As a special exception we don't do this if we notice that the specified IP address is on
+                 * the local host. SO_BINDTODEVICE in combination with destination addresses on the local
+                 * host result in EHOSTUNREACH, since Linux won't send the packets out of the specified
+                 * interface, but delivers them directly to the local socket. */
+                if (s->link &&
+                    !manager_find_link_address(s->manager, sa.sa.sa_family, sockaddr_in_addr(&sa.sa))) {
+                        r = socket_bind_to_ifindex(fd, ifindex);
+                        if (r < 0)
+                                return r;
+
+                        bound = true;
+                }
+
+                r = connect(fd, &sa.sa, salen);
+                if (r < 0 && errno != EINPROGRESS)
+                        return -errno;
+
+                if (bound) {
+                        r = socket_bind_to_ifindex(fd, 0);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        return TAKE_FD(fd);
+}
+
+int dns_scope_socket_udp(DnsScope *s, DnsServer *server) {
+        return dns_scope_socket(s, SOCK_DGRAM, AF_UNSPEC, NULL, server, dns_server_port(server), NULL);
+}
+
+int dns_scope_socket_tcp(DnsScope *s, int family, const union in_addr_union *address, DnsServer *server, uint16_t port, union sockaddr_union *ret_socket_address) {
+        /* If ret_socket_address is not NULL, the caller is responsible
+         * for calling connect() or sendmsg(). This is required by TCP
+         * Fast Open, to be able to send the initial SYN packet along
+         * with the first data packet. */
+        return dns_scope_socket(s, SOCK_STREAM, family, address, server, port, ret_socket_address);
+}
+
+static DnsScopeMatch match_link_local_reverse_lookups(const char *domain) {
+        assert(domain);
+
+        if (dns_name_endswith(domain, "254.169.in-addr.arpa") > 0)
+                return DNS_SCOPE_YES_BASE + 4; /* 4 labels match */
+
+        if (dns_name_endswith(domain, "8.e.f.ip6.arpa") > 0 ||
+            dns_name_endswith(domain, "9.e.f.ip6.arpa") > 0 ||
+            dns_name_endswith(domain, "a.e.f.ip6.arpa") > 0 ||
+            dns_name_endswith(domain, "b.e.f.ip6.arpa") > 0)
+                return DNS_SCOPE_YES_BASE + 5; /* 5 labels match */
+
+        return _DNS_SCOPE_MATCH_INVALID;
+}
+
+static DnsScopeMatch match_subnet_reverse_lookups(
+                DnsScope *s,
+                const char *domain,
+                bool exclude_own) {
+
+        union in_addr_union ia;
+        LinkAddress *a;
+        int f, r;
+
+        assert(s);
+        assert(domain);
+
+        /* Checks whether the specified domain is a reverse address domain (i.e. in the .in-addr.arpa or
+         * .ip6.arpa area), and if so, whether the address matches any of the local subnets of the link the
+         * scope is associated with. If so, our scope should consider itself relevant for any lookup in the
+         * domain, since it apparently refers to hosts on this link's subnet.
+         *
+         * If 'exclude_own' is true this will return DNS_SCOPE_NO for any IP addresses assigned locally. This
+         * is useful for LLMNR/mDNS as we never want to look up our own hostname on LLMNR/mDNS but always use
+         * the locally synthesized one. */
+
+        if (!s->link)
+                return _DNS_SCOPE_MATCH_INVALID; /* No link, hence no local addresses to check */
+
+        r = dns_name_address(domain, &f, &ia);
+        if (r < 0)
+                log_debug_errno(r, "Failed to determine whether '%s' is an address domain: %m", domain);
+        if (r <= 0)
+                return _DNS_SCOPE_MATCH_INVALID;
+
+        if (s->family != AF_UNSPEC && f != s->family)
+                return _DNS_SCOPE_MATCH_INVALID; /* Don't look for IPv4 addresses on LLMNR/mDNS over IPv6 and vice versa */
+
+        LIST_FOREACH(addresses, a, s->link->addresses) {
+
+                if (a->family != f)
+                        continue;
+
+                /* Equals our own address? nah, let's not use this scope. The local synthesizer will pick it up for us. */
+                if (exclude_own &&
+                    in_addr_equal(f, &a->in_addr, &ia) > 0)
+                        return DNS_SCOPE_NO;
+
+                if (a->prefixlen == UCHAR_MAX) /* don't know subnet mask */
+                        continue;
+
+                /* Check if the address is in the local subnet */
+                r = in_addr_prefix_covers(f, &a->in_addr, a->prefixlen, &ia);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to determine whether link address covers lookup address '%s': %m", domain);
+                if (r > 0)
+                        /* Note that we only claim zero labels match. This is so that this is at the same
+                         * priority a DNS scope with "." as routing domain is. */
+                        return DNS_SCOPE_YES_BASE + 0;
+        }
+
+        return _DNS_SCOPE_MATCH_INVALID;
+}
+
+DnsScopeMatch dns_scope_good_domain(
+                DnsScope *s,
+                DnsQuery *q) {
+
+        DnsQuestion *question;
+        DnsSearchDomain *d;
+        const char *domain;
+        uint64_t flags;
+        int ifindex;
+
+        /* This returns the following return values:
+         *
+         *    DNS_SCOPE_NO         → This scope is not suitable for lookups of this domain, at all
+         *    DNS_SCOPE_MAYBE      → This scope is suitable, but only if nothing else wants it
+         *    DNS_SCOPE_YES_BASE+n → This scope is suitable, and 'n' suffix labels match
+         *
+         *  (The idea is that the caller will only use the scopes with the longest 'n' returned. If no scopes return
+         *  DNS_SCOPE_YES_BASE+n, then it should use those which returned DNS_SCOPE_MAYBE. It should never use those
+         *  which returned DNS_SCOPE_NO.)
+         */
+
+        assert(s);
+        assert(q);
+
+        question = dns_query_question_for_protocol(q, s->protocol);
+        if (!question)
+                return DNS_SCOPE_NO;
+
+        domain = dns_question_first_name(question);
+        if (!domain)
+                return DNS_SCOPE_NO;
+
+        ifindex = q->ifindex;
+        flags = q->flags;
+
+        /* Checks if the specified domain is something to look up on this scope. Note that this accepts
+         * non-qualified hostnames, i.e. those without any search path suffixed. */
+
+        if (ifindex != 0 && (!s->link || s->link->ifindex != ifindex))
+                return DNS_SCOPE_NO;
+
+        if ((SD_RESOLVED_FLAGS_MAKE(s->protocol, s->family, false, false) & flags) == 0)
+                return DNS_SCOPE_NO;
+
+        /* Never resolve any loopback hostname or IP address via DNS, LLMNR or mDNS. Instead, always rely on
+         * synthesized RRs for these. */
+        if (is_localhost(domain) ||
+            dns_name_endswith(domain, "127.in-addr.arpa") > 0 ||
+            dns_name_equal(domain, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa") > 0)
+                return DNS_SCOPE_NO;
+
+        /* Never respond to some of the domains listed in RFC6303 */
+        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 or _outbound domain — they're something special, synthesized locally. */
+        if (is_gateway_hostname(domain) || is_outbound_hostname(domain))
+                return DNS_SCOPE_NO;
+
+        switch (s->protocol) {
+
+        case DNS_PROTOCOL_DNS: {
+                bool has_search_domains = false;
+                DnsScopeMatch m;
+                int n_best = -1;
+
+                if (dns_name_is_empty(domain)) {
+                        DnsResourceKey *t;
+                        bool found = false;
+
+                        /* Refuse empty name if only A and/or AAAA records are requested. */
+
+                        DNS_QUESTION_FOREACH(t, question)
+                                if (!IN_SET(t->type, DNS_TYPE_A, DNS_TYPE_AAAA)) {
+                                        found = true;
+                                        break;
+                                }
+
+                        if (!found)
+                                return DNS_SCOPE_NO;
+                }
+
+                /* Never route things to scopes that lack DNS servers */
+                if (!dns_scope_get_dns_server(s))
+                        return DNS_SCOPE_NO;
+
+                /* Always honour search domains for routing queries, except if this scope lacks DNS servers. Note that
+                 * we return DNS_SCOPE_YES here, rather than just DNS_SCOPE_MAYBE, which means other wildcard scopes
+                 * won't be considered anymore. */
+                LIST_FOREACH(domains, d, dns_scope_get_search_domains(s)) {
+
+                        if (!d->route_only && !dns_name_is_root(d->name))
+                                has_search_domains = true;
+
+                        if (dns_name_endswith(domain, d->name) > 0) {
+                                int c;
+
+                                c = dns_name_count_labels(d->name);
+                                if (c < 0)
+                                        continue;
+
+                                if (c > n_best)
+                                        n_best = c;
+                        }
+                }
+
+                /* If there's a true search domain defined for this scope, and the query is single-label,
+                 * then let's resolve things here, prefereably. Note that LLMNR considers itself
+                 * authoritative for single-label names too, at the same preference, see below. */
+                if (has_search_domains && dns_name_is_single_label(domain))
+                        return DNS_SCOPE_YES_BASE + 1;
+
+                /* Let's return the number of labels in the best matching result */
+                if (n_best >= 0) {
+                        assert(n_best <= DNS_SCOPE_YES_END - DNS_SCOPE_YES_BASE);
+                        return DNS_SCOPE_YES_BASE + n_best;
+                }
+
+                /* Exclude link-local IP ranges */
+                if (match_link_local_reverse_lookups(domain) >= DNS_SCOPE_YES_BASE ||
+                    /* If networks use .local in their private setups, they are supposed to also add .local
+                     * to their search domains, which we already checked above. Otherwise, we consider .local
+                     * specific to mDNS and won't send such queries ordinary DNS servers. */
+                    dns_name_endswith(domain, "local") > 0)
+                        return DNS_SCOPE_NO;
+
+                /* If the IP address to look up matches the local subnet, then implicitly synthesizes
+                 * DNS_SCOPE_YES_BASE + 0 on this interface, i.e. preferably resolve IP addresses via the DNS
+                 * server belonging to this interface. */
+                m = match_subnet_reverse_lookups(s, domain, false);
+                if (m >= 0)
+                        return m;
+
+                /* If there was no match at all, then see if this scope is suitable as default route. */
+                if (!dns_scope_is_default_route(s))
+                        return DNS_SCOPE_NO;
+
+                return DNS_SCOPE_MAYBE;
+        }
+
+        case DNS_PROTOCOL_MDNS: {
+                DnsScopeMatch m;
+
+                m = match_link_local_reverse_lookups(domain);
+                if (m >= 0)
+                        return m;
+
+                m = match_subnet_reverse_lookups(s, domain, true);
+                if (m >= 0)
+                        return m;
+
+                if ((s->family == AF_INET && dns_name_endswith(domain, "in-addr.arpa") > 0) ||
+                    (s->family == AF_INET6 && dns_name_endswith(domain, "ip6.arpa") > 0))
+                        return DNS_SCOPE_MAYBE;
+
+                if ((dns_name_endswith(domain, "local") > 0 && /* only resolve names ending in .local via mDNS */
+                     dns_name_equal(domain, "local") == 0 &&   /* but not the single-label "local" name itself */
+                     manager_is_own_hostname(s->manager, domain) <= 0)) /* never resolve the local hostname via mDNS */
+                        return DNS_SCOPE_YES_BASE + 1; /* Return +1, as the top-level .local domain matches, i.e. one label */
+
+                return DNS_SCOPE_NO;
+        }
+
+        case DNS_PROTOCOL_LLMNR: {
+                DnsScopeMatch m;
+
+                m = match_link_local_reverse_lookups(domain);
+                if (m >= 0)
+                        return m;
+
+                m = match_subnet_reverse_lookups(s, domain, true);
+                if (m >= 0)
+                        return m;
+
+                if ((s->family == AF_INET && dns_name_endswith(domain, "in-addr.arpa") > 0) ||
+                    (s->family == AF_INET6 && dns_name_endswith(domain, "ip6.arpa") > 0))
+                        return DNS_SCOPE_MAYBE;
+
+                if ((dns_name_is_single_label(domain) && /* only resolve single label names via LLMNR */
+                     !is_gateway_hostname(domain) && /* don't resolve "_gateway" with LLMNR, let local synthesizing logic handle that */
+                     !is_outbound_hostname(domain) && /* similar for "_outbound" */
+                     dns_name_equal(domain, "local") == 0 && /* don't resolve "local" with LLMNR, it's the top-level domain of mDNS after all, see above */
+                     manager_is_own_hostname(s->manager, domain) <= 0))  /* never resolve the local hostname via LLMNR */
+                        return DNS_SCOPE_YES_BASE + 1; /* Return +1, as we consider ourselves authoritative
+                                                        * for single-label names, i.e. one label. This is
+                                                        * particularly relevant as it means a "." route on some
+                                                        * other scope won't pull all traffic away from
+                                                        * us. (If people actually want to pull traffic away
+                                                        * from us they should turn off LLMNR on the
+                                                        * link). Note that unicast DNS scopes with search
+                                                        * domains also consider themselves authoritative for
+                                                        * single-label domains, at the same preference (see
+                                                        * above). */
+
+                return DNS_SCOPE_NO;
+        }
+
+        default:
+                assert_not_reached("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 a
+         * fully qualified name, i.e. the search suffixes already appended. */
+
+        if (!IN_SET(key->class, DNS_CLASS_IN, DNS_CLASS_ANY))
+                return false;
+
+        if (s->protocol == DNS_PROTOCOL_DNS) {
+
+                /* On classic DNS, looking up non-address RRs is always fine. (Specifically, we want to
+                 * permit looking up DNSKEY and DS records on the root and top-level domains.) */
+                if (!dns_resource_key_is_address(key))
+                        return true;
+
+                /* Unless explicitly overridden, we refuse to look up A and AAAA RRs on the root and
+                 * single-label domains, under the assumption that those should be resolved via LLMNR or
+                 * search path only, and should not be leaked onto the internet. */
+                const char* name = dns_resource_key_name(key);
+
+                if (!s->manager->resolve_unicast_single_label &&
+                    dns_name_is_single_label(name))
+                        return false;
+
+                return !dns_name_is_root(name);
+        }
+
+        /* Never route DNSSEC RR queries to LLMNR/mDNS scopes */
+        if (dns_type_is_dnssec(key->type))
+                return false;
+
+        /* On mDNS and LLMNR, send A and AAAA queries only on the respective scopes */
+
+        key_family = dns_type_to_af(key->type);
+        if (key_family < 0)
+                return true;
+
+        return key_family == s->family;
+}
+
+static int dns_scope_multicast_membership(DnsScope *s, bool b, struct in_addr in, struct in6_addr in6) {
+        int fd;
+
+        assert(s);
+        assert(s->link);
+
+        if (s->family == AF_INET) {
+                struct ip_mreqn mreqn = {
+                        .imr_multiaddr = in,
+                        .imr_ifindex = s->link->ifindex,
+                };
+
+                if (s->protocol == DNS_PROTOCOL_LLMNR)
+                        fd = manager_llmnr_ipv4_udp_fd(s->manager);
+                else
+                        fd = manager_mdns_ipv4_fd(s->manager);
+
+                if (fd < 0)
+                        return fd;
+
+                /* Always first try to drop membership before we add
+                 * one. This is necessary on some devices, such as
+                 * veth. */
+                if (b)
+                        (void) setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreqn, sizeof(mreqn));
+
+                if (setsockopt(fd, IPPROTO_IP, b ? IP_ADD_MEMBERSHIP : IP_DROP_MEMBERSHIP, &mreqn, sizeof(mreqn)) < 0)
+                        return -errno;
+
+        } else if (s->family == AF_INET6) {
+                struct ipv6_mreq mreq = {
+                        .ipv6mr_multiaddr = in6,
+                        .ipv6mr_interface = s->link->ifindex,
+                };
+
+                if (s->protocol == DNS_PROTOCOL_LLMNR)
+                        fd = manager_llmnr_ipv6_udp_fd(s->manager);
+                else
+                        fd = manager_mdns_ipv6_fd(s->manager);
+
+                if (fd < 0)
+                        return fd;
+
+                if (b)
+                        (void) setsockopt(fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
+
+                if (setsockopt(fd, IPPROTO_IPV6, b ? IPV6_ADD_MEMBERSHIP : IPV6_DROP_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
+                        return -errno;
+        } else
+                return -EAFNOSUPPORT;
+
+        return 0;
+}
+
+int dns_scope_llmnr_membership(DnsScope *s, bool b) {
+        assert(s);
+
+        if (s->protocol != DNS_PROTOCOL_LLMNR)
+                return 0;
+
+        return dns_scope_multicast_membership(s, b, LLMNR_MULTICAST_IPV4_ADDRESS, LLMNR_MULTICAST_IPV6_ADDRESS);
+}
+
+int dns_scope_mdns_membership(DnsScope *s, bool b) {
+        assert(s);
+
+        if (s->protocol != DNS_PROTOCOL_MDNS)
+                return 0;
+
+        return dns_scope_multicast_membership(s, b, MDNS_MULTICAST_IPV4_ADDRESS, MDNS_MULTICAST_IPV6_ADDRESS);
+}
+
+int dns_scope_make_reply_packet(
+                DnsScope *s,
+                uint16_t id,
+                int rcode,
+                DnsQuestion *q,
+                DnsAnswer *answer,
+                DnsAnswer *soa,
+                bool tentative,
+                DnsPacket **ret) {
+
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        unsigned n_answer = 0, n_soa = 0;
+        int r;
+        bool c_or_aa;
+
+        assert(s);
+        assert(ret);
+
+        if (dns_question_isempty(q) &&
+            dns_answer_isempty(answer) &&
+            dns_answer_isempty(soa))
+                return -EINVAL;
+
+        r = dns_packet_new(&p, s->protocol, 0, DNS_PACKET_SIZE_MAX);
+        if (r < 0)
+                return r;
+
+        /* mDNS answers must have the Authoritative Answer bit set, see RFC 6762, section 18.4. */
+        c_or_aa = s->protocol == DNS_PROTOCOL_MDNS;
+
+        DNS_PACKET_HEADER(p)->id = id;
+        DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS(
+                                                              1 /* qr */,
+                                                              0 /* opcode */,
+                                                              c_or_aa,
+                                                              0 /* tc */,
+                                                              tentative,
+                                                              0 /* (ra) */,
+                                                              0 /* (ad) */,
+                                                              0 /* (cd) */,
+                                                              rcode));
+
+        r = dns_packet_append_question(p, q);
+        if (r < 0)
+                return r;
+        DNS_PACKET_HEADER(p)->qdcount = htobe16(dns_question_size(q));
+
+        r = dns_packet_append_answer(p, answer, &n_answer);
+        if (r < 0)
+                return r;
+        DNS_PACKET_HEADER(p)->ancount = htobe16(n_answer);
+
+        r = dns_packet_append_answer(p, soa, &n_soa);
+        if (r < 0)
+                return r;
+        DNS_PACKET_HEADER(p)->arcount = htobe16(n_soa);
+
+        *ret = TAKE_PTR(p);
+
+        return 0;
+}
+
+static void dns_scope_verify_conflicts(DnsScope *s, DnsPacket *p) {
+        DnsResourceRecord *rr;
+        DnsResourceKey *key;
+
+        assert(s);
+        assert(p);
+
+        DNS_QUESTION_FOREACH(key, p->question)
+                dns_zone_verify_conflicts(&s->zone, key);
+
+        DNS_ANSWER_FOREACH(rr, p->answer)
+                dns_zone_verify_conflicts(&s->zone, rr->key);
+}
+
+void dns_scope_process_query(DnsScope *s, DnsStream *stream, DnsPacket *p) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL;
+        _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL;
+        DnsResourceKey *key = NULL;
+        bool tentative = false;
+        int r;
+
+        assert(s);
+        assert(p);
+
+        if (p->protocol != DNS_PROTOCOL_LLMNR)
+                return;
+
+        if (p->ipproto == IPPROTO_UDP) {
+                /* Don't accept UDP queries directed to anything but
+                 * the LLMNR multicast addresses. See RFC 4795,
+                 * section 2.5. */
+
+                if (p->family == AF_INET && !in4_addr_equal(&p->destination.in, &LLMNR_MULTICAST_IPV4_ADDRESS))
+                        return;
+
+                if (p->family == AF_INET6 && !in6_addr_equal(&p->destination.in6, &LLMNR_MULTICAST_IPV6_ADDRESS))
+                        return;
+        }
+
+        r = dns_packet_extract(p);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to extract resource records from incoming packet: %m");
+                return;
+        }
+
+        if (DNS_PACKET_LLMNR_C(p)) {
+                /* Somebody notified us about a possible conflict */
+                dns_scope_verify_conflicts(s, p);
+                return;
+        }
+
+        assert(dns_question_size(p->question) == 1);
+        key = dns_question_first_key(p->question);
+
+        r = dns_zone_lookup(&s->zone, key, 0, &answer, &soa, &tentative);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to look up key: %m");
+                return;
+        }
+        if (r == 0)
+                return;
+
+        if (answer)
+                dns_answer_order_by_scope(answer, in_addr_is_link_local(p->family, &p->sender) > 0);
+
+        r = dns_scope_make_reply_packet(s, DNS_PACKET_ID(p), DNS_RCODE_SUCCESS, p->question, answer, soa, tentative, &reply);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to build reply packet: %m");
+                return;
+        }
+
+        if (stream) {
+                r = dns_stream_write_packet(stream, reply);
+                if (r < 0) {
+                        log_debug_errno(r, "Failed to enqueue reply packet: %m");
+                        return;
+                }
+
+                /* Let's take an extra reference on this stream, so that it stays around after returning. The reference
+                 * will be dangling until the stream is disconnected, and the default completion handler of the stream
+                 * will then unref the stream and destroy it */
+                if (DNS_STREAM_QUEUED(stream))
+                        dns_stream_ref(stream);
+        } else {
+                int fd;
+
+                if (!ratelimit_below(&s->ratelimit))
+                        return;
+
+                if (p->family == AF_INET)
+                        fd = manager_llmnr_ipv4_udp_fd(s->manager);
+                else if (p->family == AF_INET6)
+                        fd = manager_llmnr_ipv6_udp_fd(s->manager);
+                else {
+                        log_debug("Unknown protocol");
+                        return;
+                }
+                if (fd < 0) {
+                        log_debug_errno(fd, "Failed to get reply socket: %m");
+                        return;
+                }
+
+                /* Note that we always immediately reply to all LLMNR
+                 * requests, and do not wait any time, since we
+                 * verified uniqueness for all records. Also see RFC
+                 * 4795, Section 2.7 */
+
+                r = manager_send(s->manager, fd, p->ifindex, p->family, &p->sender, p->sender_port, NULL, reply);
+                if (r < 0) {
+                        log_debug_errno(r, "Failed to send reply packet: %m");
+                        return;
+                }
+        }
+}
+
+DnsTransaction *dns_scope_find_transaction(
+                DnsScope *scope,
+                DnsResourceKey *key,
+                uint64_t query_flags) {
+
+        DnsTransaction *first, *t;
+
+        assert(scope);
+        assert(key);
+
+        /* Iterate through the list of transactions with a matching key */
+        first = hashmap_get(scope->transactions_by_key, key);
+        LIST_FOREACH(transactions_by_key, t, first) {
+
+                /* These four flags must match exactly: we cannot use a validated response for a
+                 * non-validating client, and we cannot use a non-validated response for a validating
+                 * client. Similar, if the sources don't match things aren't usable either. */
+                if (((query_flags ^ t->query_flags) &
+                     (SD_RESOLVED_NO_VALIDATE|
+                     SD_RESOLVED_NO_ZONE|
+                      SD_RESOLVED_NO_TRUST_ANCHOR|
+                      SD_RESOLVED_NO_NETWORK)) != 0)
+                        continue;
+
+                /* We can reuse a primary query if a regular one is requested, but not vice versa */
+                if ((query_flags & SD_RESOLVED_REQUIRE_PRIMARY) &&
+                    !(t->query_flags & SD_RESOLVED_REQUIRE_PRIMARY))
+                        continue;
+
+                /* Don't reuse a transaction that allowed caching when we got told not to use it */
+                if ((query_flags & SD_RESOLVED_NO_CACHE) &&
+                    !(t->query_flags & SD_RESOLVED_NO_CACHE))
+                        continue;
+
+                /* If we are asked to clamp ttls an the existing transaction doesn't do it, we can't
+                 * reuse */
+                if ((query_flags & SD_RESOLVED_CLAMP_TTL) &&
+                    !(t->query_flags & SD_RESOLVED_CLAMP_TTL))
+                        continue;
+
+                return t;
+        }
+
+        return NULL;
+}
+
+static int dns_scope_make_conflict_packet(
+                DnsScope *s,
+                DnsResourceRecord *rr,
+                DnsPacket **ret) {
+
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        int r;
+
+        assert(s);
+        assert(rr);
+        assert(ret);
+
+        r = dns_packet_new(&p, s->protocol, 0, DNS_PACKET_SIZE_MAX);
+        if (r < 0)
+                return r;
+
+        DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS(
+                                                              0 /* qr */,
+                                                              0 /* opcode */,
+                                                              1 /* conflict */,
+                                                              0 /* tc */,
+                                                              0 /* t */,
+                                                              0 /* (ra) */,
+                                                              0 /* (ad) */,
+                                                              0 /* (cd) */,
+                                                              0));
+
+        /* For mDNS, the transaction ID should always be 0 */
+        if (s->protocol != DNS_PROTOCOL_MDNS)
+                random_bytes(&DNS_PACKET_HEADER(p)->id, sizeof(uint16_t));
+
+        DNS_PACKET_HEADER(p)->qdcount = htobe16(1);
+        DNS_PACKET_HEADER(p)->arcount = htobe16(1);
+
+        r = dns_packet_append_key(p, rr->key, 0, NULL);
+        if (r < 0)
+                return r;
+
+        r = dns_packet_append_rr(p, rr, 0, NULL, NULL);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(p);
+
+        return 0;
+}
+
+static int on_conflict_dispatch(sd_event_source *es, usec_t usec, void *userdata) {
+        DnsScope *scope = userdata;
+        int r;
+
+        assert(es);
+        assert(scope);
+
+        scope->conflict_event_source = sd_event_source_disable_unref(scope->conflict_event_source);
+
+        for (;;) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+                _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+
+                key = ordered_hashmap_first_key(scope->conflict_queue);
+                if (!key)
+                        break;
+
+                rr = ordered_hashmap_remove(scope->conflict_queue, key);
+                assert(rr);
+
+                r = dns_scope_make_conflict_packet(scope, rr, &p);
+                if (r < 0) {
+                        log_error_errno(r, "Failed to make conflict packet: %m");
+                        return 0;
+                }
+
+                r = dns_scope_emit_udp(scope, -1, AF_UNSPEC, p);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to send conflict packet: %m");
+        }
+
+        return 0;
+}
+
+int dns_scope_notify_conflict(DnsScope *scope, DnsResourceRecord *rr) {
+        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_packet_from_local_address(scope->manager, p))
+                return;
+
+        r = dns_packet_extract(p);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to extract packet: %m");
+                return;
+        }
+
+        log_debug("Checking for conflicts...");
+
+        DNS_ANSWER_FOREACH(rr, p->answer) {
+                /* No conflict if it is DNS-SD RR used for service enumeration. */
+                if (dns_resource_key_is_dnssd_ptr(rr->key))
+                        continue;
+
+                /* Check for conflicts against the local zone. If we
+                 * found one, we won't check any further */
+                r = dns_zone_check_conflicts(&scope->zone, rr);
+                if (r != 0)
+                        continue;
+
+                /* Check for conflicts against the local cache. If so,
+                 * send out an advisory query, to inform everybody */
+                r = dns_cache_check_conflicts(&scope->cache, rr, p->family, &p->sender);
+                if (r <= 0)
+                        continue;
+
+                dns_scope_notify_conflict(scope, rr);
+        }
+}
+
+void dns_scope_dump(DnsScope *s, FILE *f) {
+        assert(s);
+
+        if (!f)
+                f = stdout;
+
+        fputs("[Scope protocol=", f);
+        fputs(dns_protocol_to_string(s->protocol), f);
+
+        if (s->link) {
+                fputs(" interface=", f);
+                fputs(s->link->ifname, f);
+        }
+
+        if (s->family != AF_UNSPEC) {
+                fputs(" family=", f);
+                fputs(af_to_name(s->family), f);
+        }
+
+        fputs("]\n", f);
+
+        if (!dns_zone_is_empty(&s->zone)) {
+                fputs("ZONE:\n", f);
+                dns_zone_dump(&s->zone, f);
+        }
+
+        if (!dns_cache_is_empty(&s->cache)) {
+                fputs("CACHE:\n", f);
+                dns_cache_dump(&s->cache, f);
+        }
+}
+
+DnsSearchDomain *dns_scope_get_search_domains(DnsScope *s) {
+        assert(s);
+
+        if (s->protocol != DNS_PROTOCOL_DNS)
+                return NULL;
+
+        if (s->link)
+                return s->link->search_domains;
+
+        return s->manager->search_domains;
+}
+
+bool dns_scope_name_wants_search_domain(DnsScope *s, const char *name) {
+        assert(s);
+
+        if (s->protocol != DNS_PROTOCOL_DNS)
+                return false;
+
+        return dns_name_is_single_label(name);
+}
+
+bool dns_scope_network_good(DnsScope *s) {
+        /* Checks whether the network is in good state for lookups on this scope. For mDNS/LLMNR/Classic DNS scopes
+         * bound to links this is easy, as they don't even exist if the link isn't in a suitable state. For the global
+         * DNS scope we check whether there are any links that are up and have an address.
+         *
+         * Note that Linux routing is complex and even systems that superficially have no IPv4 address might
+         * be able to route IPv4 (and similar for IPv6), hence let's make a check here independent of address
+         * family. */
+
+        if (s->link)
+                return true;
+
+        return manager_routable(s->manager);
+}
+
+int dns_scope_ifindex(DnsScope *s) {
+        assert(s);
+
+        if (s->link)
+                return s->link->ifindex;
+
+        return 0;
+}
+
+static int on_announcement_timeout(sd_event_source *s, usec_t usec, void *userdata) {
+        DnsScope *scope = userdata;
+
+        assert(s);
+
+        scope->announce_event_source = sd_event_source_disable_unref(scope->announce_event_source);
+
+        (void) dns_scope_announce(scope, false);
+        return 0;
+}
+
+int dns_scope_announce(DnsScope *scope, bool goodbye) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        _cleanup_set_free_ Set *types = NULL;
+        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, NULL);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to add RR to announce: %m");
+                }
+
+        /* Since all the active services are in the zone make them discoverable now. */
+        SET_FOREACH(service_type, types) {
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+
+                rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR,
+                                                  "_services._dns-sd._udp.local");
+                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, NULL);
+                if (r < 0)
+                        return log_debug_errno(r, "Failed to add RR to announce: %m");
+        }
+
+        if (dns_answer_isempty(answer))
+                return 0;
+
+        r = dns_scope_make_reply_packet(scope, 0, DNS_RCODE_SUCCESS, NULL, answer, NULL, false, &p);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to build reply packet: %m");
+
+        r = dns_scope_emit_udp(scope, -1, AF_UNSPEC, p);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to send reply packet: %m");
+
+        /* In section 8.3 of RFC6762: "The Multicast DNS responder MUST send at least two unsolicited
+         * responses, one second apart." */
+        if (!scope->announced) {
+                scope->announced = true;
+
+                r = sd_event_add_time_relative(
+                                scope->manager->event,
+                                &scope->announce_event_source,
+                                clock_boottime_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..1f9d22b
--- /dev/null
+++ b/src/resolve/resolved-dns-scope.h
@@ -0,0 +1,112 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "list.h"
+#include "ratelimit.h"
+
+typedef struct DnsQueryCandidate DnsQueryCandidate;
+typedef struct DnsScope DnsScope;
+
+#include "resolved-dns-cache.h"
+#include "resolved-dns-dnssec.h"
+#include "resolved-dns-packet.h"
+#include "resolved-dns-query.h"
+#include "resolved-dns-search-domain.h"
+#include "resolved-dns-server.h"
+#include "resolved-dns-stream.h"
+#include "resolved-dns-zone.h"
+
+typedef enum DnsScopeMatch {
+        DNS_SCOPE_NO,
+        DNS_SCOPE_MAYBE,
+        DNS_SCOPE_YES_BASE, /* Add the number of matching labels to this */
+        DNS_SCOPE_YES_END = DNS_SCOPE_YES_BASE + DNS_N_LABELS_MAX,
+        _DNS_SCOPE_MATCH_MAX,
+        _DNS_SCOPE_MATCH_INVALID = -EINVAL,
+} DnsScopeMatch;
+
+struct DnsScope {
+        Manager *manager;
+
+        DnsProtocol protocol;
+        int family;
+
+        /* Copied at scope creation time from the link/manager */
+        DnssecMode dnssec_mode;
+        DnsOverTlsMode dns_over_tls_mode;
+
+        Link *link;
+
+        DnsCache cache;
+        DnsZone zone;
+
+        OrderedHashmap *conflict_queue;
+        sd_event_source *conflict_event_source;
+
+        sd_event_source *announce_event_source;
+
+        RateLimit ratelimit;
+
+        usec_t resend_timeout;
+        usec_t max_rtt;
+
+        LIST_HEAD(DnsQueryCandidate, query_candidates);
+
+        /* Note that we keep track of ongoing transactions in two ways: once in a hashmap, indexed by the rr
+         * key, and once in a linked list. We use the hashmap to quickly find transactions we can reuse for a
+         * key. But note that there might be multiple transactions for the same key (because the associated
+         * query flags might differ in incompatible ways: e.g. we may not reuse a non-validating transaction
+         * as validating. Hence we maintain a per-key list of transactions, which we iterate through to find
+         * one we can reuse with matching flags. */
+        Hashmap *transactions_by_key;
+        LIST_HEAD(DnsTransaction, transactions);
+
+        LIST_FIELDS(DnsScope, scopes);
+
+        bool announced;
+};
+
+int dns_scope_new(Manager *m, DnsScope **ret, Link *l, DnsProtocol p, int family);
+DnsScope* dns_scope_free(DnsScope *s);
+
+void dns_scope_packet_received(DnsScope *s, usec_t rtt);
+void dns_scope_packet_lost(DnsScope *s, usec_t usec);
+
+int dns_scope_emit_udp(DnsScope *s, int fd, int af, DnsPacket *p);
+int dns_scope_socket_tcp(DnsScope *s, int family, const union in_addr_union *address, DnsServer *server, uint16_t port, union sockaddr_union *ret_socket_address);
+int dns_scope_socket_udp(DnsScope *s, DnsServer *server);
+
+DnsScopeMatch dns_scope_good_domain(DnsScope *s, DnsQuery *q);
+bool dns_scope_good_key(DnsScope *s, const DnsResourceKey *key);
+
+DnsServer *dns_scope_get_dns_server(DnsScope *s);
+unsigned dns_scope_get_n_dns_servers(DnsScope *s);
+void dns_scope_next_dns_server(DnsScope *s, DnsServer *if_current);
+
+int dns_scope_llmnr_membership(DnsScope *s, bool b);
+int dns_scope_mdns_membership(DnsScope *s, bool b);
+
+int dns_scope_make_reply_packet(DnsScope *s, uint16_t id, int rcode, DnsQuestion *q, DnsAnswer *answer, DnsAnswer *soa, bool tentative, DnsPacket **ret);
+void dns_scope_process_query(DnsScope *s, DnsStream *stream, DnsPacket *p);
+
+DnsTransaction *dns_scope_find_transaction(DnsScope *scope, DnsResourceKey *key, uint64_t query_flags);
+
+int dns_scope_notify_conflict(DnsScope *scope, DnsResourceRecord *rr);
+void dns_scope_check_conflicts(DnsScope *scope, DnsPacket *p);
+
+void dns_scope_dump(DnsScope *s, FILE *f);
+
+DnsSearchDomain *dns_scope_get_search_domains(DnsScope *s);
+
+bool dns_scope_name_wants_search_domain(DnsScope *s, const char *name);
+
+bool dns_scope_network_good(DnsScope *s);
+
+int dns_scope_ifindex(DnsScope *s);
+
+int dns_scope_announce(DnsScope *scope, bool goodbye);
+
+int dns_scope_add_dnssd_services(DnsScope *scope);
+int dns_scope_remove_dnssd_services(DnsScope *scope);
+
+bool dns_scope_is_default_route(DnsScope *scope);
diff --git a/src/resolve/resolved-dns-search-domain.c b/src/resolve/resolved-dns-search-domain.c
new file mode 100644
index 0000000..94a4657
--- /dev/null
+++ b/src/resolve/resolved-dns-search-domain.c
@@ -0,0 +1,200 @@
+/* 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);
+}
+
+bool dns_search_domain_unlink_marked(DnsSearchDomain *first) {
+        DnsSearchDomain *next;
+        bool changed;
+
+        if (!first)
+                return false;
+
+        next = first->domains_next;
+
+        if (first->marked) {
+                dns_search_domain_unlink(first);
+                changed = true;
+        } else
+                changed = false;
+
+        return changed || 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..f0d96ac
--- /dev/null
+++ b/src/resolve/resolved-dns-search-domain.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "list.h"
+#include "macro.h"
+
+typedef struct DnsSearchDomain DnsSearchDomain;
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+typedef enum DnsSearchDomainType {
+        DNS_SEARCH_DOMAIN_SYSTEM,
+        DNS_SEARCH_DOMAIN_LINK,
+} DnsSearchDomainType;
+
+struct DnsSearchDomain {
+        Manager *manager;
+
+        unsigned n_ref;
+
+        DnsSearchDomainType type;
+        Link *link;
+
+        char *name;
+
+        bool marked:1;
+        bool route_only:1;
+
+        bool linked:1;
+        LIST_FIELDS(DnsSearchDomain, domains);
+};
+
+int dns_search_domain_new(
+                Manager *m,
+                DnsSearchDomain **ret,
+                DnsSearchDomainType type,
+                Link *link,
+                const char *name);
+
+DnsSearchDomain* dns_search_domain_ref(DnsSearchDomain *d);
+DnsSearchDomain* dns_search_domain_unref(DnsSearchDomain *d);
+
+void dns_search_domain_unlink(DnsSearchDomain *d);
+void dns_search_domain_move_back_and_unmark(DnsSearchDomain *d);
+
+void dns_search_domain_unlink_all(DnsSearchDomain *first);
+bool dns_search_domain_unlink_marked(DnsSearchDomain *first);
+void dns_search_domain_mark_all(DnsSearchDomain *first);
+
+int dns_search_domain_find(DnsSearchDomain *first, const char *name, DnsSearchDomain **ret);
+
+static inline const char* DNS_SEARCH_DOMAIN_NAME(DnsSearchDomain *d) {
+        return d ? d->name : NULL;
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsSearchDomain*, dns_search_domain_unref);
diff --git a/src/resolve/resolved-dns-server.c b/src/resolve/resolved-dns-server.c
new file mode 100644
index 0000000..a21148d
--- /dev/null
+++ b/src/resolve/resolved-dns-server.c
@@ -0,0 +1,1086 @@
+/* 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->packet_invalid = false;
+        s->verified_usec = 0;
+
+        /* Note that we do not reset s->packet_bad_opt and s->packet_rrsig_missing here. We reset them only when the
+         * grace period ends, but not when lowering the possible feature level, as a lower level feature level should
+         * not make RRSIGs appear or OPT appear, but rather make them disappear. If the reappear anyway, then that's
+         * indication for a differently broken OPT/RRSIG implementation, and we really don't want to support that
+         * either.
+         *
+         * This is particularly important to deal with certain Belkin routers which break OPT for certain lookups (A),
+         * but pass traffic through for others (AAAA). If we detect the broken behaviour on one lookup we should not
+         * re-enable it for another, because we cannot validate things anyway, given that the RRSIG/OPT data will be
+         * incomplete. */
+}
+
+void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t fragsize) {
+        assert(s);
+
+        if (protocol == IPPROTO_UDP) {
+                if (s->possible_feature_level == level)
+                        s->n_failed_udp = 0;
+        } else if (protocol == IPPROTO_TCP) {
+                if (DNS_SERVER_FEATURE_LEVEL_IS_TLS(level)) {
+                        if (s->possible_feature_level == level)
+                                s->n_failed_tls = 0;
+                } else {
+                        if (s->possible_feature_level == level)
+                                s->n_failed_tcp = 0;
+
+                        /* Successful TCP connections are only useful to verify the TCP feature level. */
+                        level = DNS_SERVER_FEATURE_LEVEL_TCP;
+                }
+        }
+
+        /* If the RRSIG data is missing, then we can only validate EDNS0 at max */
+        if (s->packet_rrsig_missing && level >= DNS_SERVER_FEATURE_LEVEL_DO)
+                level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN : DNS_SERVER_FEATURE_LEVEL_EDNS0;
+
+        /* If the OPT RR got lost, then we can only validate UDP at max */
+        if (s->packet_bad_opt && level >= DNS_SERVER_FEATURE_LEVEL_EDNS0)
+                level = DNS_SERVER_FEATURE_LEVEL_EDNS0 - 1;
+
+        dns_server_verified(s, level);
+
+        /* Remember the size of the largest UDP packet fragment we received from a server, we know that we
+         * can always announce support for packets with at least this size. */
+        if (protocol == IPPROTO_UDP && s->received_udp_fragment_max < fragsize)
+                s->received_udp_fragment_max = fragsize;
+}
+
+void dns_server_packet_lost(DnsServer *s, int protocol, DnsServerFeatureLevel level) {
+        assert(s);
+        assert(s->manager);
+
+        if (s->possible_feature_level != level)
+                return;
+
+        if (protocol == IPPROTO_UDP)
+                s->n_failed_udp++;
+        else if (protocol == IPPROTO_TCP) {
+                if (DNS_SERVER_FEATURE_LEVEL_IS_TLS(level))
+                        s->n_failed_tls++;
+                else
+                        s->n_failed_tcp++;
+        }
+}
+
+void dns_server_packet_truncated(DnsServer *s, DnsServerFeatureLevel level) {
+        assert(s);
+
+        /* Invoked whenever we get a packet with TC bit set. */
+
+        if (s->possible_feature_level != level)
+                return;
+
+        s->packet_truncated = true;
+}
+
+void dns_server_packet_rrsig_missing(DnsServer *s, DnsServerFeatureLevel level) {
+        assert(s);
+
+        if (level < DNS_SERVER_FEATURE_LEVEL_DO)
+                return;
+
+        /* If the RRSIG RRs are missing, we have to downgrade what we previously verified */
+        if (s->verified_feature_level >= DNS_SERVER_FEATURE_LEVEL_DO)
+                s->verified_feature_level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN : DNS_SERVER_FEATURE_LEVEL_EDNS0;
+
+        s->packet_rrsig_missing = true;
+}
+
+void dns_server_packet_bad_opt(DnsServer *s, DnsServerFeatureLevel level) {
+        assert(s);
+
+        if (level < DNS_SERVER_FEATURE_LEVEL_EDNS0)
+                return;
+
+        /* If the OPT RR got lost, we have to downgrade what we previously verified */
+        if (s->verified_feature_level >= DNS_SERVER_FEATURE_LEVEL_EDNS0)
+                s->verified_feature_level = DNS_SERVER_FEATURE_LEVEL_EDNS0-1;
+
+        s->packet_bad_opt = true;
+}
+
+void dns_server_packet_rcode_downgrade(DnsServer *s, DnsServerFeatureLevel level) {
+        assert(s);
+
+        /* Invoked whenever we got a FORMERR, SERVFAIL or NOTIMP rcode from a server and downgrading the feature level
+         * for the transaction made it go away. In this case we immediately downgrade to the feature level that made
+         * things work. */
+
+        if (s->verified_feature_level > level)
+                s->verified_feature_level = level;
+
+        if (s->possible_feature_level > level) {
+                s->possible_feature_level = level;
+                dns_server_reset_counters(s);
+                log_debug("Downgrading transaction feature level fixed an RCODE error, downgrading server %s too.", strna(dns_server_string_full(s)));
+        }
+}
+
+void dns_server_packet_invalid(DnsServer *s, DnsServerFeatureLevel level) {
+        assert(s);
+
+        /* Invoked whenever we got a packet we couldn't parse at all */
+
+        if (s->possible_feature_level != level)
+                return;
+
+        s->packet_invalid = true;
+}
+
+void dns_server_packet_do_off(DnsServer *s, DnsServerFeatureLevel level) {
+        assert(s);
+
+        /* Invoked whenever the DO flag was not copied from our request to the response. */
+
+        if (s->possible_feature_level != level)
+                return;
+
+        s->packet_do_off = true;
+}
+
+void dns_server_packet_udp_fragmented(DnsServer *s, size_t fragsize) {
+        assert(s);
+
+        /* Invoked whenever we got a fragmented UDP packet. Let's do two things: keep track of the largest
+         * fragment we ever received from the server, and remember this, so that we can use it to lower the
+         * advertised packet size in EDNS0 */
+
+        if (s->received_udp_fragment_max < fragsize)
+                s->received_udp_fragment_max = fragsize;
+
+        s->packet_fragmented = true;
+}
+
+static bool dns_server_grace_period_expired(DnsServer *s) {
+        usec_t ts;
+
+        assert(s);
+        assert(s->manager);
+
+        if (s->verified_usec == 0)
+                return false;
+
+        assert_se(sd_event_now(s->manager->event, clock_boottime_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_DO :
+                        DNS_SERVER_FEATURE_LEVEL_TLS_DO;
+        else
+                best = dns_server_get_dns_over_tls_mode(s) == DNS_OVER_TLS_NO ?
+                        DNS_SERVER_FEATURE_LEVEL_EDNS0 :
+                        DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN;
+
+        /* Clamp the feature level the highest level we care about. The DNSSEC mode might have changed since the last
+         * time, hence let's downgrade if we are still at a higher level. */
+        if (s->possible_feature_level > best)
+                s->possible_feature_level = best;
+
+        if (s->possible_feature_level < best && dns_server_grace_period_expired(s)) {
+
+                s->possible_feature_level = best;
+
+                dns_server_reset_counters(s);
+
+                s->packet_bad_opt = false;
+                s->packet_rrsig_missing = false;
+
+                log_info("Grace period over, resuming full feature set (%s) for DNS server %s.",
+                         dns_server_feature_level_to_string(s->possible_feature_level),
+                         strna(dns_server_string_full(s)));
+
+                dns_server_flush_cache(s);
+
+        } else if (s->possible_feature_level <= s->verified_feature_level)
+                s->possible_feature_level = s->verified_feature_level;
+        else {
+                DnsServerFeatureLevel p = s->possible_feature_level;
+                int log_level = LOG_WARNING;
+
+                if (s->n_failed_tcp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS &&
+                    s->possible_feature_level == DNS_SERVER_FEATURE_LEVEL_TCP) {
+
+                        /* We are at the TCP (lowest) level, and we tried a couple of TCP connections, and it didn't
+                         * work. Upgrade back to UDP again. */
+                        log_debug("Reached maximum number of failed TCP connection attempts, trying UDP again...");
+                        s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_UDP;
+
+                } else if (s->n_failed_tls > 0 &&
+                           DNS_SERVER_FEATURE_LEVEL_IS_TLS(s->possible_feature_level) &&
+                           dns_server_get_dns_over_tls_mode(s) != DNS_OVER_TLS_YES) {
+
+                        /* We tried to connect using DNS-over-TLS, and it didn't work. Downgrade to plaintext UDP
+                         * if we don't require DNS-over-TLS */
+
+                        log_debug("Server doesn't support DNS-over-TLS, downgrading protocol...");
+                        s->possible_feature_level--;
+
+                } else if (s->packet_invalid &&
+                           s->possible_feature_level > DNS_SERVER_FEATURE_LEVEL_UDP &&
+                           s->possible_feature_level != DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN) {
+
+                        /* Downgrade from DO to EDNS0 + from EDNS0 to UDP, from TLS+DO to plain TLS. Or in
+                         * other words, if we receive a packet we cannot parse jump to the next lower feature
+                         * level that actually has an influence on the packet layout (and not just the
+                         * transport). */
+
+                        log_debug("Got invalid packet from server, downgrading protocol...");
+                        s->possible_feature_level =
+                                s->possible_feature_level == DNS_SERVER_FEATURE_LEVEL_TLS_DO  ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN :
+                                DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) ? DNS_SERVER_FEATURE_LEVEL_EDNS0 :
+                                                                                                DNS_SERVER_FEATURE_LEVEL_UDP;
+
+                } else if (s->packet_bad_opt &&
+                           DNS_SERVER_FEATURE_LEVEL_IS_EDNS0(s->possible_feature_level) &&
+                           dns_server_get_dnssec_mode(s) != DNSSEC_YES &&
+                           dns_server_get_dns_over_tls_mode(s) != DNS_OVER_TLS_YES) {
+
+                        /* A reply to one of our EDNS0 queries didn't carry a valid OPT RR, then downgrade to
+                         * below EDNS0 levels. After all, some servers generate different responses with and
+                         * without OPT RR in the request. Example:
+                         *
+                         * https://open.nlnetlabs.nl/pipermail/dnssec-trigger/2014-November/000376.html
+                         *
+                         * If we are in strict DNSSEC or DoT mode, we don't do this kind of downgrade
+                         * however, as both modes imply EDNS0 to work (DNSSEC strictly requires it, and DoT
+                         * only in our implementation). */
+
+                        log_debug("Server doesn't support EDNS(0) properly, downgrading feature level...");
+                        s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_UDP;
+
+                        /* Users often don't control the DNS server they use so let's not complain too loudly
+                         * when we can't use EDNS because the DNS server doesn't support it. */
+                        log_level = LOG_NOTICE;
+
+                } else if (s->packet_do_off &&
+                           DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) &&
+                           dns_server_get_dnssec_mode(s) != DNSSEC_YES) {
+
+                        /* The server didn't copy the DO bit from request to response, thus DNSSEC is not
+                         * correctly implemented, let's downgrade if that's allowed. */
+
+                        log_debug("Detected server didn't copy DO flag from request to response, downgrading feature level...");
+                        s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(s->possible_feature_level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN :
+                                                                                                                 DNS_SERVER_FEATURE_LEVEL_EDNS0;
+
+                } else if (s->packet_rrsig_missing &&
+                           DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) &&
+                           dns_server_get_dnssec_mode(s) != DNSSEC_YES) {
+
+                        /* RRSIG data was missing on an EDNS0 packet with DO bit set. This means the server
+                         * doesn't augment responses with DNSSEC RRs. If so, let's better not ask the server
+                         * for it anymore, after all some servers generate different replies depending if an
+                         * OPT RR is in the query or not. If we are in strict DNSSEC mode, don't allow such
+                         * downgrades however, since a DNSSEC feature level is a requirement for strict
+                         * DNSSEC mode. */
+
+                        log_debug("Detected server responses lack RRSIG records, downgrading feature level...");
+                        s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_IS_TLS(s->possible_feature_level) ? DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN :
+                                                                                                                 DNS_SERVER_FEATURE_LEVEL_EDNS0;
+
+                } else if (s->n_failed_udp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS &&
+                           DNS_SERVER_FEATURE_LEVEL_IS_UDP(s->possible_feature_level) &&
+                           ((s->possible_feature_level != DNS_SERVER_FEATURE_LEVEL_DO) || dns_server_get_dnssec_mode(s) != DNSSEC_YES)) {
+
+                        /* We lost too many UDP packets in a row, and are on an UDP feature level. If the
+                         * packets are lost, maybe the server cannot parse them, hence downgrading sounds
+                         * like a good idea. We might downgrade all the way down to TCP this way.
+                         *
+                         * If strict DNSSEC mode is used we won't downgrade below DO level however, as packet loss
+                         * might have many reasons, a broken DNSSEC implementation being only one reason. And if the
+                         * user is strict on DNSSEC, then let's assume that DNSSEC is not the fault here. */
+
+                        log_debug("Lost too many UDP packets, downgrading feature level...");
+                        if (s->possible_feature_level == DNS_SERVER_FEATURE_LEVEL_DO) /* skip over TLS_PLAIN */
+                                s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_EDNS0;
+                        else
+                                s->possible_feature_level--;
+
+                } else if (s->n_failed_tcp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS &&
+                           s->packet_truncated &&
+                           s->possible_feature_level > DNS_SERVER_FEATURE_LEVEL_UDP &&
+                           DNS_SERVER_FEATURE_LEVEL_IS_UDP(s->possible_feature_level) &&
+                           (!DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level) || dns_server_get_dnssec_mode(s) != DNSSEC_YES)) {
+
+                         /* We got too many TCP connection failures in a row, we had at least one truncated
+                          * packet, and are on feature level above UDP. By downgrading things and getting rid
+                          * of DNSSEC or EDNS0 data we hope to make the packet smaller, so that it still
+                          * works via UDP given that TCP appears not to be a fallback. Note that if we are
+                          * already at the lowest UDP level, we don't go further down, since that's TCP, and
+                          * TCP failed too often after all. */
+
+                        log_debug("Got too many failed TCP connection failures and truncated UDP packets, downgrading feature level...");
+
+                        if (DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(s->possible_feature_level))
+                                s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_EDNS0; /* Go DNSSEC → EDNS0 */
+                        else
+                                s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_UDP; /* Go EDNS0 → UDP */
+                }
+
+                if (p != s->possible_feature_level) {
+
+                        /* We changed the feature level, reset the counting */
+                        dns_server_reset_counters(s);
+
+                        log_full(log_level, "Using degraded feature set %s instead of %s for DNS server %s.",
+                                 dns_server_feature_level_to_string(s->possible_feature_level),
+                                 dns_server_feature_level_to_string(p), strna(dns_server_string_full(s)));
+                }
+        }
+
+        return s->possible_feature_level;
+}
+
+int dns_server_adjust_opt(DnsServer *server, DnsPacket *packet, DnsServerFeatureLevel level) {
+        size_t packet_size, udp_size;
+        bool edns_do;
+        int r;
+
+        assert(server);
+        assert(packet);
+        assert(packet->protocol == DNS_PROTOCOL_DNS);
+
+        /* Fix the OPT field in the packet to match our current feature level. */
+
+        r = dns_packet_truncate_opt(packet);
+        if (r < 0)
+                return r;
+
+        if (level < DNS_SERVER_FEATURE_LEVEL_EDNS0)
+                return 0;
+
+        edns_do = level >= DNS_SERVER_FEATURE_LEVEL_DO;
+
+        udp_size = udp_header_size(server->family);
+
+        if (in_addr_is_localhost(server->family, &server->address) > 0)
+                packet_size = 65536 - udp_size; /* force linux loopback MTU if localhost address */
+        else {
+                /* Use the MTU pointing to the server, subtract the IP/UDP header size */
+                packet_size = LESS_BY(dns_server_get_mtu(server), udp_size);
+
+                /* On the Internet we want to avoid fragmentation for security reasons. If we saw
+                 * fragmented packets, the above was too large, let's clamp it to the largest
+                 * fragment we saw */
+                if (server->packet_fragmented)
+                        packet_size = MIN(server->received_udp_fragment_max, packet_size);
+
+                /* Let's not pick ridiculously large sizes, i.e. not more than 4K. No one appears
+                 * to ever use such large sized on the Internet IRL, hence let's not either. */
+                packet_size = MIN(packet_size, 4096U);
+        }
+
+        /* Strictly speaking we quite possibly can receive larger datagrams than the MTU (since the
+         * MTU is for egress, not for ingress), but more often than not the value is symmetric, and
+         * we want something that does the right thing in the majority of cases, and not just in the
+         * theoretical edge case. */
+
+        /* Safety clamp, never advertise less than 512 or more than 65535 */
+        packet_size = CLAMP(packet_size,
+                            DNS_PACKET_UNICAST_SIZE_MAX,
+                            DNS_PACKET_SIZE_MAX);
+
+        log_debug("Announcing packet size %zu in egress EDNS(0) packet.", packet_size);
+
+        return dns_packet_append_opt(packet, packet_size, edns_do, /* include_rfc6975 = */ true, NULL, 0, NULL);
+}
+
+int dns_server_ifindex(const DnsServer *s) {
+        assert(s);
+
+        /* The link ifindex always takes precedence */
+        if (s->link)
+                return s->link->ifindex;
+
+        if (s->ifindex > 0)
+                return s->ifindex;
+
+        return 0;
+}
+
+uint16_t dns_server_port(const DnsServer *s) {
+        assert(s);
+
+        if (s->port > 0)
+                return s->port;
+
+        return 53;
+}
+
+const char *dns_server_string(DnsServer *server) {
+        assert(server);
+
+        if (!server->server_string)
+                (void) in_addr_ifindex_to_string(server->family, &server->address, dns_server_ifindex(server), &server->server_string);
+
+        return server->server_string;
+}
+
+const char *dns_server_string_full(DnsServer *server) {
+        assert(server);
+
+        if (!server->server_string_full)
+                (void) in_addr_port_ifindex_name_to_string(
+                                server->family,
+                                &server->address,
+                                server->port,
+                                dns_server_ifindex(server),
+                                server->server_name,
+                                &server->server_string_full);
+
+        return server->server_string_full;
+}
+
+bool dns_server_dnssec_supported(DnsServer *server) {
+        assert(server);
+
+        /* Returns whether the server supports DNSSEC according to what we know about it */
+
+        if (dns_server_get_dnssec_mode(server) == DNSSEC_YES) /* If strict DNSSEC mode is enabled, always assume DNSSEC mode is supported. */
+                return true;
+
+        if (!DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(server->possible_feature_level))
+                return false;
+
+        if (server->packet_bad_opt)
+                return false;
+
+        if (server->packet_rrsig_missing)
+                return false;
+
+        if (server->packet_do_off)
+                return false;
+
+        /* DNSSEC servers need to support TCP properly (see RFC5966), if they don't, we assume DNSSEC is borked too */
+        if (server->n_failed_tcp >= DNS_SERVER_FEATURE_RETRY_ATTEMPTS)
+                return false;
+
+        return true;
+}
+
+void dns_server_warn_downgrade(DnsServer *server) {
+        assert(server);
+
+        if (server->warned_downgrade)
+                return;
+
+        log_struct(LOG_NOTICE,
+                   "MESSAGE_ID=" SD_MESSAGE_DNSSEC_DOWNGRADE_STR,
+                   LOG_MESSAGE("Server %s does not support DNSSEC, downgrading to non-DNSSEC mode.", strna(dns_server_string_full(server))),
+                   "DNS_SERVER=%s", strna(dns_server_string_full(server)),
+                   "DNS_SERVER_FEATURE_LEVEL=%s", dns_server_feature_level_to_string(server->possible_feature_level));
+
+        server->warned_downgrade = true;
+}
+
+size_t dns_server_get_mtu(DnsServer *s) {
+        assert(s);
+
+        if (s->link && s->link->mtu != 0)
+                return s->link->mtu;
+
+        return manager_find_mtu(s->manager);
+}
+
+static void dns_server_hash_func(const DnsServer *s, struct siphash *state) {
+        assert(s);
+
+        siphash24_compress(&s->family, sizeof(s->family), state);
+        siphash24_compress(&s->address, FAMILY_ADDRESS_SIZE(s->family), state);
+        siphash24_compress(&s->port, sizeof(s->port), state);
+        siphash24_compress(&s->ifindex, sizeof(s->ifindex), state);
+        siphash24_compress_string(s->server_name, state);
+}
+
+static int dns_server_compare_func(const DnsServer *x, const DnsServer *y) {
+        int r;
+
+        r = CMP(x->family, y->family);
+        if (r != 0)
+                return r;
+
+        r = memcmp(&x->address, &y->address, FAMILY_ADDRESS_SIZE(x->family));
+        if (r != 0)
+                return r;
+
+        r = CMP(x->port, y->port);
+        if (r != 0)
+                return r;
+
+        r = CMP(x->ifindex, y->ifindex);
+        if (r != 0)
+                return r;
+
+        return streq_ptr(x->server_name, y->server_name);
+}
+
+DEFINE_HASH_OPS(dns_server_hash_ops, DnsServer, dns_server_hash_func, dns_server_compare_func);
+
+void dns_server_unlink_all(DnsServer *first) {
+        DnsServer *next;
+
+        if (!first)
+                return;
+
+        next = first->servers_next;
+        dns_server_unlink(first);
+
+        dns_server_unlink_all(next);
+}
+
+bool dns_server_unlink_marked(DnsServer *server) {
+        bool changed = false;
+
+        while (server) {
+                DnsServer *next;
+
+                next = server->servers_next;
+
+                if (server->marked) {
+                        dns_server_unlink(server);
+                        changed = true;
+                }
+
+                server = next;
+        }
+
+        return changed;
+}
+
+void dns_server_mark_all(DnsServer *server) {
+        while (server) {
+                server->marked = true;
+                server = server->servers_next;
+        }
+}
+
+DnsServer *dns_server_find(DnsServer *first, int family, const union in_addr_union *in_addr, uint16_t port, int ifindex, const char *name) {
+        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;
+
+        /* Let's log about the server switch, at debug level. Except if we switch from a non-fallback server
+         * to a fallback server or back, since that is noteworthy and possibly a configuration issue */
+        if (s)
+                log_full((s->type == DNS_SERVER_FALLBACK) != (m->current_dns_server && m->current_dns_server->type == DNS_SERVER_FALLBACK) ? LOG_NOTICE : LOG_DEBUG,
+                         "Switching to %s DNS server %s.", dns_server_type_to_string(s->type), strna(dns_server_string_full(s)));
+
+        dns_server_unref(m->current_dns_server);
+        m->current_dns_server = dns_server_ref(s);
+
+        if (m->unicast_scope)
+                dns_cache_flush(&m->unicast_scope->cache);
+
+        (void) manager_send_changed(m, "CurrentDNSServer");
+
+        return s;
+}
+
+DnsServer *manager_get_dns_server(Manager *m) {
+        Link *l;
+        assert(m);
+
+        /* Try to read updates resolv.conf */
+        manager_read_resolv_conf(m);
+
+        /* If no DNS server was chosen so far, pick the first one */
+        if (!m->current_dns_server)
+                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, DnsServer *if_current) {
+        assert(m);
+
+        /* If the DNS server is already a different one than the one specified in 'if_current' don't do anything */
+        if (if_current && m->current_dns_server != if_current)
+                return;
+
+        /* If there's currently no DNS server set, then the next manager_get_dns_server() will find one */
+        if (!m->current_dns_server)
+                return;
+
+        /* Change to the next one, but make sure to follow the linked list only if the server is still
+         * linked. */
+        if (m->current_dns_server->linked && m->current_dns_server->servers_next) {
+                manager_set_dns_server(m, m->current_dns_server->servers_next);
+                return;
+        }
+
+        /* If there was no next one, then start from the beginning of the list */
+        if (m->current_dns_server->type == DNS_SERVER_FALLBACK)
+                manager_set_dns_server(m, m->fallback_dns_servers);
+        else
+                manager_set_dns_server(m, m->dns_servers);
+}
+
+DnssecMode dns_server_get_dnssec_mode(DnsServer *s) {
+        assert(s);
+
+        if (s->link)
+                return link_get_dnssec_mode(s->link);
+
+        return manager_get_dnssec_mode(s->manager);
+}
+
+DnsOverTlsMode dns_server_get_dns_over_tls_mode(DnsServer *s) {
+        assert(s);
+
+        if (s->link)
+                return link_get_dns_over_tls_mode(s->link);
+
+        return manager_get_dns_over_tls_mode(s->manager);
+}
+
+void dns_server_flush_cache(DnsServer *s) {
+        DnsServer *current;
+        DnsScope *scope;
+
+        assert(s);
+
+        /* Flush the cache of the scope this server belongs to */
+
+        current = s->link ? s->link->current_dns_server : s->manager->current_dns_server;
+        if (current != s)
+                return;
+
+        scope = s->link ? s->link->unicast_scope : s->manager->unicast_scope;
+        if (!scope)
+                return;
+
+        dns_cache_flush(&scope->cache);
+}
+
+void dns_server_reset_features(DnsServer *s) {
+        assert(s);
+
+        s->verified_feature_level = _DNS_SERVER_FEATURE_LEVEL_INVALID;
+        s->possible_feature_level = DNS_SERVER_FEATURE_LEVEL_BEST;
+
+        s->received_udp_fragment_max = DNS_PACKET_UNICAST_SIZE_MAX;
+
+        s->packet_bad_opt = false;
+        s->packet_rrsig_missing = false;
+        s->packet_do_off = false;
+
+        s->features_grace_period_usec = DNS_SERVER_FEATURE_GRACE_PERIOD_MIN_USEC;
+
+        s->warned_downgrade = false;
+
+        dns_server_reset_counters(s);
+
+        /* Let's close the default stream, so that we reprobe with the new features */
+        dns_server_unref_stream(s);
+}
+
+void dns_server_reset_features_all(DnsServer *s) {
+        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 fragment size received: %zu\n"
+                "\tFailed UDP attempts: %u\n"
+                "\tFailed TCP attempts: %u\n"
+                "\tSeen truncated packet: %s\n"
+                "\tSeen OPT RR getting lost: %s\n"
+                "\tSeen RRSIG RR missing: %s\n"
+                "\tSeen invalid packet: %s\n"
+                "\tServer dropped DO flag: %s\n",
+                s->received_udp_fragment_max,
+                s->n_failed_udp,
+                s->n_failed_tcp,
+                yes_no(s->packet_truncated),
+                yes_no(s->packet_bad_opt),
+                yes_no(s->packet_rrsig_missing),
+                yes_no(s->packet_invalid),
+                yes_no(s->packet_do_off));
+}
+
+void dns_server_unref_stream(DnsServer *s) {
+        DnsStream *ref;
+
+        assert(s);
+
+        /* Detaches the default stream of this server. Some special care needs to be taken here, as that stream and
+         * this server reference each other. First, take the stream out of the server. It's destructor will check if it
+         * is registered with us, hence let's invalidate this separately, so that it is already unregistered. */
+        ref = TAKE_PTR(s->stream);
+
+        /* And then, unref it */
+        dns_stream_unref(ref);
+}
+
+DnsScope *dns_server_scope(DnsServer *s) {
+        assert(s);
+        assert((s->type == DNS_SERVER_LINK) == !!s->link);
+
+        if (s->link)
+                return s->link->unicast_scope;
+
+        return s->manager->unicast_scope;
+}
+
+static const char* const dns_server_type_table[_DNS_SERVER_TYPE_MAX] = {
+        [DNS_SERVER_SYSTEM]   = "system",
+        [DNS_SERVER_FALLBACK] = "fallback",
+        [DNS_SERVER_LINK]     = "link",
+};
+DEFINE_STRING_TABLE_LOOKUP(dns_server_type, DnsServerType);
+
+static const char* const dns_server_feature_level_table[_DNS_SERVER_FEATURE_LEVEL_MAX] = {
+        [DNS_SERVER_FEATURE_LEVEL_TCP]       = "TCP",
+        [DNS_SERVER_FEATURE_LEVEL_UDP]       = "UDP",
+        [DNS_SERVER_FEATURE_LEVEL_EDNS0]     = "UDP+EDNS0",
+        [DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN] = "TLS+EDNS0",
+        [DNS_SERVER_FEATURE_LEVEL_DO]        = "UDP+EDNS0+DO",
+        [DNS_SERVER_FEATURE_LEVEL_TLS_DO]    = "TLS+EDNS0+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..be9efb0
--- /dev/null
+++ b/src/resolve/resolved-dns-server.h
@@ -0,0 +1,174 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "in-addr-util.h"
+#include "list.h"
+#include "resolve-util.h"
+#include "time-util.h"
+
+typedef struct DnsScope DnsScope;
+typedef struct DnsServer DnsServer;
+typedef struct DnsStream DnsStream;
+typedef struct DnsPacket DnsPacket;
+typedef struct Link Link;
+typedef struct Manager Manager;
+
+#include "resolved-dnstls.h"
+
+typedef enum DnsServerType {
+        DNS_SERVER_SYSTEM,
+        DNS_SERVER_FALLBACK,
+        DNS_SERVER_LINK,
+        _DNS_SERVER_TYPE_MAX,
+        _DNS_SERVER_TYPE_INVALID = -EINVAL,
+} DnsServerType;
+
+const char* dns_server_type_to_string(DnsServerType i) _const_;
+DnsServerType dns_server_type_from_string(const char *s) _pure_;
+
+typedef enum DnsServerFeatureLevel {
+        DNS_SERVER_FEATURE_LEVEL_TCP,
+        DNS_SERVER_FEATURE_LEVEL_UDP,
+        DNS_SERVER_FEATURE_LEVEL_EDNS0,
+        DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN,
+        DNS_SERVER_FEATURE_LEVEL_DO,
+        DNS_SERVER_FEATURE_LEVEL_TLS_DO,
+        _DNS_SERVER_FEATURE_LEVEL_MAX,
+        _DNS_SERVER_FEATURE_LEVEL_INVALID = -EINVAL,
+} DnsServerFeatureLevel;
+
+#define DNS_SERVER_FEATURE_LEVEL_WORST 0
+#define DNS_SERVER_FEATURE_LEVEL_BEST (_DNS_SERVER_FEATURE_LEVEL_MAX - 1)
+#define DNS_SERVER_FEATURE_LEVEL_IS_EDNS0(x) ((x) >= DNS_SERVER_FEATURE_LEVEL_EDNS0)
+#define DNS_SERVER_FEATURE_LEVEL_IS_TLS(x) IN_SET(x, DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN, DNS_SERVER_FEATURE_LEVEL_TLS_DO)
+#define DNS_SERVER_FEATURE_LEVEL_IS_DNSSEC(x) ((x) >= DNS_SERVER_FEATURE_LEVEL_DO)
+#define DNS_SERVER_FEATURE_LEVEL_IS_UDP(x) IN_SET(x, DNS_SERVER_FEATURE_LEVEL_UDP, DNS_SERVER_FEATURE_LEVEL_EDNS0, DNS_SERVER_FEATURE_LEVEL_DO)
+
+const char* dns_server_feature_level_to_string(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_fragment_max;   /* largest packet or fragment (without IP/UDP header) we saw so far */
+
+        unsigned n_failed_udp;
+        unsigned n_failed_tcp;
+        unsigned n_failed_tls;
+
+        bool packet_truncated:1;        /* Set when TC bit was set on reply */
+        bool packet_bad_opt:1;          /* Set when OPT was missing or otherwise bad on reply */
+        bool packet_rrsig_missing:1;    /* Set when RRSIG was missing */
+        bool packet_invalid:1;          /* Set when we failed to parse a reply */
+        bool packet_do_off:1;           /* Set when the server didn't copy DNSSEC DO flag from request to response */
+        bool packet_fragmented:1;       /* Set when we ever saw a fragmented packet */
+
+        usec_t verified_usec;
+        usec_t features_grace_period_usec;
+
+        /* Whether we already warned about downgrading to non-DNSSEC mode for this server */
+        bool warned_downgrade:1;
+
+        /* Used when GC'ing old DNS servers when configuration changes. */
+        bool marked:1;
+
+        /* If linked is set, then this server appears in the servers linked list */
+        bool linked:1;
+        LIST_FIELDS(DnsServer, servers);
+};
+
+int dns_server_new(
+                Manager *m,
+                DnsServer **ret,
+                DnsServerType type,
+                Link *link,
+                int family,
+                const union in_addr_union *address,
+                uint16_t port,
+                int ifindex,
+                const char *server_string);
+
+DnsServer* dns_server_ref(DnsServer *s);
+DnsServer* dns_server_unref(DnsServer *s);
+
+void dns_server_unlink(DnsServer *s);
+void dns_server_move_back_and_unmark(DnsServer *s);
+
+void dns_server_packet_received(DnsServer *s, int protocol, DnsServerFeatureLevel level, size_t fragsize);
+void dns_server_packet_lost(DnsServer *s, int protocol, DnsServerFeatureLevel level);
+void dns_server_packet_truncated(DnsServer *s, DnsServerFeatureLevel level);
+void dns_server_packet_rrsig_missing(DnsServer *s, DnsServerFeatureLevel level);
+void dns_server_packet_bad_opt(DnsServer *s, DnsServerFeatureLevel level);
+void dns_server_packet_rcode_downgrade(DnsServer *s, DnsServerFeatureLevel level);
+void dns_server_packet_invalid(DnsServer *s, DnsServerFeatureLevel level);
+void dns_server_packet_do_off(DnsServer *s, DnsServerFeatureLevel level);
+void dns_server_packet_udp_fragmented(DnsServer *s, size_t fragsize);
+
+DnsServerFeatureLevel dns_server_possible_feature_level(DnsServer *s);
+
+int dns_server_adjust_opt(DnsServer *server, DnsPacket *packet, DnsServerFeatureLevel level);
+
+const char *dns_server_string(DnsServer *server);
+const char *dns_server_string_full(DnsServer *server);
+int dns_server_ifindex(const DnsServer *s);
+uint16_t dns_server_port(const DnsServer *s);
+
+bool dns_server_dnssec_supported(DnsServer *server);
+
+void dns_server_warn_downgrade(DnsServer *server);
+
+DnsServer *dns_server_find(DnsServer *first, int family, const union in_addr_union *in_addr, uint16_t port, int ifindex, const char *name);
+
+void dns_server_unlink_all(DnsServer *first);
+bool dns_server_unlink_marked(DnsServer *first);
+void dns_server_mark_all(DnsServer *first);
+
+DnsServer *manager_get_first_dns_server(Manager *m, DnsServerType t);
+
+DnsServer *manager_set_dns_server(Manager *m, DnsServer *s);
+DnsServer *manager_get_dns_server(Manager *m);
+void manager_next_dns_server(Manager *m, DnsServer *if_current);
+
+DnssecMode dns_server_get_dnssec_mode(DnsServer *s);
+DnsOverTlsMode dns_server_get_dns_over_tls_mode(DnsServer *s);
+
+size_t dns_server_get_mtu(DnsServer *s);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsServer*, dns_server_unref);
+
+extern const struct hash_ops dns_server_hash_ops;
+
+void dns_server_flush_cache(DnsServer *s);
+
+void dns_server_reset_features(DnsServer *s);
+void dns_server_reset_features_all(DnsServer *s);
+
+void dns_server_dump(DnsServer *s, FILE *f);
+
+void dns_server_unref_stream(DnsServer *s);
+
+DnsScope *dns_server_scope(DnsServer *s);
diff --git a/src/resolve/resolved-dns-stream.c b/src/resolve/resolved-dns-stream.c
new file mode 100644
index 0000000..3e6505c
--- /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_disable_unref(s->io_event_source);
+        s->timeout_event_source = sd_event_source_disable_unref(s->timeout_event_source);
+        s->fd = safe_close(s->fd);
+
+        /* Disconnect us from the server object if we are now not usable anymore */
+        dns_stream_detach(s);
+}
+
+static int dns_stream_update_io(DnsStream *s) {
+        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, sockaddr_in_addr(&s->local.sa));
+
+        if (s->protocol == DNS_PROTOCOL_LLMNR && s->ifindex > 0) {
+                /* Make sure all packets for this connection are sent on the same interface */
+                r = socket_set_unicast_if(s->fd, s->local.sa.sa_family, s->ifindex);
+                if (r < 0)
+                        log_debug_errno(errno, "Failed to invoke IP_UNICAST_IF/IPV6_UNICAST_IF: %m");
+        }
+
+        s->identified = true;
+
+        return 0;
+}
+
+ssize_t dns_stream_writev(DnsStream *s, const struct iovec *iov, size_t iovcnt, int flags) {
+        ssize_t m;
+
+        assert(s);
+        assert(iov);
+
+#if ENABLE_DNS_OVER_TLS
+        if (s->encrypted && !(flags & DNS_STREAM_WRITE_TLS_DATA)) {
+                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[] = {
+                        IOVEC_MAKE(&s->write_size, sizeof(s->write_size)),
+                        IOVEC_MAKE(DNS_PACKET_DATA(s->write_packet), s->write_packet->size),
+                };
+
+                IOVEC_INCREMENT(iov, ELEMENTSOF(iov), s->n_written);
+
+                ssize_t ss = dns_stream_writev(s, iov, ELEMENTSOF(iov), 0);
+                if (ss < 0) {
+                        if (!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;
+                                        s->read_packet->timestamp = now(clock_boottime_or_monotonic());
+
+                                        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..470d446
--- /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 = -EINVAL,
+} DnsStreamType;
+
+#define DNS_STREAM_WRITE_TLS_DATA 1
+
+/* Streams are used by three subsystems:
+ *
+ *   1. The normal transaction logic when doing a DNS or LLMNR lookup via TCP
+ *   2. The LLMNR logic when accepting a TCP-based lookup
+ *   3. The DNS stub logic when accepting a TCP-based lookup
+ */
+
+struct DnsStream {
+        Manager *manager;
+        unsigned n_ref;
+
+        DnsStreamType type;
+        DnsProtocol protocol;
+
+        int fd;
+        union sockaddr_union peer;
+        socklen_t peer_salen;
+        union sockaddr_union local;
+        socklen_t local_salen;
+        int ifindex;
+        uint32_t ttl;
+        bool identified;
+
+        /* 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..400e741
--- /dev/null
+++ b/src/resolve/resolved-dns-stub.c
@@ -0,0 +1,1310 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if_arp.h>
+#include <netinet/tcp.h>
+
+#include "errno-util.h"
+#include "fd-util.h"
+#include "missing_network.h"
+#include "missing_socket.h"
+#include "resolved-dns-stub.h"
+#include "socket-netlink.h"
+#include "socket-util.h"
+#include "stdio-util.h"
+#include "string-table.h"
+
+/* The MTU of the loopback device is 64K on Linux, advertise that as maximum datagram size, but subtract the Ethernet,
+ * IP and UDP header sizes */
+#define ADVERTISE_DATAGRAM_SIZE_MAX (65536U-14U-20U-8U)
+
+/* On the extra stubs, use a more conservative choice */
+#define ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX DNS_PACKET_UNICAST_SIZE_LARGE_MAX
+
+static int manager_dns_stub_fd_extra(Manager *m, DnsStubListenerExtra *l, int type);
+
+static void dns_stub_listener_extra_hash_func(const DnsStubListenerExtra *a, struct siphash *state) {
+        assert(a);
+
+        siphash24_compress(&a->mode, sizeof(a->mode), state);
+        siphash24_compress(&a->family, sizeof(a->family), state);
+        siphash24_compress(&a->address, FAMILY_ADDRESS_SIZE(a->family), state);
+        siphash24_compress(&a->port, sizeof(a->port), state);
+}
+
+static int dns_stub_listener_extra_compare_func(const DnsStubListenerExtra *a, const DnsStubListenerExtra *b) {
+        int r;
+
+        assert(a);
+        assert(b);
+
+        r = CMP(a->mode, b->mode);
+        if (r != 0)
+                return r;
+
+        r = CMP(a->family, b->family);
+        if (r != 0)
+                return r;
+
+        r = memcmp(&a->address, &b->address, FAMILY_ADDRESS_SIZE(a->family));
+        if (r != 0)
+                return r;
+
+        return CMP(a->port, b->port);
+}
+
+DEFINE_HASH_OPS_WITH_KEY_DESTRUCTOR(
+                dns_stub_listener_extra_hash_ops,
+                DnsStubListenerExtra,
+                dns_stub_listener_extra_hash_func,
+                dns_stub_listener_extra_compare_func,
+                dns_stub_listener_extra_free);
+
+int dns_stub_listener_extra_new(
+                Manager *m,
+                DnsStubListenerExtra **ret) {
+
+        DnsStubListenerExtra *l;
+
+        l = new(DnsStubListenerExtra, 1);
+        if (!l)
+                return -ENOMEM;
+
+        *l = (DnsStubListenerExtra) {
+                .manager = m,
+        };
+
+        *ret = TAKE_PTR(l);
+        return 0;
+}
+
+DnsStubListenerExtra *dns_stub_listener_extra_free(DnsStubListenerExtra *p) {
+        if (!p)
+                return NULL;
+
+        p->udp_event_source = sd_event_source_disable_unref(p->udp_event_source);
+        p->tcp_event_source = sd_event_source_disable_unref(p->tcp_event_source);
+
+        hashmap_free(p->queries_by_packet);
+
+        return mfree(p);
+}
+
+static void stub_packet_hash_func(const DnsPacket *p, struct siphash *state) {
+        assert(p);
+
+        siphash24_compress(&p->protocol, sizeof(p->protocol), state);
+        siphash24_compress(&p->family, sizeof(p->family), state);
+        siphash24_compress(&p->sender, sizeof(p->sender), state);
+        siphash24_compress(&p->ipproto, sizeof(p->ipproto), state);
+        siphash24_compress(&p->sender_port, sizeof(p->sender_port), state);
+        siphash24_compress(DNS_PACKET_HEADER(p), sizeof(DnsPacketHeader), state);
+
+        /* We don't bother hashing the full packet here, just the header */
+}
+
+static int stub_packet_compare_func(const DnsPacket *x, const DnsPacket *y) {
+        int r;
+
+        r = CMP(x->protocol, y->protocol);
+        if (r != 0)
+                return r;
+
+        r = CMP(x->family, y->family);
+        if (r != 0)
+                return r;
+
+        r = memcmp(&x->sender, &y->sender, sizeof(x->sender));
+        if (r != 0)
+                return r;
+
+        r = CMP(x->ipproto, y->ipproto);
+        if (r != 0)
+                return r;
+
+        r = CMP(x->sender_port, y->sender_port);
+        if (r != 0)
+                return r;
+
+        return memcmp(DNS_PACKET_HEADER(x), DNS_PACKET_HEADER(y), sizeof(DnsPacketHeader));
+}
+
+DEFINE_HASH_OPS(stub_packet_hash_ops, DnsPacket, stub_packet_hash_func, stub_packet_compare_func);
+
+static int reply_add_with_rrsig(
+                DnsAnswer **reply,
+                DnsResourceRecord *rr,
+                int ifindex,
+                DnsAnswerFlags flags,
+                DnsResourceRecord *rrsig,
+                bool with_rrsig) {
+        int r;
+
+        assert(reply);
+        assert(rr);
+
+        r = dns_answer_add_extend(reply, rr, ifindex, flags, rrsig);
+        if (r < 0)
+                return r;
+
+        if (with_rrsig && rrsig) {
+                r = dns_answer_add_extend(reply, rrsig, ifindex, flags, NULL);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_stub_collect_answer_by_question(
+                DnsAnswer **reply,
+                DnsAnswer *answer,
+                DnsQuestion *question,
+                bool with_rrsig) { /* Add RRSIG RR matching each RR */
+
+        DnsAnswerItem *item;
+        int r;
+
+        assert(reply);
+
+        /* Copies all RRs from 'answer' into 'reply', if they match 'question'. */
+
+        DNS_ANSWER_FOREACH_ITEM(item, answer) {
+
+                /* We have a question, let's see if this RR matches it */
+                r = dns_question_matches_rr(question, item->rr, NULL);
+                if (r < 0)
+                        return r;
+                if (!r) {
+                        /* Maybe there's a CNAME/DNAME in here? If so, that's an answer too */
+                        r = dns_question_matches_cname_or_dname(question, item->rr, NULL);
+                        if (r < 0)
+                                return r;
+                        if (!r)
+                                continue;
+                }
+
+                /* Mask the section info, we want the primary answers to always go without section
+                 * info, so that it is added to the answer section when we synthesize a reply. */
+
+                r = reply_add_with_rrsig(
+                                reply,
+                                item->rr,
+                                item->ifindex,
+                                item->flags & ~DNS_ANSWER_MASK_SECTIONS,
+                                item->rrsig,
+                                with_rrsig);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_stub_collect_answer_by_section(
+                DnsAnswer **reply,
+                DnsAnswer *answer,
+                DnsAnswerFlags section,
+                DnsAnswer *exclude1,
+                DnsAnswer *exclude2,
+                bool with_dnssec) { /* Include DNSSEC RRs. RRSIG, NSEC, … */
+
+        DnsAnswerItem *item;
+        int r;
+
+        assert(reply);
+
+        /* Copies all RRs from 'answer' into 'reply', if they originate from the specified section. Also,
+         * avoid any RRs listed in 'exclude'. */
+
+        DNS_ANSWER_FOREACH_ITEM(item, answer) {
+
+                if (dns_answer_contains(exclude1, item->rr) ||
+                    dns_answer_contains(exclude2, item->rr))
+                        continue;
+
+                if (!with_dnssec &&
+                    dns_type_is_dnssec(item->rr->key->type))
+                        continue;
+
+                if (((item->flags ^ section) & DNS_ANSWER_MASK_SECTIONS) != 0)
+                        continue;
+
+                r = reply_add_with_rrsig(
+                                reply,
+                                item->rr,
+                                item->ifindex,
+                                item->flags,
+                                item->rrsig,
+                                with_dnssec);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_stub_assign_sections(
+                DnsQuery *q,
+                DnsQuestion *question,
+                bool edns0_do) {
+
+        int r;
+
+        assert(q);
+        assert(question);
+
+        /* Let's assign the 'answer' RRs we collected to their respective sections in the reply datagram. We
+         * try to reproduce a section assignment similar to what the upstream DNS server responded to us. We
+         * use the DNS_ANSWER_SECTION_xyz flags to match things up, which is where the original upstream's
+         * packet section assignment is stored in the DnsAnswer object. Not all RRs in the 'answer' objects
+         * come with section information though (for example, because they were synthesized locally, and not
+         * from a DNS packet). To deal with that we extend the assignment logic a bit: anything from the
+         * 'answer' object that directly matches the original question is always put in the ANSWER section,
+         * regardless if it carries section info, or what that section info says. Then, anything from the
+         * 'answer' objects that is from the ANSWER or AUTHORITY sections, and wasn't already added to the
+         * ANSWER section is placed in the AUTHORITY section. Everything else from either object is added to
+         * the ADDITIONAL section. */
+
+        /* Include all RRs that directly answer the question in the answer section */
+        r = dns_stub_collect_answer_by_question(
+                        &q->reply_answer,
+                        q->answer,
+                        question,
+                        edns0_do);
+        if (r < 0)
+                return r;
+
+        /* Include all RRs that originate from the authority sections, and aren't already listed in the
+         * answer section, in the authority section */
+        r = dns_stub_collect_answer_by_section(
+                        &q->reply_authoritative,
+                        q->answer,
+                        DNS_ANSWER_SECTION_AUTHORITY,
+                        q->reply_answer, NULL,
+                        edns0_do);
+        if (r < 0)
+                return r;
+
+        /* Include all RRs that originate from the answer or additional sections in the additional section
+         * (except if already listed in the other two sections). Also add all RRs with no section marking. */
+        r = dns_stub_collect_answer_by_section(
+                        &q->reply_additional,
+                        q->answer,
+                        DNS_ANSWER_SECTION_ANSWER,
+                        q->reply_answer, q->reply_authoritative,
+                        edns0_do);
+        if (r < 0)
+                return r;
+        r = dns_stub_collect_answer_by_section(
+                        &q->reply_additional,
+                        q->answer,
+                        DNS_ANSWER_SECTION_ADDITIONAL,
+                        q->reply_answer, q->reply_authoritative,
+                        edns0_do);
+        if (r < 0)
+                return r;
+        r = dns_stub_collect_answer_by_section(
+                        &q->reply_additional,
+                        q->answer,
+                        0,
+                        q->reply_answer, q->reply_authoritative,
+                        edns0_do);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int dns_stub_make_reply_packet(
+                DnsPacket **ret,
+                size_t max_size,
+                DnsQuestion *q,
+                bool *ret_truncated) {
+
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        bool tc = false;
+        int r;
+
+        assert(ret);
+
+        r = dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, max_size);
+        if (r < 0)
+                return r;
+
+        r = dns_packet_append_question(p, q);
+        if (r == -EMSGSIZE)
+                tc = true;
+        else if (r < 0)
+                return r;
+
+        if (ret_truncated)
+                *ret_truncated = tc;
+        else if (tc)
+                return -EMSGSIZE;
+
+        DNS_PACKET_HEADER(p)->qdcount = htobe16(dns_question_size(q));
+
+        *ret = TAKE_PTR(p);
+        return 0;
+}
+
+static int dns_stub_add_reply_packet_body(
+                DnsPacket *p,
+                DnsAnswer *answer,
+                DnsAnswer *authoritative,
+                DnsAnswer *additional,
+                bool edns0_do, /* Client expects DNSSEC RRs? */
+                bool *truncated) {
+
+        unsigned n_answer = 0, n_authoritative = 0, n_additional = 0;
+        bool tc = false;
+        int r;
+
+        assert(p);
+
+        /* Add the three sections to the packet. If the answer section doesn't fit we'll signal that as
+         * truncation. If the authoritative section doesn't fit and we are in DNSSEC mode, also signal
+         * truncation. In all other cases where things don't fit don't signal truncation, as for those cases
+         * the dropped RRs should not be essential. */
+
+        r = dns_packet_append_answer(p, answer, &n_answer);
+        if (r == -EMSGSIZE)
+                tc = true;
+        else if (r < 0)
+                return r;
+        else {
+                r = dns_packet_append_answer(p, authoritative, &n_authoritative);
+                if (r == -EMSGSIZE) {
+                        if (edns0_do)
+                                tc = true;
+                } else if (r < 0)
+                        return r;
+                else {
+                        r = dns_packet_append_answer(p, additional, &n_additional);
+                        if (r < 0 && r != -EMSGSIZE)
+                                return r;
+                }
+        }
+
+        if (tc) {
+                if (!truncated)
+                        return -EMSGSIZE;
+
+                *truncated = true;
+        }
+
+        DNS_PACKET_HEADER(p)->ancount = htobe16(n_answer);
+        DNS_PACKET_HEADER(p)->nscount = htobe16(n_authoritative);
+        DNS_PACKET_HEADER(p)->arcount = htobe16(n_additional);
+        return 0;
+}
+
+static const char *nsid_string(void) {
+        static char buffer[SD_ID128_STRING_MAX + STRLEN(".resolved.systemd.io")] = "";
+        sd_id128_t id;
+        int r;
+
+        /* Let's generate a string that we can use as RFC5001 NSID identifier. The string shall identify us
+         * as systemd-resolved, and return a different string for each resolved instance without leaking host
+         * identity. Hence let's use a fixed suffix that identifies resolved, and a prefix generated from the
+         * machine ID but from which the machine ID cannot be determined.
+         *
+         * Clients can use this to determine whether an answer is originating locally or is proxied from
+         * upstream. */
+
+        if (!isempty(buffer))
+                return buffer;
+
+        r = sd_id128_get_machine_app_specific(
+                        SD_ID128_MAKE(ed,d3,12,5d,16,b9,41,f9,a1,49,5f,ab,15,62,ab,27),
+                        &id);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to determine machine ID, ignoring: %m");
+                return NULL;
+        }
+
+        xsprintf(buffer, SD_ID128_FORMAT_STR ".resolved.systemd.io", SD_ID128_FORMAT_VAL(id));
+        return buffer;
+}
+
+static int dns_stub_finish_reply_packet(
+                DnsPacket *p,
+                uint16_t id,
+                int rcode,
+                bool tc,        /* set the Truncated bit? */
+                bool aa,        /* set the Authoritative Answer bit? */
+                bool rd,        /* set the Recursion Desired bit? */
+                bool add_opt,   /* add an OPT RR to this packet? */
+                bool edns0_do,  /* set the EDNS0 DNSSEC OK bit? */
+                bool ad,        /* set the DNSSEC authenticated data bit? */
+                bool cd,        /* set the DNSSEC checking disabled bit? */
+                uint16_t max_udp_size, /* The maximum UDP datagram size to advertise to clients */
+                bool nsid) {    /* whether to add NSID */
+
+        int r;
+
+        assert(p);
+
+        if (add_opt) {
+                r = dns_packet_append_opt(p, max_udp_size, edns0_do, /* include_rfc6975 = */ false, nsid ? nsid_string() : NULL, rcode, NULL);
+                if (r == -EMSGSIZE) /* Hit the size limit? then indicate truncation */
+                        tc = true;
+                else if (r < 0)
+                        return r;
+        } else {
+                /* If the client can't to EDNS0, don't do DO either */
+                edns0_do = false;
+
+                /* If we don't do EDNS, clamp the rcode to 4 bit */
+                if (rcode > 0xF)
+                        rcode = DNS_RCODE_SERVFAIL;
+        }
+
+        /* Don't set the CD bit unless DO is on, too */
+        if (!edns0_do)
+                cd = false;
+
+        /* Note that we allow the AD bit to be set even if client didn't signal DO, as per RFC 6840, section
+         * 5.7 */
+
+        DNS_PACKET_HEADER(p)->id = id;
+
+        DNS_PACKET_HEADER(p)->flags = htobe16(DNS_PACKET_MAKE_FLAGS(
+                                                              1  /* qr */,
+                                                              0  /* opcode */,
+                                                              aa /* aa */,
+                                                              tc /* tc */,
+                                                              rd /* rd */,
+                                                              1  /* ra */,
+                                                              ad /* ad */,
+                                                              cd /* cd */,
+                                                              rcode));
+
+        return 0;
+}
+
+static 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_reply_with_edns0_do(DnsQuery *q) {
+         assert(q);
+
+        /* Reply with DNSSEC DO set? Only if client supports it; and we did any DNSSEC verification
+         * ourselves, or consider the data fully authenticated because we generated it locally, or the client
+         * set cd */
+
+         return DNS_PACKET_DO(q->request_packet) &&
+                 (q->answer_dnssec_result >= 0 ||        /* we did proper DNSSEC validation … */
+                  dns_query_fully_authenticated(q) ||    /* … or we considered it authentic otherwise … */
+                  DNS_PACKET_CD(q->request_packet));     /* … or client set CD */
+}
+
+static void dns_stub_suppress_duplicate_section_rrs(DnsQuery *q) {
+        /* If we follow a CNAME/DNAME chain we might end up populating our sections with redundant RRs
+         * because we built up the sections from multiple reply packets (one from each CNAME/DNAME chain
+         * element). E.g. it could be that an RR that was included in the first reply's additional section
+         * ends up being relevant as main answer in a subsequent reply in the chain. Let's clean this up, and
+         * remove everything in the "higher priority" sections from the "lower priority" sections.
+         *
+         * Note that this removal matches by RR keys instead of the full RRs. This is because RRsets should
+         * always end up in one section fully or not at all, but never be split among sections.
+         *
+         * Specifically: we remove ANSWER section RRs from the AUTHORITATIVE and ADDITIONAL sections, as well
+         * as AUTHORITATIVE section RRs from the ADDITIONAL section. */
+
+        dns_answer_remove_by_answer_keys(&q->reply_authoritative, q->reply_answer);
+        dns_answer_remove_by_answer_keys(&q->reply_additional, q->reply_answer);
+        dns_answer_remove_by_answer_keys(&q->reply_additional, q->reply_authoritative);
+}
+
+static int dns_stub_send_reply(
+                DnsQuery *q,
+                int rcode) {
+
+        _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL;
+        bool truncated, edns0_do;
+        int r;
+
+        assert(q);
+
+        edns0_do = dns_stub_reply_with_edns0_do(q); /* let's check if we shall reply with EDNS0 DO? */
+
+        r = dns_stub_make_reply_packet(
+                        &reply,
+                        DNS_PACKET_PAYLOAD_SIZE_MAX(q->request_packet),
+                        q->request_packet->question,
+                        &truncated);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to build reply packet: %m");
+
+        dns_stub_suppress_duplicate_section_rrs(q);
+
+        r = dns_stub_add_reply_packet_body(
+                        reply,
+                        q->reply_answer,
+                        q->reply_authoritative,
+                        q->reply_additional,
+                        edns0_do,
+                        &truncated);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to append reply packet body: %m");
+
+        r = dns_stub_finish_reply_packet(
+                        reply,
+                        DNS_PACKET_ID(q->request_packet),
+                        rcode,
+                        truncated,
+                        dns_query_fully_authoritative(q),
+                        DNS_PACKET_RD(q->request_packet),
+                        !!q->request_packet->opt,
+                        edns0_do,
+                        (DNS_PACKET_AD(q->request_packet) || DNS_PACKET_DO(q->request_packet)) && dns_query_fully_authenticated(q),
+                        DNS_PACKET_CD(q->request_packet),
+                        q->stub_listener_extra ? ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX : ADVERTISE_DATAGRAM_SIZE_MAX,
+                        dns_packet_has_nsid_request(q->request_packet) > 0 && !q->stub_listener_extra);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to build failure packet: %m");
+
+        return dns_stub_send(q->manager, q->stub_listener_extra, q->request_stream, q->request_packet, reply);
+}
+
+static int dns_stub_send_failure(
+                Manager *m,
+                DnsStubListenerExtra *l,
+                DnsStream *s,
+                DnsPacket *p,
+                int rcode,
+                bool authenticated) {
+
+        _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL;
+        bool truncated;
+        int r;
+
+        assert(m);
+        assert(p);
+
+        r = dns_stub_make_reply_packet(
+                        &reply,
+                        DNS_PACKET_PAYLOAD_SIZE_MAX(p),
+                        p->question,
+                        &truncated);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to make failure packet: %m");
+
+        r = dns_stub_finish_reply_packet(
+                        reply,
+                        DNS_PACKET_ID(p),
+                        rcode,
+                        truncated,
+                        false,
+                        DNS_PACKET_RD(p),
+                        !!p->opt,
+                        DNS_PACKET_DO(p),
+                        (DNS_PACKET_AD(p) || DNS_PACKET_DO(p)) && authenticated,
+                        DNS_PACKET_CD(p),
+                        l ? ADVERTISE_EXTRA_DATAGRAM_SIZE_MAX : ADVERTISE_DATAGRAM_SIZE_MAX,
+                        dns_packet_has_nsid_request(p) > 0 && !l);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to build failure packet: %m");
+
+        return dns_stub_send(m, l, s, p, reply);
+}
+
+static int dns_stub_patch_bypass_reply_packet(
+                DnsPacket **ret,       /* Where to place the patched packet */
+                DnsPacket *original,   /* The packet to patch */
+                DnsPacket *request) {  /* The packet the patched packet shall look like a reply to */
+        _cleanup_(dns_packet_unrefp) DnsPacket *c = NULL;
+        int r;
+
+        assert(ret);
+        assert(original);
+        assert(request);
+
+        r = dns_packet_dup(&c, original);
+        if (r < 0)
+                return r;
+
+        /* Extract the packet, so that we know where the OPT field is */
+        r = dns_packet_extract(c);
+        if (r < 0)
+                return r;
+
+        /* Copy over the original client request ID, so that we can make the upstream query look like our own reply. */
+        DNS_PACKET_HEADER(c)->id = DNS_PACKET_HEADER(request)->id;
+
+        /* Patch in our own maximum datagram size, if EDNS0 was on */
+        r = dns_packet_patch_max_udp_size(c, ADVERTISE_DATAGRAM_SIZE_MAX);
+        if (r < 0)
+                return r;
+
+        /* Lower all TTLs by the time passed since we received the datagram. */
+        if (timestamp_is_set(original->timestamp)) {
+                r = dns_packet_patch_ttls(c, original->timestamp);
+                if (r < 0)
+                        return r;
+        }
+
+        /* Our upstream connection might have supported larger DNS requests than our downstream one, hence
+         * set the TC bit if our reply is larger than what the client supports, and truncate. */
+        if (c->size > DNS_PACKET_PAYLOAD_SIZE_MAX(request)) {
+                log_debug("Artificially truncating stub response, as advertised size of client is smaller than upstream one.");
+                dns_packet_truncate(c, DNS_PACKET_PAYLOAD_SIZE_MAX(request));
+                DNS_PACKET_HEADER(c)->flags = htobe16(be16toh(DNS_PACKET_HEADER(c)->flags) | DNS_PACKET_FLAG_TC);
+        }
+
+        *ret = TAKE_PTR(c);
+        return 0;
+}
+
+static void dns_stub_query_complete(DnsQuery *query) {
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        int r;
+
+        assert(q);
+        assert(q->request_packet);
+
+        if (q->question_bypass) {
+                /* This is a bypass reply. If so, let's propagate the upstream packet, if we have it and it
+                 * is regular DNS. (We can't do this if the upstream packet is LLMNR or mDNS, since the
+                 * packets are not 100% compatible.) */
+
+                if (q->answer_full_packet &&
+                    q->answer_full_packet->protocol == DNS_PROTOCOL_DNS) {
+                        _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL;
+
+                        r = dns_stub_patch_bypass_reply_packet(&reply, q->answer_full_packet, q->request_packet);
+                        if (r < 0)
+                                log_debug_errno(r, "Failed to patch bypass reply packet: %m");
+                        else
+                                (void) dns_stub_send(q->manager, q->stub_listener_extra, q->request_stream, q->request_packet, reply);
+
+                        return;
+                }
+        }
+
+        /* Take all data from the current reply, and merge it into the three reply sections we are building
+         * up. We do this before processing CNAME redirects, so that we gradually build up our sections, and
+         * and keep adding all RRs in the CNAME chain. */
+        r = dns_stub_assign_sections(
+                        q,
+                        dns_query_question_for_protocol(q, DNS_PROTOCOL_DNS),
+                        dns_stub_reply_with_edns0_do(q));
+        if (r < 0)
+                return (void) log_debug_errno(r, "Failed to assign sections: %m");
+
+        switch (q->state) {
+
+        case DNS_TRANSACTION_SUCCESS: {
+                bool first = true;
+
+                for (;;) {
+                        int cname_result;
+
+                        cname_result = dns_query_process_cname_one(q);
+                        if (cname_result == -ELOOP) { /* CNAME loop, let's send what we already have */
+                                log_debug_errno(r, "Detected CNAME loop, returning what we already have.");
+                                (void) dns_stub_send_reply(q, q->answer_rcode);
+                                break;
+                        }
+                        if (cname_result < 0) {
+                                log_debug_errno(cname_result, "Failed to process CNAME: %m");
+                                break;
+                        }
+
+                        if (cname_result == DNS_QUERY_NOMATCH) {
+                                /* This answer doesn't contain any RR that would answer our question
+                                 * positively, i.e. neither directly nor via CNAME. */
+
+                                if (first) /* We never followed a CNAME and the answer doesn't match our
+                                            * question at all? Then this is final, the empty answer is the
+                                            * answer. */
+                                        break;
+
+                                /* Otherwise, we already followed a CNAME once within this packet, and the
+                                 * packet doesn't answer our question. In that case let's restart the query,
+                                 * now with the redirected question. We'll */
+                                r = dns_query_go(q);
+                                if (r < 0)
+                                        return (void) log_debug_errno(r, "Failed to restart query: %m");
+
+                                TAKE_PTR(q);
+                                return;
+                        }
+
+                        r = dns_stub_assign_sections(
+                                        q,
+                                        dns_query_question_for_protocol(q, DNS_PROTOCOL_DNS),
+                                        dns_stub_reply_with_edns0_do(q));
+                        if (r < 0)
+                                return (void) log_debug_errno(r, "Failed to assign sections: %m");
+
+                        if (cname_result == DNS_QUERY_MATCH) /* A match? Then we are done, let's return what we got */
+                                break;
+
+                        /* We followed a CNAME. and collected the RRs that answer the redirected question
+                         * successfully. Let's not try to do this again. */
+                        assert(cname_result == DNS_QUERY_CNAME);
+                        first = false;
+                }
+
+                _fallthrough_;
+        }
+
+        case DNS_TRANSACTION_RCODE_FAILURE:
+                (void) dns_stub_send_reply(q, q->answer_rcode);
+                break;
+
+        case DNS_TRANSACTION_NOT_FOUND:
+                (void) dns_stub_send_reply(q, DNS_RCODE_NXDOMAIN);
+                break;
+
+        case DNS_TRANSACTION_TIMEOUT:
+        case DNS_TRANSACTION_ATTEMPTS_MAX_REACHED:
+                /* Propagate a timeout as a no packet, i.e. that the client also gets a timeout */
+                break;
+
+        case DNS_TRANSACTION_NO_SERVERS:
+        case DNS_TRANSACTION_INVALID_REPLY:
+        case DNS_TRANSACTION_ERRNO:
+        case DNS_TRANSACTION_ABORTED:
+        case DNS_TRANSACTION_DNSSEC_FAILED:
+        case DNS_TRANSACTION_NO_TRUST_ANCHOR:
+        case DNS_TRANSACTION_RR_TYPE_UNSUPPORTED:
+        case DNS_TRANSACTION_NETWORK_DOWN:
+        case DNS_TRANSACTION_NO_SOURCE:
+        case DNS_TRANSACTION_STUB_LOOP:
+                (void) dns_stub_send_reply(q, DNS_RCODE_SERVFAIL);
+                break;
+
+        case DNS_TRANSACTION_NULL:
+        case DNS_TRANSACTION_PENDING:
+        case DNS_TRANSACTION_VALIDATING:
+        default:
+                assert_not_reached("Impossible state");
+        }
+}
+
+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;
+        Hashmap **queries_by_packet;
+        DnsQuery *existing;
+        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;
+        }
+
+        if (manager_packet_from_our_transaction(m, p)) {
+                log_debug("Got our own packet looped back, ignoring.");
+                return;
+        }
+
+        queries_by_packet = l ? &l->queries_by_packet : &m->stub_queries_by_packet;
+        existing = hashmap_get(*queries_by_packet, p);
+        if (existing && dns_packet_equal(existing->request_packet, p)) {
+                log_debug("Got repeat packet from client, ignoring.");
+                return;
+        }
+
+        r = dns_packet_extract(p);
+        if (r < 0) {
+                log_debug_errno(r, "Failed to extract resources from incoming packet, ignoring packet: %m");
+                dns_stub_send_failure(m, l, s, p, DNS_RCODE_FORMERR, false);
+                return;
+        }
+
+        if (!DNS_PACKET_VERSION_SUPPORTED(p)) {
+                log_debug("Got EDNS OPT field with unsupported version number.");
+                dns_stub_send_failure(m, l, s, p, DNS_RCODE_BADVERS, false);
+                return;
+        }
+
+        if (dns_type_is_obsolete(dns_question_first_key(p->question)->type)) {
+                log_debug("Got message with obsolete key type, refusing.");
+                dns_stub_send_failure(m, l, s, p, DNS_RCODE_REFUSED, false);
+                return;
+        }
+
+        if (dns_type_is_zone_transer(dns_question_first_key(p->question)->type)) {
+                log_debug("Got request for zone transfer, refusing.");
+                dns_stub_send_failure(m, l, s, p, DNS_RCODE_REFUSED, false);
+                return;
+        }
+
+        if (!DNS_PACKET_RD(p))  {
+                /* If the "rd" bit is off (i.e. recursion was not requested), then refuse operation */
+                log_debug("Got request with recursion disabled, refusing.");
+                dns_stub_send_failure(m, l, s, p, DNS_RCODE_REFUSED, false);
+                return;
+        }
+
+        r = hashmap_ensure_allocated(queries_by_packet, &stub_packet_hash_ops);
+        if (r < 0) {
+                log_oom();
+                return;
+        }
+
+        if (DNS_PACKET_DO(p) && DNS_PACKET_CD(p)) {
+                log_debug("Got request with DNSSEC checking disabled, enabling bypass logic.");
+
+                r = dns_query_new(m, &q, NULL, NULL, p, 0,
+                                  SD_RESOLVED_PROTOCOLS_ALL|
+                                  SD_RESOLVED_NO_CNAME|
+                                  SD_RESOLVED_NO_SEARCH|
+                                  SD_RESOLVED_NO_VALIDATE|
+                                  SD_RESOLVED_REQUIRE_PRIMARY|
+                                  SD_RESOLVED_CLAMP_TTL);
+        } else
+                r = dns_query_new(m, &q, p->question, p->question, NULL, 0,
+                                  SD_RESOLVED_PROTOCOLS_ALL|
+                                  SD_RESOLVED_NO_SEARCH|
+                                  (DNS_PACKET_DO(p) ? SD_RESOLVED_REQUIRE_PRIMARY : 0)|
+                                  SD_RESOLVED_CLAMP_TTL);
+        if (r < 0) {
+                log_error_errno(r, "Failed to generate query object: %m");
+                dns_stub_send_failure(m, l, s, p, DNS_RCODE_SERVFAIL, false);
+                return;
+        }
+
+        q->request_packet = dns_packet_ref(p);
+        q->request_stream = dns_stream_ref(s); /* make sure the stream stays around until we can send a reply through it */
+        q->stub_listener_extra = l;
+        q->complete = dns_stub_query_complete;
+
+        if (s) {
+                /* Remember which queries belong to this stream, so that we can cancel them when the stream
+                 * is disconnected early */
+
+                r = set_ensure_put(&s->queries, NULL, q);
+                if (r < 0) {
+                        log_oom();
+                        return;
+                }
+                assert(r > 0);
+        }
+
+        /* Add the query to the hash table we use to determine repeat packets now. We don't care about
+         * failures here, since in the worst case we'll not recognize duplicate incoming requests, which
+         * isn't particularly bad. */
+        (void) hashmap_put(*queries_by_packet, q->request_packet, q);
+
+        r = dns_query_go(q);
+        if (r < 0) {
+                log_error_errno(r, "Failed to start query: %m");
+                dns_stub_send_failure(m, l, s, p, DNS_RCODE_SERVFAIL, false);
+                return;
+        }
+
+        log_debug("Processing query...");
+        TAKE_PTR(q);
+}
+
+static int on_dns_stub_packet_internal(sd_event_source *s, int fd, uint32_t revents, Manager *m, DnsStubListenerExtra *l) {
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        int r;
+
+        r = manager_recv(m, fd, DNS_PROTOCOL_DNS, &p);
+        if (r <= 0)
+                return r;
+
+        if (dns_packet_validate_query(p) > 0) {
+                log_debug("Got DNS stub UDP query packet for id %u", DNS_PACKET_ID(p));
+
+                dns_stub_process_query(m, l, NULL, p);
+        } else
+                log_debug("Invalid DNS stub UDP packet, ignoring.");
+
+        return 0;
+}
+
+static int on_dns_stub_packet(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        return on_dns_stub_packet_internal(s, fd, revents, userdata, NULL);
+}
+
+static int on_dns_stub_packet_extra(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        DnsStubListenerExtra *l = 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 set_dns_stub_common_tcp_socket_options(int fd) {
+        int r;
+
+        assert(fd >= 0);
+
+        r = setsockopt_int(fd, IPPROTO_TCP, TCP_FASTOPEN, 5); /* Everybody appears to pick qlen=5, let's do the same here. */
+        if (r < 0)
+                log_debug_errno(r, "Failed to enable TCP_FASTOPEN on TCP listening socket, ignoring: %m");
+
+        r = setsockopt_int(fd, IPPROTO_TCP, TCP_NODELAY, true);
+        if (r < 0)
+                log_debug_errno(r, "Failed to enable TCP_NODELAY mode, ignoring: %m");
+
+        return 0;
+}
+
+static int manager_dns_stub_fd(Manager *m, int 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;
+
+        if (type == SOCK_STREAM) {
+                r = set_dns_stub_common_tcp_socket_options(fd);
+                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(dns_stub_listener_extra_port(l)),
+                        .in.sin_addr = l->address.in,
+                };
+        else
+                sa = (union sockaddr_union) {
+                        .in6.sin6_family = l->family,
+                        .in6.sin6_port = htobe16(dns_stub_listener_extra_port(l)),
+                        .in6.sin6_addr = l->address.in6,
+                };
+
+        fd = socket(l->family, type | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+        if (fd < 0) {
+                r = -errno;
+                goto fail;
+        }
+
+        r = set_dns_stub_common_socket_options(fd, l->family);
+        if (r < 0)
+                goto fail;
+
+        if (type == SOCK_STREAM) {
+                r = set_dns_stub_common_tcp_socket_options(fd);
+                if (r < 0)
+                        goto fail;
+        }
+
+        /* Do not set IP_TTL for extra DNS stub listeners, as the address may not be local and in that case
+         * people may want ttl > 1. */
+
+        r = socket_set_freebind(fd, l->family, true);
+        if (r < 0)
+                goto fail;
+
+        if (type == SOCK_DGRAM) {
+                r = socket_disable_pmtud(fd, l->family);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to disable UDP PMTUD, ignoring: %m");
+
+                r = socket_set_recvfragsize(fd, l->family, true);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to enable fragment size reception, ignoring: %m");
+        }
+
+        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_disable_unref(m->dns_stub_udp_event_source);
+        m->dns_stub_tcp_event_source = sd_event_source_disable_unref(m->dns_stub_tcp_event_source);
+}
+
+static const char* const dns_stub_listener_mode_table[_DNS_STUB_LISTENER_MODE_MAX] = {
+        [DNS_STUB_LISTENER_NO]  = "no",
+        [DNS_STUB_LISTENER_UDP] = "udp",
+        [DNS_STUB_LISTENER_TCP] = "tcp",
+        [DNS_STUB_LISTENER_YES] = "yes",
+};
+DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN(dns_stub_listener_mode, DnsStubListenerMode, DNS_STUB_LISTENER_YES);
diff --git a/src/resolve/resolved-dns-stub.h b/src/resolve/resolved-dns-stub.h
new file mode 100644
index 0000000..3b9bf65
--- /dev/null
+++ b/src/resolve/resolved-dns-stub.h
@@ -0,0 +1,48 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "hash-funcs.h"
+
+typedef struct DnsStubListenerExtra DnsStubListenerExtra;
+
+typedef enum DnsStubListenerMode {
+        DNS_STUB_LISTENER_NO,
+        DNS_STUB_LISTENER_UDP = 1 << 0,
+        DNS_STUB_LISTENER_TCP = 1 << 1,
+        DNS_STUB_LISTENER_YES = DNS_STUB_LISTENER_UDP | DNS_STUB_LISTENER_TCP,
+        _DNS_STUB_LISTENER_MODE_MAX,
+        _DNS_STUB_LISTENER_MODE_INVALID = -EINVAL,
+} DnsStubListenerMode;
+
+#include "resolved-manager.h"
+
+struct DnsStubListenerExtra {
+        Manager *manager;
+
+        DnsStubListenerMode mode;
+
+        int family;
+        union in_addr_union address;
+        uint16_t port;
+
+        sd_event_source *udp_event_source;
+        sd_event_source *tcp_event_source;
+
+        Hashmap *queries_by_packet;
+};
+
+extern const struct hash_ops dns_stub_listener_extra_hash_ops;
+
+int dns_stub_listener_extra_new(Manager *m, DnsStubListenerExtra **ret);
+DnsStubListenerExtra *dns_stub_listener_extra_free(DnsStubListenerExtra *p);
+static inline uint16_t dns_stub_listener_extra_port(DnsStubListenerExtra *p) {
+        assert(p);
+
+        return p->port > 0 ? p->port : 53;
+}
+
+void manager_dns_stub_stop(Manager *m);
+int manager_dns_stub_start(Manager *m);
+
+const char* dns_stub_listener_mode_to_string(DnsStubListenerMode p) _const_;
+DnsStubListenerMode dns_stub_listener_mode_from_string(const char *s) _pure_;
diff --git a/src/resolve/resolved-dns-synthesize.c b/src/resolve/resolved-dns-synthesize.c
new file mode 100644
index 0000000..0914515
--- /dev/null
+++ b/src/resolve/resolved-dns-synthesize.c
@@ -0,0 +1,481 @@
+/* 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, NULL);
+                if (r < 0)
+                        return r;
+        }
+
+        if (IN_SET(key->type, DNS_TYPE_AAAA, DNS_TYPE_ANY) && socket_ipv6_is_enabled()) {
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+
+                rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_AAAA, dns_resource_key_name(key));
+                if (!rr)
+                        return -ENOMEM;
+
+                rr->aaaa.in6_addr = in6addr_loopback;
+
+                r = dns_answer_add(*answer, rr, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED, NULL);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int answer_add_ptr(DnsAnswer **answer, const char *from, const char *to, int ifindex, DnsAnswerFlags flags) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+
+        rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR, from);
+        if (!rr)
+                return -ENOMEM;
+
+        rr->ptr.name = strdup(to);
+        if (!rr->ptr.name)
+                return -ENOMEM;
+
+        return dns_answer_add(*answer, rr, ifindex, flags, NULL);
+}
+
+static int synthesize_localhost_ptr(Manager *m, const DnsResourceKey *key, int ifindex, DnsAnswer **answer) {
+        int r;
+
+        assert(m);
+        assert(key);
+        assert(answer);
+
+        if (IN_SET(key->type, DNS_TYPE_PTR, DNS_TYPE_ANY)) {
+                r = dns_answer_reserve(answer, 1);
+                if (r < 0)
+                        return r;
+
+                r = answer_add_ptr(answer, dns_resource_key_name(key), "localhost", dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int answer_add_addresses_rr(
+                DnsAnswer **answer,
+                const char *name,
+                struct local_address *addresses,
+                unsigned n_addresses) {
+
+        unsigned j;
+        int r;
+
+        assert(answer);
+        assert(name);
+
+        r = dns_answer_reserve(answer, n_addresses);
+        if (r < 0)
+                return r;
+
+        for (j = 0; j < n_addresses; j++) {
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+
+                r = dns_resource_record_new_address(&rr, addresses[j].family, &addresses[j].address, name);
+                if (r < 0)
+                        return r;
+
+                r = dns_answer_add(*answer, rr, addresses[j].ifindex, DNS_ANSWER_AUTHENTICATED, NULL);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int answer_add_addresses_ptr(
+                DnsAnswer **answer,
+                const char *name,
+                struct local_address *addresses,
+                unsigned n_addresses,
+                int af, const union in_addr_union *match) {
+
+        bool added = false;
+        unsigned j;
+        int r;
+
+        assert(answer);
+        assert(name);
+
+        for (j = 0; j < n_addresses; j++) {
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+
+                if (af != AF_UNSPEC) {
+
+                        if (addresses[j].family != af)
+                                continue;
+
+                        if (match && !in_addr_equal(af, match, &addresses[j].address))
+                                continue;
+                }
+
+                r = dns_answer_reserve(answer, 1);
+                if (r < 0)
+                        return r;
+
+                r = dns_resource_record_new_reverse(&rr, addresses[j].family, &addresses[j].address, name);
+                if (r < 0)
+                        return r;
+
+                r = dns_answer_add(*answer, rr, addresses[j].ifindex, DNS_ANSWER_AUTHENTICATED, NULL);
+                if (r < 0)
+                        return r;
+
+                added = true;
+        }
+
+        return added;
+}
+
+static int synthesize_system_hostname_rr(Manager *m, const DnsResourceKey *key, int ifindex, DnsAnswer **answer) {
+        _cleanup_free_ struct local_address *addresses = NULL;
+        int n = 0, af;
+
+        assert(m);
+        assert(key);
+        assert(answer);
+
+        af = dns_type_to_af(key->type);
+        if (af >= 0) {
+                n = local_addresses(m->rtnl, ifindex, af, &addresses);
+                if (n < 0)
+                        return n;
+
+                if (n == 0) {
+                        struct local_address buffer[2];
+
+                        /* If we have no local addresses then use ::1
+                         * and 127.0.0.2 as local ones. */
+
+                        if (IN_SET(af, AF_INET, AF_UNSPEC))
+                                buffer[n++] = (struct local_address) {
+                                        .family = AF_INET,
+                                        .ifindex = dns_synthesize_ifindex(ifindex),
+                                        .address.in.s_addr = htobe32(0x7F000002),
+                                };
+
+                        if (IN_SET(af, AF_INET6, AF_UNSPEC) && socket_ipv6_is_enabled())
+                                buffer[n++] = (struct local_address) {
+                                        .family = AF_INET6,
+                                        .ifindex = dns_synthesize_ifindex(ifindex),
+                                        .address.in6 = in6addr_loopback,
+                                };
+
+                        return answer_add_addresses_rr(answer,
+                                                       dns_resource_key_name(key),
+                                                       buffer, n);
+                }
+        }
+
+        return answer_add_addresses_rr(answer, dns_resource_key_name(key), addresses, n);
+}
+
+static int synthesize_system_hostname_ptr(Manager *m, int af, const union in_addr_union *address, int ifindex, DnsAnswer **answer) {
+        _cleanup_free_ struct local_address *addresses = NULL;
+        bool added = false;
+        int n, r;
+
+        assert(m);
+        assert(address);
+        assert(answer);
+
+        if (af == AF_INET && address->in.s_addr == htobe32(0x7F000002)) {
+
+                /* Always map the IPv4 address 127.0.0.2 to the local hostname, in addition to "localhost": */
+
+                r = dns_answer_reserve(answer, 4);
+                if (r < 0)
+                        return r;
+
+                r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", m->full_hostname, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED);
+                if (r < 0)
+                        return r;
+
+                r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", m->llmnr_hostname, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED);
+                if (r < 0)
+                        return r;
+
+                r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", m->mdns_hostname, dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED);
+                if (r < 0)
+                        return r;
+
+                r = answer_add_ptr(answer, "2.0.0.127.in-addr.arpa", "localhost", dns_synthesize_ifindex(ifindex), DNS_ANSWER_AUTHENTICATED);
+                if (r < 0)
+                        return r;
+
+                return 1;
+        }
+
+        n = local_addresses(m->rtnl, ifindex, af, &addresses);
+        if (n <= 0)
+                return n;
+
+        r = answer_add_addresses_ptr(answer, m->full_hostname, addresses, n, af, address);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                added = true;
+
+        r = answer_add_addresses_ptr(answer, m->llmnr_hostname, addresses, n, af, address);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                added = true;
+
+        r = answer_add_addresses_ptr(answer, m->mdns_hostname, addresses, n, af, address);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                added = true;
+
+        return added;
+}
+
+static int synthesize_gateway_rr(
+                Manager *m,
+                const DnsResourceKey *key,
+                int ifindex,
+                int (*lookup)(sd_netlink *context, int ifindex, int af, struct local_address **ret), /* either local_gateways() or local_outbound() */
+                DnsAnswer **answer) {
+        _cleanup_free_ struct local_address *addresses = NULL;
+        int n = 0, af, r;
+
+        assert(m);
+        assert(key);
+        assert(lookup);
+        assert(answer);
+
+        af = dns_type_to_af(key->type);
+        if (af >= 0) {
+                n = lookup(m->rtnl, ifindex, af, &addresses);
+                if (n < 0) /* < 0 means: error */
+                        return n;
+
+                if (n == 0) { /* == 0 means we have no gateway */
+                        /* See if there's a gateway on the other protocol */
+                        if (af == AF_INET)
+                                n = lookup(m->rtnl, ifindex, AF_INET6, NULL);
+                        else {
+                                assert(af == AF_INET6);
+                                n = lookup(m->rtnl, ifindex, AF_INET, NULL);
+                        }
+                        if (n <= 0) /* error (if < 0) or really no gateway at all (if == 0) */
+                                return n;
+
+                        /* We have a gateway on the other protocol. Let's return > 0 without adding any RR to
+                         * the answer, i.e. synthesize NODATA (and not NXDOMAIN!) */
+                        return 1;
+                }
+        }
+
+        r = answer_add_addresses_rr(answer, dns_resource_key_name(key), addresses, n);
+        if (r < 0)
+                return r;
+
+        return 1; /* > 0 means: we have some gateway */
+}
+
+static int synthesize_gateway_ptr(Manager *m, int af, const union in_addr_union *address, int ifindex, DnsAnswer **answer) {
+        _cleanup_free_ struct local_address *addresses = NULL;
+        int n;
+
+        assert(m);
+        assert(address);
+        assert(answer);
+
+        n = local_gateways(m->rtnl, ifindex, af, &addresses);
+        if (n <= 0)
+                return n;
+
+        return answer_add_addresses_ptr(answer, "_gateway", addresses, n, af, address);
+}
+
+int dns_synthesize_answer(
+                Manager *m,
+                DnsQuestion *q,
+                int ifindex,
+                DnsAnswer **ret) {
+
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        DnsResourceKey *key;
+        bool found = false, nxdomain = false;
+        int r;
+
+        assert(m);
+        assert(q);
+
+        DNS_QUESTION_FOREACH(key, q) {
+                union in_addr_union address;
+                const char *name;
+                int af;
+
+                if (!IN_SET(key->class, DNS_CLASS_IN, DNS_CLASS_ANY))
+                        continue;
+
+                name = dns_resource_key_name(key);
+
+                if (dns_name_is_empty(name)) {
+                        /* Do nothing. */
+
+                } else if (dns_name_endswith(name, "0.in-addr.arpa") > 0 ||
+                           dns_name_equal(name, "255.255.255.255.in-addr.arpa") > 0 ||
+                           dns_name_equal(name, "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 ||
+                           dns_name_endswith(name, "invalid") > 0) {
+
+                        nxdomain = true;
+                        continue;
+
+                } else if (is_localhost(name)) {
+
+                        r = synthesize_localhost_rr(m, key, ifindex, &answer);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to synthesize localhost RRs: %m");
+
+                } else if (manager_is_own_hostname(m, name)) {
+
+                        r = synthesize_system_hostname_rr(m, key, ifindex, &answer);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to synthesize system hostname RRs: %m");
+
+                } else if (is_gateway_hostname(name)) {
+
+                        r = synthesize_gateway_rr(m, key, ifindex, local_gateways, &answer);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to synthesize gateway RRs: %m");
+                        if (r == 0) { /* if we have no gateway return NXDOMAIN */
+                                nxdomain = true;
+                                continue;
+                        }
+
+                } else if (is_outbound_hostname(name)) {
+
+                        r = synthesize_gateway_rr(m, key, ifindex, local_outbounds, &answer);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to synthesize outbound RRs: %m");
+                        if (r == 0) { /* if we have no gateway return NXDOMAIN */
+                                nxdomain = true;
+                                continue;
+                        }
+
+                } else if ((dns_name_endswith(name, "127.in-addr.arpa") > 0 && dns_name_equal(name, "2.0.0.127.in-addr.arpa") == 0) ||
+                           dns_name_equal(name, "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa") > 0) {
+
+                        r = synthesize_localhost_ptr(m, key, ifindex, &answer);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to synthesize localhost PTR RRs: %m");
+
+                } else if (dns_name_address(name, &af, &address) > 0) {
+                        int v, w;
+
+                        v = synthesize_system_hostname_ptr(m, af, &address, ifindex, &answer);
+                        if (v < 0)
+                                return log_error_errno(v, "Failed to synthesize system hostname PTR RR: %m");
+
+                        w = synthesize_gateway_ptr(m, af, &address, ifindex, &answer);
+                        if (w < 0)
+                                return log_error_errno(w, "Failed to synthesize gateway hostname PTR RR: %m");
+
+                        if (v == 0 && w == 0) /* This IP address is neither a local one nor a gateway */
+                                continue;
+
+                        /* Note that we never synthesize reverse PTR for _outbound, since those are local
+                         * addresses and thus mapped to the local hostname anyway, hence they already have a
+                         * mapping. */
+
+                } else
+                        continue;
+
+                found = true;
+        }
+
+        if (found) {
+
+                if (ret)
+                        *ret = TAKE_PTR(answer);
+
+                return 1;
+        } else if (nxdomain)
+                return -ENXIO;
+
+        return 0;
+}
diff --git a/src/resolve/resolved-dns-synthesize.h b/src/resolve/resolved-dns-synthesize.h
new file mode 100644
index 0000000..fb62458
--- /dev/null
+++ b/src/resolve/resolved-dns-synthesize.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "resolved-dns-answer.h"
+#include "resolved-dns-question.h"
+#include "resolved-manager.h"
+
+int dns_synthesize_ifindex(int ifindex);
+int dns_synthesize_family(uint64_t flags);
+DnsProtocol dns_synthesize_protocol(uint64_t flags);
+
+int dns_synthesize_answer(Manager *m, DnsQuestion *q, int ifindex, DnsAnswer **ret);
diff --git a/src/resolve/resolved-dns-transaction.c b/src/resolve/resolved-dns-transaction.c
new file mode 100644
index 0000000..cf10c5c
--- /dev/null
+++ b/src/resolve/resolved-dns-transaction.c
@@ -0,0 +1,3581 @@
+/* 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_query_flags = 0;
+        t->answer_nsec_ttl = UINT32_MAX;
+        t->answer_errno = 0;
+}
+
+static void dns_transaction_flush_dnssec_transactions(DnsTransaction *t) {
+        DnsTransaction *z;
+
+        assert(t);
+
+        while ((z = set_steal_first(t->dnssec_transactions))) {
+                set_remove(z->notify_transactions, t);
+                set_remove(z->notify_transactions_done, t);
+                dns_transaction_gc(z);
+        }
+}
+
+static void dns_transaction_close_connection(
+                DnsTransaction *t,
+                bool use_graveyard) { /* Set use_graveyard = false when you know the connection is already
+                                       * dead, for example because you got a connection error back from the
+                                       * kernel. In that case there's no point in keeping the fd around,
+                                       * hence don't. */
+        int r;
+
+        assert(t);
+
+        if (t->stream) {
+                /* Let's detach the stream from our transaction, in case something else keeps a reference to it. */
+                LIST_REMOVE(transactions_by_stream, t->stream->transactions, t);
+
+                /* Remove packet in case it's still in the queue */
+                dns_packet_unref(ordered_set_remove(t->stream->write_queue, t->sent));
+
+                t->stream = dns_stream_unref(t->stream);
+        }
+
+        t->dns_udp_event_source = sd_event_source_disable_unref(t->dns_udp_event_source);
+
+        /* If we have an UDP socket where we sent a packet, but never received one, then add it to the socket
+         * graveyard, instead of closing it right away. That way it will stick around for a moment longer,
+         * and the reply we might still get from the server will be eaten up instead of resulting in an ICMP
+         * port unreachable error message. */
+
+        if (use_graveyard && t->dns_udp_fd >= 0 && t->sent && !t->received) {
+                r = manager_add_socket_to_graveyard(t->scope->manager, t->dns_udp_fd);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to add UDP socket to graveyard, closing immediately: %m");
+                else
+                        TAKE_FD(t->dns_udp_fd);
+        }
+
+        t->dns_udp_fd = safe_close(t->dns_udp_fd);
+}
+
+static void dns_transaction_stop_timeout(DnsTransaction *t) {
+        assert(t);
+
+        t->timeout_event_source = sd_event_source_disable_unref(t->timeout_event_source);
+}
+
+DnsTransaction* dns_transaction_free(DnsTransaction *t) {
+        DnsQueryCandidate *c;
+        DnsZoneItem *i;
+        DnsTransaction *z;
+
+        if (!t)
+                return NULL;
+
+        log_debug("Freeing transaction %" PRIu16 ".", t->id);
+
+        dns_transaction_close_connection(t, true);
+        dns_transaction_stop_timeout(t);
+
+        dns_packet_unref(t->sent);
+        dns_transaction_reset_answer(t);
+
+        dns_server_unref(t->server);
+
+        if (t->scope) {
+                if (t->key) {
+                        DnsTransaction *first;
+
+                        first = hashmap_get(t->scope->transactions_by_key, t->key);
+                        LIST_REMOVE(transactions_by_key, first, t);
+                        if (first)
+                                hashmap_replace(t->scope->transactions_by_key, first->key, first);
+                        else
+                                hashmap_remove(t->scope->transactions_by_key, t->key);
+                }
+
+                LIST_REMOVE(transactions_by_scope, t->scope->transactions, t);
+
+                if (t->id != 0)
+                        hashmap_remove(t->scope->manager->dns_transactions, UINT_TO_PTR(t->id));
+        }
+
+        while ((c = set_steal_first(t->notify_query_candidates)))
+                set_remove(c->transactions, t);
+        set_free(t->notify_query_candidates);
+
+        while ((c = set_steal_first(t->notify_query_candidates_done)))
+                set_remove(c->transactions, t);
+        set_free(t->notify_query_candidates_done);
+
+        while ((i = set_steal_first(t->notify_zone_items)))
+                i->probe_transaction = NULL;
+        set_free(t->notify_zone_items);
+
+        while ((i = set_steal_first(t->notify_zone_items_done)))
+                i->probe_transaction = NULL;
+        set_free(t->notify_zone_items_done);
+
+        while ((z = set_steal_first(t->notify_transactions)))
+                set_remove(z->dnssec_transactions, t);
+        set_free(t->notify_transactions);
+
+        while ((z = set_steal_first(t->notify_transactions_done)))
+                set_remove(z->dnssec_transactions, t);
+        set_free(t->notify_transactions_done);
+
+        dns_transaction_flush_dnssec_transactions(t);
+        set_free(t->dnssec_transactions);
+
+        dns_answer_unref(t->validated_keys);
+        dns_resource_key_unref(t->key);
+        dns_packet_unref(t->bypass);
+
+        return mfree(t);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsTransaction*, dns_transaction_free);
+
+DnsTransaction* dns_transaction_gc(DnsTransaction *t) {
+        assert(t);
+
+        /* Returns !NULL if we can't gc yet. */
+
+        if (t->block_gc > 0)
+                return t;
+
+        if (set_isempty(t->notify_query_candidates) &&
+            set_isempty(t->notify_query_candidates_done) &&
+            set_isempty(t->notify_zone_items) &&
+            set_isempty(t->notify_zone_items_done) &&
+            set_isempty(t->notify_transactions) &&
+            set_isempty(t->notify_transactions_done))
+                return dns_transaction_free(t);
+
+        return t;
+}
+
+static uint16_t pick_new_id(Manager *m) {
+        uint16_t new_id;
+
+        /* Find a fresh, unused transaction id. Note that this loop is bounded because there's a limit on the
+         * number of transactions, and it's much lower than the space of IDs. */
+
+        assert_cc(TRANSACTIONS_MAX < 0xFFFF);
+
+        do
+                random_bytes(&new_id, sizeof(new_id));
+        while (new_id == 0 ||
+               hashmap_get(m->dns_transactions, UINT_TO_PTR(new_id)));
+
+        return new_id;
+}
+
+static int key_ok(
+                DnsScope *scope,
+                DnsResourceKey *key) {
+
+        /* Don't allow looking up invalid or pseudo RRs */
+        if (!dns_type_is_valid_query(key->type))
+                return -EINVAL;
+        if (dns_type_is_obsolete(key->type))
+                return -EOPNOTSUPP;
+
+        /* We only support the IN class */
+        if (!IN_SET(key->class, DNS_CLASS_IN, DNS_CLASS_ANY))
+                return -EOPNOTSUPP;
+
+        /* Don't allows DNSSEC RRs to be looked up via LLMNR/mDNS. They don't really make sense
+         * there, and it speeds up our queries if we refuse this early */
+        if (scope->protocol != DNS_PROTOCOL_DNS &&
+            dns_type_is_dnssec(key->type))
+                return -EOPNOTSUPP;
+
+        return 0;
+}
+
+int dns_transaction_new(
+                DnsTransaction **ret,
+                DnsScope *s,
+                DnsResourceKey *key,
+                DnsPacket *bypass,
+                uint64_t query_flags) {
+
+        _cleanup_(dns_transaction_freep) DnsTransaction *t = NULL;
+        int r;
+
+        assert(ret);
+        assert(s);
+
+        if (key) {
+                assert(!bypass);
+
+                r = key_ok(s, key);
+                if (r < 0)
+                        return r;
+        } else {
+                DnsResourceKey *qk;
+                assert(bypass);
+
+                r = dns_packet_validate_query(bypass);
+                if (r < 0)
+                        return r;
+
+                DNS_QUESTION_FOREACH(qk, bypass->question) {
+                        r = key_ok(s, qk);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        if (hashmap_size(s->manager->dns_transactions) >= TRANSACTIONS_MAX)
+                return -EBUSY;
+
+        r = hashmap_ensure_allocated(&s->manager->dns_transactions, NULL);
+        if (r < 0)
+                return r;
+
+        if (key) {
+                r = hashmap_ensure_allocated(&s->transactions_by_key, &dns_resource_key_hash_ops);
+                if (r < 0)
+                        return r;
+        }
+
+        t = new(DnsTransaction, 1);
+        if (!t)
+                return -ENOMEM;
+
+        *t = (DnsTransaction) {
+                .dns_udp_fd = -1,
+                .answer_source = _DNS_TRANSACTION_SOURCE_INVALID,
+                .answer_dnssec_result = _DNSSEC_RESULT_INVALID,
+                .answer_nsec_ttl = UINT32_MAX,
+                .key = dns_resource_key_ref(key),
+                .query_flags = query_flags,
+                .bypass = dns_packet_ref(bypass),
+                .current_feature_level = _DNS_SERVER_FEATURE_LEVEL_INVALID,
+                .clamp_feature_level_servfail = _DNS_SERVER_FEATURE_LEVEL_INVALID,
+                .clamp_feature_level_nxdomain = _DNS_SERVER_FEATURE_LEVEL_INVALID,
+                .id = pick_new_id(s->manager),
+        };
+
+        r = hashmap_put(s->manager->dns_transactions, UINT_TO_PTR(t->id), t);
+        if (r < 0) {
+                t->id = 0;
+                return r;
+        }
+
+        if (t->key) {
+                DnsTransaction *first;
+
+                first = hashmap_get(s->transactions_by_key, t->key);
+                LIST_PREPEND(transactions_by_key, first, t);
+
+                r = hashmap_replace(s->transactions_by_key, first->key, first);
+                if (r < 0) {
+                        LIST_REMOVE(transactions_by_key, first, t);
+                        return r;
+                }
+        }
+
+        LIST_PREPEND(transactions_by_scope, s->transactions, t);
+        t->scope = s;
+
+        s->manager->n_transactions_total++;
+
+        if (ret)
+                *ret = t;
+
+        TAKE_PTR(t);
+        return 0;
+}
+
+static void dns_transaction_shuffle_id(DnsTransaction *t) {
+        uint16_t new_id;
+        assert(t);
+
+        /* Pick a new ID for this transaction. */
+
+        new_id = pick_new_id(t->scope->manager);
+        assert_se(hashmap_remove_and_put(t->scope->manager->dns_transactions, UINT_TO_PTR(t->id), UINT_TO_PTR(new_id), t) >= 0);
+
+        log_debug("Transaction %" PRIu16 " is now %" PRIu16 ".", t->id, new_id);
+        t->id = new_id;
+
+        /* Make sure we generate a new packet with the new ID */
+        t->sent = dns_packet_unref(t->sent);
+}
+
+static void dns_transaction_tentative(DnsTransaction *t, DnsPacket *p) {
+        _cleanup_free_ char *pretty = NULL;
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+        DnsZoneItem *z;
+
+        assert(t);
+        assert(p);
+        assert(t->scope->protocol == DNS_PROTOCOL_LLMNR);
+
+        if (manager_packet_from_local_address(t->scope->manager, p) != 0)
+                return;
+
+        (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(dns_transaction_key(t), key_str, sizeof key_str),
+                  dns_protocol_to_string(t->scope->protocol),
+                  t->scope->link ? t->scope->link->ifname : "*",
+                  af_to_name_short(t->scope->family),
+                  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(dns_transaction_key(t), key_str, sizeof key_str);
+
+                log_struct(LOG_NOTICE,
+                           "MESSAGE_ID=" SD_MESSAGE_DNSSEC_FAILURE_STR,
+                           LOG_MESSAGE("DNSSEC validation failed for question %s: %s", key_str, dnssec_result_to_string(t->answer_dnssec_result)),
+                           "DNS_TRANSACTION=%" PRIu16, t->id,
+                           "DNS_QUESTION=%s", key_str,
+                           "DNSSEC_RESULT=%s", dnssec_result_to_string(t->answer_dnssec_result),
+                           "DNS_SERVER=%s", strna(dns_server_string_full(t->server)),
+                           "DNS_SERVER_FEATURE_LEVEL=%s", dns_server_feature_level_to_string(t->server->possible_feature_level));
+        }
+
+        /* Note that this call might invalidate the query. Callers
+         * should hence not attempt to access the query or transaction
+         * after calling this function. */
+
+        if (state == DNS_TRANSACTION_ERRNO)
+                st = errno_to_name(t->answer_errno);
+        else
+                st = dns_transaction_state_to_string(state);
+
+        log_debug("%s transaction %" PRIu16 " for <%s> on scope %s on %s/%s now complete with <%s> from %s (%s; %s).",
+                  t->bypass ? "Bypass" : "Regular",
+                  t->id,
+                  dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str),
+                  dns_protocol_to_string(t->scope->protocol),
+                  t->scope->link ? t->scope->link->ifname : "*",
+                  af_to_name_short(t->scope->family),
+                  st,
+                  t->answer_source < 0 ? "none" : dns_transaction_source_to_string(t->answer_source),
+                  FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) ? "not validated" :
+                  (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED) ? "authenticated" : "unsigned"),
+                  FLAGS_SET(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL) ? "confidential" : "non-confidential");
+
+        t->state = state;
+
+        dns_transaction_close_connection(t, true);
+        dns_transaction_stop_timeout(t);
+
+        /* Notify all queries that are interested, but make sure the
+         * transaction isn't freed while we are still looking at it */
+        t->block_gc++;
+
+        SET_FOREACH_MOVE(c, t->notify_query_candidates_done, t->notify_query_candidates)
+                dns_query_candidate_notify(c);
+        SWAP_TWO(t->notify_query_candidates, t->notify_query_candidates_done);
+
+        SET_FOREACH_MOVE(z, t->notify_zone_items_done, t->notify_zone_items)
+                dns_zone_item_notify(z);
+        SWAP_TWO(t->notify_zone_items, t->notify_zone_items_done);
+        if (t->probing && t->state == DNS_TRANSACTION_ATTEMPTS_MAX_REACHED)
+                (void) dns_scope_announce(t->scope, false);
+
+        SET_FOREACH_MOVE(d, t->notify_transactions_done, t->notify_transactions)
+                dns_transaction_notify(d, t);
+        SWAP_TWO(t->notify_transactions, t->notify_transactions_done);
+
+        t->block_gc--;
+        dns_transaction_gc(t);
+}
+
+static void dns_transaction_complete_errno(DnsTransaction *t, int error) {
+        assert(t);
+        assert(error != 0);
+
+        t->answer_errno = abs(error);
+        dns_transaction_complete(t, DNS_TRANSACTION_ERRNO);
+}
+
+static int dns_transaction_pick_server(DnsTransaction *t) {
+        DnsServer *server;
+
+        assert(t);
+        assert(t->scope->protocol == DNS_PROTOCOL_DNS);
+
+        /* Pick a DNS server and a feature level for it. */
+
+        server = dns_scope_get_dns_server(t->scope);
+        if (!server)
+                return -ESRCH;
+
+        /* If we changed the server invalidate the feature level clamping, as the new server might have completely
+         * different properties. */
+        if (server != t->server) {
+                t->clamp_feature_level_servfail = _DNS_SERVER_FEATURE_LEVEL_INVALID;
+                t->clamp_feature_level_nxdomain = _DNS_SERVER_FEATURE_LEVEL_INVALID;
+        }
+
+        t->current_feature_level = dns_server_possible_feature_level(server);
+
+        /* Clamp the feature level if that is requested. */
+        if (t->clamp_feature_level_servfail != _DNS_SERVER_FEATURE_LEVEL_INVALID &&
+            t->current_feature_level > t->clamp_feature_level_servfail)
+                t->current_feature_level = t->clamp_feature_level_servfail;
+        if (t->clamp_feature_level_nxdomain != _DNS_SERVER_FEATURE_LEVEL_INVALID &&
+            t->current_feature_level > t->clamp_feature_level_nxdomain)
+                t->current_feature_level = t->clamp_feature_level_nxdomain;
+
+        log_debug("Using feature level %s for transaction %u.", dns_server_feature_level_to_string(t->current_feature_level), t->id);
+
+        if (server == t->server)
+                return 0;
+
+        dns_server_unref(t->server);
+        t->server = dns_server_ref(server);
+
+        t->n_picked_servers ++;
+
+        log_debug("Using DNS server %s for transaction %u.", strna(dns_server_string_full(t->server)), t->id);
+
+        return 1;
+}
+
+static void dns_transaction_retry(DnsTransaction *t, bool next_server) {
+        int r;
+
+        assert(t);
+
+        /* Retries the transaction as it is, possibly on a different server */
+
+        if (next_server && t->scope->protocol == DNS_PROTOCOL_DNS)
+                log_debug("Retrying transaction %" PRIu16 ", after switching servers.", t->id);
+        else
+                log_debug("Retrying transaction %" PRIu16 ".", t->id);
+
+        /* Before we try again, switch to a new server. */
+        if (next_server)
+                dns_scope_next_dns_server(t->scope, t->server);
+
+        r = dns_transaction_go(t);
+        if (r < 0)
+                dns_transaction_complete_errno(t, r);
+}
+
+static bool dns_transaction_limited_retry(DnsTransaction *t) {
+        assert(t);
+
+        /* If we haven't tried all different servers yet, let's try again with a different server */
+
+        if (t->n_picked_servers >= dns_scope_get_n_dns_servers(t->scope))
+                return false;
+
+        dns_transaction_retry(t, /* next_server= */ true);
+        return true;
+}
+
+static int dns_transaction_maybe_restart(DnsTransaction *t) {
+        int r;
+
+        assert(t);
+
+        /* Restarts the transaction, under a new ID if the feature level of the server changed since we first
+         * tried, without changing DNS server. Returns > 0 if the transaction was restarted, 0 if not. */
+
+        if (!t->server)
+                return 0;
+
+        if (t->current_feature_level <= dns_server_possible_feature_level(t->server))
+                return 0;
+
+        /* The server's current feature level is lower than when we sent the original query. We learnt something from
+           the response or possibly an auxiliary DNSSEC response that we didn't know before.  We take that as reason to
+           restart the whole transaction. This is a good idea to deal with servers that respond rubbish if we include
+           OPT RR or DO bit. One of these cases is documented here, for example:
+           https://open.nlnetlabs.nl/pipermail/dnssec-trigger/2014-November/000376.html */
+
+        log_debug("Server feature level is now lower than when we began our transaction. Restarting with new ID.");
+        dns_transaction_shuffle_id(t);
+
+        r = dns_transaction_go(t);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+static void on_transaction_stream_error(DnsTransaction *t, int error) {
+        assert(t);
+
+        dns_transaction_close_connection(t, true);
+
+        if (ERRNO_IS_DISCONNECT(error)) {
+                if (t->scope->protocol == DNS_PROTOCOL_LLMNR) {
+                        /* If the LLMNR/TCP connection failed, the host doesn't support LLMNR, and we cannot answer the
+                         * question on this scope. */
+                        dns_transaction_complete(t, DNS_TRANSACTION_NOT_FOUND);
+                        return;
+                }
+
+                dns_transaction_retry(t, true);
+                return;
+        }
+        if (error != 0)
+                dns_transaction_complete_errno(t, error);
+}
+
+static int dns_transaction_on_stream_packet(DnsTransaction *t, DnsStream *s, DnsPacket *p) {
+        bool encrypted;
+
+        assert(t);
+        assert(s);
+        assert(p);
+
+        encrypted = s->encrypted;
+
+        dns_transaction_close_connection(t, true);
+
+        if (dns_packet_validate_reply(p) <= 0) {
+                log_debug("Invalid TCP reply packet.");
+                dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY);
+                return 0;
+        }
+
+        dns_scope_check_conflicts(t->scope, p);
+
+        t->block_gc++;
+        dns_transaction_process_reply(t, p, encrypted);
+        t->block_gc--;
+
+        /* If the response wasn't useful, then complete the transition
+         * now. After all, we are the worst feature set now with TCP
+         * sockets, and there's really no point in retrying. */
+        if (t->state == DNS_TRANSACTION_PENDING)
+                dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY);
+        else
+                dns_transaction_gc(t);
+
+        return 0;
+}
+
+static int on_stream_complete(DnsStream *s, int error) {
+        assert(s);
+
+        if (ERRNO_IS_DISCONNECT(error) && s->protocol != DNS_PROTOCOL_LLMNR) {
+                log_debug_errno(error, "Connection failure for DNS TCP stream: %m");
+
+                if (s->transactions) {
+                        DnsTransaction *t;
+
+                        t = s->transactions;
+                        dns_server_packet_lost(t->server, IPPROTO_TCP, t->current_feature_level);
+                }
+        }
+
+        if (error != 0) {
+                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 */
+        assert_se(p = dns_stream_take_read_packet(s));
+
+        t = hashmap_get(s->manager->dns_transactions, UINT_TO_PTR(DNS_PACKET_ID(p)));
+        if (t && t->stream == s) /* Validate that the stream we got this on actually is the stream the
+                                  * transaction was using. */
+                return dns_transaction_on_stream_packet(t, s, p);
+
+        /* Ignore incorrect transaction id as an old transaction can have been canceled. */
+        log_debug("Received unexpected TCP reply packet with id %" PRIu16 ", ignoring.", DNS_PACKET_ID(p));
+        return 0;
+}
+
+static uint16_t dns_transaction_port(DnsTransaction *t) {
+        assert(t);
+
+        if (t->server->port > 0)
+                return t->server->port;
+
+        return DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level) ? 853 : 53;
+}
+
+static int dns_transaction_emit_tcp(DnsTransaction *t) {
+        _cleanup_(dns_stream_unrefp) DnsStream *s = NULL;
+        _cleanup_close_ int fd = -1;
+        union sockaddr_union sa;
+        DnsStreamType type;
+        int r;
+
+        assert(t);
+        assert(t->sent);
+
+        dns_transaction_close_connection(t, true);
+
+        switch (t->scope->protocol) {
+
+        case DNS_PROTOCOL_DNS:
+                r = dns_transaction_pick_server(t);
+                if (r < 0)
+                        return r;
+
+                if (manager_server_is_stub(t->scope->manager, t->server))
+                        return -ELOOP;
+
+                if (!t->bypass) {
+                        if (!dns_server_dnssec_supported(t->server) && dns_type_is_dnssec(dns_transaction_key(t)->type))
+                                return -EOPNOTSUPP;
+
+                        r = dns_server_adjust_opt(t->server, t->sent, t->current_feature_level);
+                        if (r < 0)
+                                return r;
+                }
+
+                if (t->server->stream && (DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level) == t->server->stream->encrypted))
+                        s = dns_stream_ref(t->server->stream);
+                else
+                        fd = dns_scope_socket_tcp(t->scope, AF_UNSPEC, NULL, t->server, dns_transaction_port(t), &sa);
+
+                type = DNS_STREAM_LOOKUP;
+                break;
+
+        case DNS_PROTOCOL_LLMNR:
+                /* When we already received a reply to this (but it was truncated), send to its sender address */
+                if (t->received)
+                        fd = dns_scope_socket_tcp(t->scope, t->received->family, &t->received->sender, NULL, t->received->sender_port, &sa);
+                else {
+                        union in_addr_union address;
+                        int family = AF_UNSPEC;
+
+                        /* Otherwise, try to talk to the owner of a
+                         * the IP address, in case this is a reverse
+                         * PTR lookup */
+
+                        r = dns_name_address(dns_resource_key_name(dns_transaction_key(t)), &family, &address);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                return -EINVAL;
+                        if (family != t->scope->family)
+                                return -ESRCH;
+
+                        fd = dns_scope_socket_tcp(t->scope, family, &address, NULL, LLMNR_PORT, &sa);
+                }
+
+                type = DNS_STREAM_LLMNR_SEND;
+                break;
+
+        default:
+                return -EAFNOSUPPORT;
+        }
+
+        if (!s) {
+                if (fd < 0)
+                        return fd;
+
+                r = dns_stream_new(t->scope->manager, &s, type, t->scope->protocol, fd, &sa);
+                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, /* use_graveyard= */ false);
+                return r;
+        }
+
+        dns_transaction_reset_answer(t);
+
+        t->tried_stream = true;
+
+        return 0;
+}
+
+static void dns_transaction_cache_answer(DnsTransaction *t) {
+        assert(t);
+
+        /* For mDNS we cache whenever we get the packet, rather than
+         * in each transaction. */
+        if (!IN_SET(t->scope->protocol, DNS_PROTOCOL_DNS, DNS_PROTOCOL_LLMNR))
+                return;
+
+        /* Caching disabled? */
+        if (t->scope->manager->enable_cache == DNS_CACHE_MODE_NO)
+                return;
+
+        /* If validation is turned off for this transaction, but DNSSEC is on, then let's not cache this */
+        if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) && t->scope->dnssec_mode != DNSSEC_NO)
+                return;
+
+        /* Packet from localhost? */
+        if (!t->scope->manager->cache_from_localhost &&
+            in_addr_is_localhost(t->received->family, &t->received->sender) != 0)
+                return;
+
+        dns_cache_put(&t->scope->cache,
+                      t->scope->manager->enable_cache,
+                      dns_transaction_key(t),
+                      t->answer_rcode,
+                      t->answer,
+                      DNS_PACKET_CD(t->received) ? t->received : NULL, /* only cache full packets with CD on,
+                                                                        * since our usecase for caching them
+                                                                        * is "bypass" mode which is only
+                                                                        * enabled for CD packets. */
+                      t->answer_query_flags,
+                      t->answer_dnssec_result,
+                      t->answer_nsec_ttl,
+                      t->received->family,
+                      &t->received->sender);
+}
+
+static bool dns_transaction_dnssec_is_live(DnsTransaction *t) {
+        DnsTransaction *dt;
+
+        assert(t);
+
+        SET_FOREACH(dt, t->dnssec_transactions)
+                if (DNS_TRANSACTION_IS_LIVE(dt->state))
+                        return true;
+
+        return false;
+}
+
+static int dns_transaction_dnssec_ready(DnsTransaction *t) {
+        DnsTransaction *dt;
+        int r;
+
+        assert(t);
+
+        /* Checks whether the auxiliary DNSSEC transactions of our transaction have completed, or are still
+         * ongoing. Returns 0, if we aren't ready for the DNSSEC validation, positive if we are. */
+
+        SET_FOREACH(dt, t->dnssec_transactions) {
+
+                switch (dt->state) {
+
+                case DNS_TRANSACTION_NULL:
+                case DNS_TRANSACTION_PENDING:
+                case DNS_TRANSACTION_VALIDATING:
+                        /* Still ongoing */
+                        return 0;
+
+                case DNS_TRANSACTION_RCODE_FAILURE:
+                        if (!IN_SET(dt->answer_rcode, DNS_RCODE_NXDOMAIN, DNS_RCODE_SERVFAIL)) {
+                                log_debug("Auxiliary DNSSEC RR query failed with rcode=%s.", 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
+                         * validation result */
+
+                        log_debug("Auxiliary DNSSEC RR query failed validation: %s", dnssec_result_to_string(dt->answer_dnssec_result));
+                        t->answer_dnssec_result = dt->answer_dnssec_result; /* Copy error code over */
+                        dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED);
+                        return 0;
+
+                default:
+                        log_debug("Auxiliary DNSSEC RR query failed with %s", dns_transaction_state_to_string(dt->state));
+                        goto fail;
+                }
+        }
+
+        /* All is ready, we can go and validate */
+        return 1;
+
+fail:
+        /* Some auxiliary DNSSEC transaction failed for some reason. Maybe we learned something about the
+         * server due to this failure, and the feature level is now different? Let's see and restart the
+         * transaction if so. If not, let's propagate the auxiliary failure.
+         *
+         * This is particularly relevant if an auxiliary request figured out that DNSSEC doesn't work, and we
+         * are in permissive DNSSEC mode, and thus should restart things without DNSSEC magic. */
+        r = dns_transaction_maybe_restart(t);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                return 0; /* don't validate just yet, we restarted things */
+
+        t->answer_dnssec_result = DNSSEC_FAILED_AUXILIARY;
+        dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED);
+        return 0;
+}
+
+static void dns_transaction_process_dnssec(DnsTransaction *t) {
+        int r;
+
+        assert(t);
+
+        /* Are there ongoing DNSSEC transactions? If so, let's wait for them. */
+        r = dns_transaction_dnssec_ready(t);
+        if (r < 0)
+                goto fail;
+        if (r == 0) /* We aren't ready yet (or one of our auxiliary transactions failed, and we shouldn't validate now */
+                return;
+
+        /* See if we learnt things from the additional DNSSEC transactions, that we didn't know before, and better
+         * restart the lookup immediately. */
+        r = dns_transaction_maybe_restart(t);
+        if (r < 0)
+                goto fail;
+        if (r > 0) /* Transaction got restarted... */
+                return;
+
+        /* All our auxiliary DNSSEC transactions are complete now. Try
+         * to validate our RRset now. */
+        r = dns_transaction_validate_dnssec(t);
+        if (r == -EBADMSG) {
+                dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY);
+                return;
+        }
+        if (r < 0)
+                goto fail;
+
+        if (t->answer_dnssec_result == DNSSEC_INCOMPATIBLE_SERVER &&
+            t->scope->dnssec_mode == DNSSEC_YES) {
+
+                /*  We are not in automatic downgrade mode, and the server is bad. Let's try a different server, maybe
+                 *  that works. */
+
+                if (dns_transaction_limited_retry(t))
+                        return;
+
+                /* OK, let's give up, apparently all servers we tried didn't work. */
+                dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED);
+                return;
+        }
+
+        if (!IN_SET(t->answer_dnssec_result,
+                    _DNSSEC_RESULT_INVALID,        /* No DNSSEC validation enabled */
+                    DNSSEC_VALIDATED,              /* Answer is signed and validated successfully */
+                    DNSSEC_UNSIGNED,               /* Answer is right-fully unsigned */
+                    DNSSEC_INCOMPATIBLE_SERVER)) { /* Server does not do DNSSEC (Yay, we are downgrade attack vulnerable!) */
+                dns_transaction_complete(t, DNS_TRANSACTION_DNSSEC_FAILED);
+                return;
+        }
+
+        if (t->answer_dnssec_result == DNSSEC_INCOMPATIBLE_SERVER)
+                dns_server_warn_downgrade(t->server);
+
+        dns_transaction_cache_answer(t);
+
+        if (t->answer_rcode == DNS_RCODE_SUCCESS)
+                dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
+        else
+                dns_transaction_complete(t, DNS_TRANSACTION_RCODE_FAILURE);
+
+        return;
+
+fail:
+        dns_transaction_complete_errno(t, r);
+}
+
+static int dns_transaction_has_positive_answer(DnsTransaction *t, DnsAnswerFlags *flags) {
+        int r;
+
+        assert(t);
+
+        /* Checks whether the answer is positive, i.e. either a direct
+         * answer to the question, or a CNAME/DNAME for it */
+
+        r = dns_answer_match_key(t->answer, dns_transaction_key(t), flags);
+        if (r != 0)
+                return r;
+
+        r = dns_answer_find_cname_or_dname(t->answer, dns_transaction_key(t), NULL, flags);
+        if (r != 0)
+                return r;
+
+        return false;
+}
+
+static int dns_transaction_fix_rcode(DnsTransaction *t) {
+        int r;
+
+        assert(t);
+
+        /* Fix up the RCODE to SUCCESS if we get at least one matching RR in a response. Note that this contradicts the
+         * DNS RFCs a bit. Specifically, RFC 6604 Section 3 clarifies that the RCODE shall say something about a
+         * CNAME/DNAME chain element coming after the last chain element contained in the message, and not the first
+         * one included. However, it also indicates that not all DNS servers implement this correctly. Moreover, when
+         * using DNSSEC we usually only can prove the first element of a CNAME/DNAME chain anyway, hence let's settle
+         * on always processing the RCODE as referring to the immediate look-up we do, i.e. the first element of a
+         * CNAME/DNAME chain. This way, we uniformly handle CNAME/DNAME chains, regardless if the DNS server
+         * incorrectly implements RCODE, whether DNSSEC is in use, or whether the DNS server only supplied us with an
+         * incomplete CNAME/DNAME chain.
+         *
+         * Or in other words: if we get at least one positive reply in a message we patch NXDOMAIN to become SUCCESS,
+         * and then rely on the CNAME chasing logic to figure out that there's actually a CNAME error with a new
+         * lookup. */
+
+        if (t->answer_rcode != DNS_RCODE_NXDOMAIN)
+                return 0;
+
+        r = dns_transaction_has_positive_answer(t, NULL);
+        if (r <= 0)
+                return r;
+
+        t->answer_rcode = DNS_RCODE_SUCCESS;
+        return 0;
+}
+
+void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypted) {
+        bool retry_with_tcp = false;
+        int r;
+
+        assert(t);
+        assert(p);
+        assert(t->scope);
+        assert(t->scope->manager);
+
+        if (t->state != DNS_TRANSACTION_PENDING)
+                return;
+
+        /* Note that this call might invalidate the query. Callers
+         * should hence not attempt to access the query or transaction
+         * after calling this function. */
+
+        log_debug("Processing incoming packet of size %zu on transaction %" PRIu16" (rcode=%s).",
+                  p->size,
+                  t->id, 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;
+                }
+        }
+
+        switch (t->scope->protocol) {
+
+        case DNS_PROTOCOL_DNS:
+                assert(t->server);
+
+                if (!t->bypass &&
+                    IN_SET(DNS_PACKET_RCODE(p), DNS_RCODE_FORMERR, DNS_RCODE_SERVFAIL, DNS_RCODE_NOTIMP)) {
+
+                        /* Request failed, immediately try again with reduced features */
+
+                        if (t->current_feature_level <= DNS_SERVER_FEATURE_LEVEL_UDP) {
+
+                                /* This was already at UDP feature level? If so, it doesn't make sense to downgrade
+                                 * this transaction anymore, but let's see if it might make sense to send the request
+                                 * to a different DNS server instead. If not let's process the response, and accept the
+                                 * rcode. Note that we don't retry on TCP, since that's a suitable way to mitigate
+                                 * packet loss, but is not going to give us better rcodes should we actually have
+                                 * managed to get them already at UDP level. */
+
+                                if (dns_transaction_limited_retry(t))
+                                        return;
+
+                                /* Give up, accept the rcode */
+                                log_debug("Server returned error: %s", dns_rcode_to_string(DNS_PACKET_RCODE(p)));
+                                break;
+                        }
+
+                        /* SERVFAIL can happen for many reasons and may be transient.
+                         * To avoid unnecessary downgrades retry once with the initial level.
+                         * Check for clamp_feature_level_servfail having an invalid value as a sign that this is the
+                         * first attempt to downgrade. If so, clamp to the current value so that the transaction
+                         * is retried without actually downgrading. If the next try also fails we will downgrade by
+                         * hitting the else branch below. */
+                        if (DNS_PACKET_RCODE(p) == DNS_RCODE_SERVFAIL &&
+                            t->clamp_feature_level_servfail < 0) {
+                                t->clamp_feature_level_servfail = t->current_feature_level;
+                                log_debug("Server returned error %s, retrying transaction.",
+                                          dns_rcode_to_string(DNS_PACKET_RCODE(p)));
+                        } else {
+                                /* Reduce this feature level by one and try again. */
+                                switch (t->current_feature_level) {
+                                case DNS_SERVER_FEATURE_LEVEL_TLS_DO:
+                                        t->clamp_feature_level_servfail = DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN;
+                                        break;
+                                case DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN + 1:
+                                        /* Skip plain TLS when TLS is not supported */
+                                        t->clamp_feature_level_servfail = DNS_SERVER_FEATURE_LEVEL_TLS_PLAIN - 1;
+                                        break;
+                                default:
+                                        t->clamp_feature_level_servfail = t->current_feature_level - 1;
+                                }
+
+                                log_debug("Server returned error %s, retrying transaction with reduced feature level %s.",
+                                          dns_rcode_to_string(DNS_PACKET_RCODE(p)),
+                                          dns_server_feature_level_to_string(t->clamp_feature_level_servfail));
+                        }
+
+                        dns_transaction_retry(t, false /* use the same server */);
+                        return;
+                }
+
+                if (DNS_PACKET_RCODE(p) == DNS_RCODE_REFUSED) {
+                        /* This server refused our request? If so, try again, use a different server */
+                        log_debug("Server returned REFUSED, switching servers, and retrying.");
+
+                        if (dns_transaction_limited_retry(t))
+                                return;
+
+                        break;
+                }
+
+                if (DNS_PACKET_TC(p))
+                        dns_server_packet_truncated(t->server, t->current_feature_level);
+
+                break;
+
+        case DNS_PROTOCOL_LLMNR:
+        case DNS_PROTOCOL_MDNS:
+                dns_scope_packet_received(t->scope, p->timestamp - t->start_usec);
+                break;
+
+        default:
+                assert_not_reached("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;
+                }
+
+                /* Response was truncated, let's try again with good old TCP */
+                log_debug("Reply truncated, retrying via TCP.");
+                retry_with_tcp = true;
+
+        } else if (t->scope->protocol == DNS_PROTOCOL_DNS &&
+                   DNS_PACKET_IS_FRAGMENTED(p)) {
+
+                /* Report the fragment size, so that we downgrade from LARGE to regular EDNS0 if needed */
+                if (t->server)
+                        dns_server_packet_udp_fragmented(t->server, dns_packet_size_unfragmented(p));
+
+                if (t->current_feature_level > DNS_SERVER_FEATURE_LEVEL_UDP) {
+                        /* Packet was fragmented. Let's retry with TCP to avoid fragmentation attack
+                         * issues. (We don't do that on the lowest feature level however, since crappy DNS
+                         * servers often do not implement TCP, hence falling back to TCP on fragmentation is
+                         * counter-productive there.) */
+
+                        log_debug("Reply fragmented, retrying via TCP. (Largest fragment size: %zu; Datagram size: %zu)",
+                                  p->fragsize, p->size);
+                        retry_with_tcp = true;
+                }
+        }
+
+        if (retry_with_tcp) {
+                r = dns_transaction_emit_tcp(t);
+                if (r == -ESRCH) {
+                        /* No servers found? Damn! */
+                        dns_transaction_complete(t, DNS_TRANSACTION_NO_SERVERS);
+                        return;
+                }
+                if (r == -EOPNOTSUPP) {
+                        /* Tried to ask for DNSSEC RRs, on a server that doesn't do DNSSEC  */
+                        dns_transaction_complete(t, DNS_TRANSACTION_RR_TYPE_UNSUPPORTED);
+                        return;
+                }
+                if (r < 0) {
+                        /* On LLMNR, if we cannot connect to the host,
+                         * we immediately give up */
+                        if (t->scope->protocol != DNS_PROTOCOL_DNS)
+                                goto fail;
+
+                        /* On DNS, couldn't send? Try immediately again, with a new server */
+                        if (dns_transaction_limited_retry(t))
+                                return;
+
+                        /* No new server to try, give up */
+                        dns_transaction_complete(t, DNS_TRANSACTION_ATTEMPTS_MAX_REACHED);
+                }
+
+                return;
+        }
+
+        /* After the superficial checks, actually parse the message. */
+        r = dns_packet_extract(p);
+        if (r < 0) {
+                if (t->server) {
+                        dns_server_packet_invalid(t->server, t->current_feature_level);
+
+                        r = dns_transaction_maybe_restart(t);
+                        if (r < 0)
+                                goto fail;
+                        if (r > 0) /* Transaction got restarted... */
+                                return;
+                }
+
+                dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY);
+                return;
+        }
+
+        if (t->scope->protocol == DNS_PROTOCOL_DNS &&
+            !t->bypass &&
+            DNS_PACKET_RCODE(p) == DNS_RCODE_NXDOMAIN &&
+            p->opt && !DNS_PACKET_DO(p) &&
+            DNS_SERVER_FEATURE_LEVEL_IS_EDNS0(t->current_feature_level) &&
+            DNS_SERVER_FEATURE_LEVEL_IS_UDP(t->current_feature_level) &&
+            t->scope->dnssec_mode != DNSSEC_YES) {
+
+                /* Some captive portals are special in that the Aruba/Datavalet hardware will miss
+                 * replacing the packets with the local server IP to point to the authenticated side
+                 * of the network if EDNS0 is enabled. Instead they return NXDOMAIN, with DO bit set
+                 * to zero... nothing to see here, yet respond with the captive portal IP, when using
+                 * the more simple UDP level.
+                 *
+                 * Common portal names that fail like so are:
+                 *     secure.datavalet.io
+                 *     securelogin.arubanetworks.com
+                 *     securelogin.networks.mycompany.com
+                 *
+                 * Thus retry NXDOMAIN RCODES with a lower feature level.
+                 *
+                 * Do not lower the server's tracked feature level, as the captive portal should not
+                 * be lying for the wider internet (e.g. _other_ queries were observed fine with
+                 * EDNS0 on these networks, post auth), i.e. let's just lower the level transaction's
+                 * feature level.
+                 *
+                 * This is reported as https://github.com/dns-violations/dns-violations/blob/master/2018/DVE-2018-0001.md
+                 */
+
+                t->clamp_feature_level_nxdomain = DNS_SERVER_FEATURE_LEVEL_UDP;
+
+                log_debug("Server returned error %s in EDNS0 mode, retrying transaction with reduced feature level %s (DVE-2018-0001 mitigation)",
+                          dns_rcode_to_string(DNS_PACKET_RCODE(p)),
+                          dns_server_feature_level_to_string(t->clamp_feature_level_nxdomain));
+
+                dns_transaction_retry(t, false /* use the same server */);
+                return;
+        }
+
+        if (t->server) {
+                /* Report that we successfully received a valid packet with a good rcode after we initially got a bad
+                 * rcode and subsequently downgraded the protocol */
+
+                if (IN_SET(DNS_PACKET_RCODE(p), DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN) &&
+                    t->clamp_feature_level_servfail != _DNS_SERVER_FEATURE_LEVEL_INVALID)
+                        dns_server_packet_rcode_downgrade(t->server, t->clamp_feature_level_servfail);
+
+                /* Report that the OPT RR was missing */
+                if (!p->opt)
+                        dns_server_packet_bad_opt(t->server, t->current_feature_level);
+
+                /* Report that the server didn't copy our query DO bit from request to response */
+                if (DNS_PACKET_DO(t->sent) && !DNS_PACKET_DO(t->received))
+                        dns_server_packet_do_off(t->server, t->current_feature_level);
+
+                /* Report that we successfully received a packet. We keep track of the largest packet
+                 * size/fragment size we got. Which is useful for announcing the EDNS(0) packet size we can
+                 * receive to our server. */
+                dns_server_packet_received(t->server, p->ipproto, t->current_feature_level, dns_packet_size_unfragmented(p));
+        }
+
+        /* See if we know things we didn't know before that indicate we better restart the lookup immediately. */
+        r = dns_transaction_maybe_restart(t);
+        if (r < 0)
+                goto fail;
+        if (r > 0) /* Transaction got restarted... */
+                return;
+
+        /* When dealing with protocols other than mDNS only consider responses with equivalent query section
+         * to the request. For mDNS this check doesn't make sense, because the section 6 of RFC6762 states
+         * that "Multicast DNS responses MUST NOT contain any questions in the Question Section". */
+        if (t->scope->protocol != DNS_PROTOCOL_MDNS) {
+                r = dns_packet_is_reply_for(p, dns_transaction_key(t));
+                if (r < 0)
+                        goto fail;
+                if (r == 0) {
+                        dns_transaction_complete(t, DNS_TRANSACTION_INVALID_REPLY);
+                        return;
+                }
+        }
+
+        /* Install the answer as answer to the transaction. We ref the answer twice here: the main `answer`
+         * field is later replaced by the DNSSEC validated subset. The 'answer_auxiliary' field carries the
+         * original complete record set, including RRSIG and friends. We use this when passing data to
+         * clients that ask for DNSSEC metadata. */
+        dns_answer_unref(t->answer);
+        t->answer = dns_answer_ref(p->answer);
+        t->answer_rcode = DNS_PACKET_RCODE(p);
+        t->answer_dnssec_result = _DNSSEC_RESULT_INVALID;
+        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false);
+        SET_FLAG(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL, encrypted);
+
+        r = dns_transaction_fix_rcode(t);
+        if (r < 0)
+                goto fail;
+
+        /* Block GC while starting requests for additional DNSSEC RRs */
+        t->block_gc++;
+        r = dns_transaction_request_dnssec_keys(t);
+        t->block_gc--;
+
+        /* Maybe the transaction is ready for GC'ing now? If so, free it and return. */
+        if (!dns_transaction_gc(t))
+                return;
+
+        /* Requesting additional keys might have resulted in this transaction to fail, since the auxiliary
+         * request failed for some reason. If so, we are not in pending state anymore, and we should exit
+         * quickly. */
+        if (t->state != DNS_TRANSACTION_PENDING)
+                return;
+        if (r < 0)
+                goto fail;
+        if (r > 0) {
+                /* There are DNSSEC transactions pending now. Update the state accordingly. */
+                t->state = DNS_TRANSACTION_VALIDATING;
+                dns_transaction_close_connection(t, true);
+                dns_transaction_stop_timeout(t);
+                return;
+        }
+
+        dns_transaction_process_dnssec(t);
+        return;
+
+fail:
+        dns_transaction_complete_errno(t, r);
+}
+
+static int on_dns_packet(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        DnsTransaction *t = 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_close_connection(t, /* use_graveyard = */ false);
+
+                if (dns_transaction_limited_retry(t)) /* Try a different server */
+                        return 0;
+
+                dns_transaction_complete_errno(t, r);
+                return 0;
+        }
+        if (r < 0) {
+                dns_transaction_complete_errno(t, r);
+                return 0;
+        }
+        if (r == 0)
+                /* Spurious wakeup without any data */
+                return 0;
+
+        r = dns_packet_validate_reply(p);
+        if (r < 0) {
+                log_debug_errno(r, "Received invalid DNS packet as response, ignoring: %m");
+                return 0;
+        }
+        if (r == 0) {
+                log_debug("Received inappropriate DNS packet as response, ignoring.");
+                return 0;
+        }
+
+        if (DNS_PACKET_ID(p) != t->id) {
+                log_debug("Received packet with incorrect transaction ID, ignoring.");
+                return 0;
+        }
+
+        dns_transaction_process_reply(t, p, false);
+        return 0;
+}
+
+static int dns_transaction_emit_udp(DnsTransaction *t) {
+        int r;
+
+        assert(t);
+
+        if (t->scope->protocol == DNS_PROTOCOL_DNS) {
+
+                r = dns_transaction_pick_server(t);
+                if (r < 0)
+                        return r;
+
+                if (manager_server_is_stub(t->scope->manager, t->server))
+                        return -ELOOP;
+
+                if (t->current_feature_level < DNS_SERVER_FEATURE_LEVEL_UDP || DNS_SERVER_FEATURE_LEVEL_IS_TLS(t->current_feature_level))
+                        return -EAGAIN; /* Sorry, can't do UDP, try TCP! */
+
+                if (!t->bypass && !dns_server_dnssec_supported(t->server) && dns_type_is_dnssec(dns_transaction_key(t)->type))
+                        return -EOPNOTSUPP;
+
+                if (r > 0 || t->dns_udp_fd < 0) { /* Server changed, or no connection yet. */
+                        int fd;
+
+                        dns_transaction_close_connection(t, true);
+
+                        /* Before we allocate a new UDP socket, let's process the graveyard a bit to free some fds */
+                        manager_socket_graveyard_process(t->scope->manager);
+
+                        fd = dns_scope_socket_udp(t->scope, t->server);
+                        if (fd < 0)
+                                return fd;
+
+                        r = sd_event_add_io(t->scope->manager->event, &t->dns_udp_event_source, fd, EPOLLIN, on_dns_packet, t);
+                        if (r < 0) {
+                                safe_close(fd);
+                                return r;
+                        }
+
+                        (void) sd_event_source_set_description(t->dns_udp_event_source, "dns-transaction-udp");
+                        t->dns_udp_fd = fd;
+                }
+
+                if (!t->bypass) {
+                        r = dns_server_adjust_opt(t->server, t->sent, t->current_feature_level);
+                        if (r < 0)
+                                return r;
+                }
+        } else
+                dns_transaction_close_connection(t, true);
+
+        r = dns_scope_emit_udp(t->scope, t->dns_udp_fd, t->server ? t->server->family : AF_UNSPEC, t->sent);
+        if (r < 0)
+                return r;
+
+        dns_transaction_reset_answer(t);
+
+        return 0;
+}
+
+static int on_transaction_timeout(sd_event_source *s, usec_t usec, void *userdata) {
+        DnsTransaction *t = userdata;
+
+        assert(s);
+        assert(t);
+
+        if (t->initial_jitter_scheduled && !t->initial_jitter_elapsed) {
+                log_debug("Initial jitter phase for transaction %" PRIu16 " elapsed.", t->id);
+                t->initial_jitter_elapsed = true;
+        } else {
+                /* Timeout reached? Increase the timeout for the server used */
+                switch (t->scope->protocol) {
+
+                case DNS_PROTOCOL_DNS:
+                        assert(t->server);
+                        dns_server_packet_lost(t->server, t->stream ? IPPROTO_TCP : IPPROTO_UDP, t->current_feature_level);
+                        break;
+
+                case DNS_PROTOCOL_LLMNR:
+                case DNS_PROTOCOL_MDNS:
+                        dns_scope_packet_lost(t->scope, usec - t->start_usec);
+                        break;
+
+                default:
+                        assert_not_reached("Invalid DNS protocol.");
+                }
+
+                log_debug("Timeout reached on transaction %" PRIu16 ".", t->id);
+        }
+
+        dns_transaction_retry(t, /* next_server= */ true); /* try a different server, but given this means
+                                                            * packet loss, let's do so even if we already
+                                                            * tried a bunch */
+        return 0;
+}
+
+static 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 void dns_transaction_randomize_answer(DnsTransaction *t) {
+        int r;
+
+        assert(t);
+
+        /* Randomizes the order of the answer array. This is done for all cached responses, so that we return
+         * a different order each time. We do this only for DNS traffic, in order to do some minimal, crappy
+         * load balancing. We don't do this for LLMNR or mDNS, since the order (preferring link-local
+         * addresses, and such like) might have meaning there, and load balancing is pointless. */
+
+        if (t->scope->protocol != DNS_PROTOCOL_DNS)
+                return;
+
+        /* No point in randomizing, if there's just one RR */
+        if (dns_answer_size(t->answer) <= 1)
+                return;
+
+        r = dns_answer_reserve_or_clone(&t->answer, 0);
+        if (r < 0) /* If this fails, just don't randomize, this is non-essential stuff after all */
+                return (void) log_debug_errno(r, "Failed to clone answer record, not randomizing RR order of answer: %m");
+
+        dns_answer_randomize(t->answer);
+}
+
+static int dns_transaction_prepare(DnsTransaction *t, usec_t ts) {
+        int r;
+
+        assert(t);
+
+        /* Returns 0 if dns_transaction_complete() has been called. In that case the transaction and query
+         * candidate objects may have been invalidated and must not be accessed. Returns 1 if the transaction
+         * has been prepared. */
+
+        dns_transaction_stop_timeout(t);
+
+        if (!dns_scope_network_good(t->scope)) {
+                dns_transaction_complete(t, DNS_TRANSACTION_NETWORK_DOWN);
+                return 0;
+        }
+
+        if (t->n_attempts >= TRANSACTION_ATTEMPTS_MAX(t->scope->protocol)) {
+                DnsTransactionState result;
+
+                if (t->scope->protocol == DNS_PROTOCOL_LLMNR)
+                        /* If we didn't find anything on LLMNR, it's not an error, but a failure to resolve
+                         * the name. */
+                        result = DNS_TRANSACTION_NOT_FOUND;
+                else
+                        result = DNS_TRANSACTION_ATTEMPTS_MAX_REACHED;
+
+                dns_transaction_complete(t, result);
+                return 0;
+        }
+
+        if (t->scope->protocol == DNS_PROTOCOL_LLMNR && t->tried_stream) {
+                /* If we already tried via a stream, then we don't
+                 * retry on LLMNR. See RFC 4795, Section 2.7. */
+                dns_transaction_complete(t, DNS_TRANSACTION_ATTEMPTS_MAX_REACHED);
+                return 0;
+        }
+
+        t->n_attempts++;
+        t->start_usec = ts;
+
+        dns_transaction_reset_answer(t);
+        dns_transaction_flush_dnssec_transactions(t);
+
+        /* Check the trust anchor. Do so only on classic DNS, since DNSSEC does not apply otherwise. */
+        if (t->scope->protocol == DNS_PROTOCOL_DNS &&
+            !FLAGS_SET(t->query_flags, SD_RESOLVED_NO_TRUST_ANCHOR)) {
+                r = dns_trust_anchor_lookup_positive(&t->scope->manager->trust_anchor, dns_transaction_key(t), &t->answer);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        t->answer_rcode = DNS_RCODE_SUCCESS;
+                        t->answer_source = DNS_TRANSACTION_TRUST_ANCHOR;
+                        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL, true);
+                        dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
+                        return 0;
+                }
+
+                if (dns_name_is_root(dns_resource_key_name(dns_transaction_key(t))) &&
+                    dns_transaction_key(t)->type == DNS_TYPE_DS) {
+
+                        /* Hmm, this is a request for the root DS? A DS RR doesn't exist in the root zone,
+                         * and if our trust anchor didn't know it either, this means we cannot do any DNSSEC
+                         * logic anymore. */
+
+                        if (t->scope->dnssec_mode == DNSSEC_ALLOW_DOWNGRADE) {
+                                /* We are in downgrade mode. In this case, synthesize an unsigned empty
+                                 * response, so that the any lookup depending on this one can continue
+                                 * assuming there was no DS, and hence the root zone was unsigned. */
+
+                                t->answer_rcode = DNS_RCODE_SUCCESS;
+                                t->answer_source = DNS_TRANSACTION_TRUST_ANCHOR;
+                                SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false);
+                                SET_FLAG(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL, true);
+                                dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
+                        } else
+                                /* If we are not in downgrade mode, then fail the lookup, because we cannot
+                                 * reasonably answer it. There might be DS RRs, but we don't know them, and
+                                 * the DNS server won't tell them to us (and even if it would, we couldn't
+                                 * validate and trust them. */
+                                dns_transaction_complete(t, DNS_TRANSACTION_NO_TRUST_ANCHOR);
+
+                        return 0;
+                }
+        }
+
+        /* Check the zone. */
+        if (!FLAGS_SET(t->query_flags, SD_RESOLVED_NO_ZONE)) {
+                r = dns_zone_lookup(&t->scope->zone, dns_transaction_key(t), dns_scope_ifindex(t->scope), &t->answer, NULL, NULL);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        t->answer_rcode = DNS_RCODE_SUCCESS;
+                        t->answer_source = DNS_TRANSACTION_ZONE;
+                        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL, true);
+                        dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
+                        return 0;
+                }
+        }
+
+        /* Check the cache. */
+        if (!FLAGS_SET(t->query_flags, SD_RESOLVED_NO_CACHE)) {
+
+                /* Before trying the cache, let's make sure we figured out a server to use. Should this cause
+                 * a change of server this might flush the cache. */
+                (void) dns_scope_get_dns_server(t->scope);
+
+                /* Let's then prune all outdated entries */
+                dns_cache_prune(&t->scope->cache);
+
+                r = dns_cache_lookup(
+                                &t->scope->cache,
+                                dns_transaction_key(t),
+                                t->query_flags,
+                                &t->answer_rcode,
+                                &t->answer,
+                                &t->received,
+                                &t->answer_query_flags,
+                                &t->answer_dnssec_result);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        dns_transaction_randomize_answer(t);
+
+                        if (t->bypass && t->scope->protocol == DNS_PROTOCOL_DNS && !t->received)
+                                /* When bypass mode is on, do not use cached data unless it came with a full
+                                 * packet. */
+                                dns_transaction_reset_answer(t);
+                        else {
+                                t->answer_source = DNS_TRANSACTION_CACHE;
+                                if (t->answer_rcode == DNS_RCODE_SUCCESS)
+                                        dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
+                                else
+                                        dns_transaction_complete(t, DNS_TRANSACTION_RCODE_FAILURE);
+                                return 0;
+                        }
+                }
+        }
+
+        if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_NETWORK)) {
+                dns_transaction_complete(t, DNS_TRANSACTION_NO_SOURCE);
+                return 0;
+        }
+
+        return 1;
+}
+
+static int dns_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, dns_transaction_key(t), 0, NULL);
+        if (r < 0)
+                return r;
+
+        qdcount = 1;
+
+        if (dns_key_is_shared(dns_transaction_key(t)))
+                add_known_answers = true;
+
+        if (dns_transaction_key(t)->type == DNS_TYPE_ANY) {
+                r = set_ensure_put(&keys, &dns_resource_key_hash_ops, dns_transaction_key(t));
+                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, dns_transaction_key(other), 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(dns_transaction_key(other)))
+                        add_known_answers = true;
+
+                if (dns_transaction_key(other)->type == DNS_TYPE_ANY) {
+                        r = set_ensure_put(&keys, &dns_resource_key_hash_ops, dns_transaction_key(other));
+                        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, &nscount);
+                if (r < 0)
+                        return r;
+        }
+        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;
+
+        if (t->bypass && t->bypass->protocol == t->scope->protocol) {
+                /* If bypass logic is enabled and the protocol if the original packet and our scope match,
+                 * take the original packet, copy it, and patch in our new ID */
+                r = dns_packet_dup(&p, t->bypass);
+                if (r < 0)
+                        return r;
+        } else {
+                r = dns_packet_new_query(
+                                &p, t->scope->protocol,
+                                /* min_alloc_dsize = */ 0,
+                                /* dnssec_cd = */ !FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) &&
+                                                  t->scope->dnssec_mode != DNSSEC_NO);
+                if (r < 0)
+                        return r;
+
+                r = dns_packet_append_key(p, dns_transaction_key(t), 0, NULL);
+                if (r < 0)
+                        return r;
+
+                DNS_PACKET_HEADER(p)->qdcount = htobe16(1);
+        }
+
+        DNS_PACKET_HEADER(p)->id = t->id;
+
+        t->sent = TAKE_PTR(p);
+        return 0;
+}
+
+int dns_transaction_go(DnsTransaction *t) {
+        usec_t ts;
+        int r;
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+
+        assert(t);
+
+        /* Returns > 0 if the transaction is now pending, returns 0 if could be processed immediately and has
+         * finished now. In the latter case, the transaction and query candidate objects must not be accessed.
+         */
+
+        assert_se(sd_event_now(t->scope->manager->event, clock_boottime_or_monotonic(), &ts) >= 0);
+
+        r = dns_transaction_prepare(t, ts);
+        if (r <= 0)
+                return r;
+
+        log_debug("Firing %s transaction %" PRIu16 " for <%s> scope %s on %s/%s (validate=%s).",
+                  t->bypass ? "bypass" : "regular",
+                  t->id,
+                  dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str),
+                  dns_protocol_to_string(t->scope->protocol),
+                  t->scope->link ? t->scope->link->ifname : "*",
+                  af_to_name_short(t->scope->family),
+                  yes_no(!FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE)));
+
+        if (!t->initial_jitter_scheduled &&
+            IN_SET(t->scope->protocol, DNS_PROTOCOL_LLMNR, DNS_PROTOCOL_MDNS)) {
+                usec_t jitter, 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;
+
+                switch (t->scope->protocol) {
+
+                case DNS_PROTOCOL_LLMNR:
+                        jitter = random_u64_range(LLMNR_JITTER_INTERVAL_USEC);
+                        accuracy = LLMNR_JITTER_INTERVAL_USEC;
+                        break;
+
+                case DNS_PROTOCOL_MDNS:
+                        jitter = usec_add(random_u64_range(MDNS_JITTER_RANGE_USEC), MDNS_JITTER_MIN_USEC);
+                        accuracy = MDNS_JITTER_RANGE_USEC;
+                        break;
+                default:
+                        assert_not_reached("bad protocol");
+                }
+
+                assert(!t->timeout_event_source);
+
+                r = sd_event_add_time_relative(
+                                t->scope->manager->event,
+                                &t->timeout_event_source,
+                                clock_boottime_or_monotonic(),
+                                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 %" PRIu16 " for " USEC_FMT "us.",
+                          dns_protocol_to_string(t->scope->protocol),
+                          t->id,
+                          jitter);
+                return 1;
+        }
+
+        /* Otherwise, we need to ask the network */
+        r = dns_transaction_make_packet(t);
+        if (r < 0)
+                return r;
+
+        if (t->scope->protocol == DNS_PROTOCOL_LLMNR &&
+            (dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), "in-addr.arpa") > 0 ||
+             dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), "ip6.arpa") > 0)) {
+
+                /* RFC 4795, Section 2.4. says reverse lookups shall
+                 * always be made via TCP on LLMNR */
+                r = dns_transaction_emit_tcp(t);
+        } else {
+                /* Try via UDP, and if that fails due to large size or lack of
+                 * support try via TCP */
+                r = dns_transaction_emit_udp(t);
+                if (r == -EMSGSIZE)
+                        log_debug("Sending query via TCP since it is too large.");
+                else if (r == -EAGAIN)
+                        log_debug("Sending query via TCP since UDP isn't supported or DNS-over-TLS is selected.");
+                if (IN_SET(r, -EMSGSIZE, -EAGAIN))
+                        r = dns_transaction_emit_tcp(t);
+        }
+        if (r == -ELOOP) {
+                if (t->scope->protocol != DNS_PROTOCOL_DNS)
+                        return r;
+
+                /* One of our own stub listeners */
+                log_debug_errno(r, "Detected that specified DNS server is our own extra listener, switching DNS servers.");
+
+                dns_scope_next_dns_server(t->scope, t->server);
+
+                if (dns_scope_get_dns_server(t->scope) == t->server) {
+                        log_debug_errno(r, "Still pointing to extra listener after switching DNS servers, refusing operation.");
+                        dns_transaction_complete(t, DNS_TRANSACTION_STUB_LOOP);
+                        return 0;
+                }
+
+                return dns_transaction_go(t);
+        }
+        if (r == -ESRCH) {
+                /* No servers to send this to? */
+                dns_transaction_complete(t, DNS_TRANSACTION_NO_SERVERS);
+                return 0;
+        }
+        if (r == -EOPNOTSUPP) {
+                /* Tried to ask for DNSSEC RRs, on a server that doesn't do DNSSEC  */
+                dns_transaction_complete(t, DNS_TRANSACTION_RR_TYPE_UNSUPPORTED);
+                return 0;
+        }
+        if (t->scope->protocol == DNS_PROTOCOL_LLMNR && ERRNO_IS_DISCONNECT(r)) {
+                /* On LLMNR, if we cannot connect to a host via TCP when doing reverse lookups. This means we cannot
+                 * answer this request with this protocol. */
+                dns_transaction_complete(t, DNS_TRANSACTION_NOT_FOUND);
+                return 0;
+        }
+        if (r < 0) {
+                if (t->scope->protocol != DNS_PROTOCOL_DNS)
+                        return r;
+
+                /* Couldn't send? Try immediately again, with a new server */
+                dns_scope_next_dns_server(t->scope, t->server);
+
+                return dns_transaction_go(t);
+        }
+
+        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, t->query_flags);
+        if (!aux) {
+                r = dns_transaction_new(&aux, t->scope, key, NULL, t->query_flags);
+                if (r < 0)
+                        return r;
+        } else {
+                if (set_contains(t->dnssec_transactions, aux)) {
+                        *ret = aux;
+                        return 0;
+                }
+
+                r = dns_transaction_find_cyclic(t, aux);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        char s[DNS_RESOURCE_KEY_STRING_MAX], saux[DNS_RESOURCE_KEY_STRING_MAX];
+
+                        return log_debug_errno(SYNTHETIC_ERRNO(ELOOP),
+                                               "Potential cyclic dependency, refusing to add transaction %" PRIu16 " (%s) as dependency for %" PRIu16 " (%s).",
+                                               aux->id,
+                                               dns_resource_key_to_string(dns_transaction_key(t), s, sizeof s),
+                                               t->id,
+                                               dns_resource_key_to_string(dns_transaction_key(aux), saux, sizeof saux));
+                }
+        }
+
+        r = set_ensure_allocated(&aux->notify_transactions_done, NULL);
+        if (r < 0)
+                return r;
+
+        r = set_ensure_put(&t->dnssec_transactions, NULL, aux);
+        if (r < 0)
+                return r;
+
+        r = set_ensure_put(&aux->notify_transactions, NULL, t);
+        if (r < 0) {
+                (void) set_remove(t->dnssec_transactions, aux);
+                return r;
+        }
+
+        *ret = TAKE_PTR(aux);
+        return 1;
+}
+
+static int dns_transaction_request_dnssec_rr(DnsTransaction *t, DnsResourceKey *key) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *a = NULL;
+        DnsTransaction *aux;
+        int r;
+
+        assert(t);
+        assert(key);
+
+        /* Try to get the data from the trust anchor */
+        r = dns_trust_anchor_lookup_positive(&t->scope->manager->trust_anchor, key, &a);
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                r = dns_answer_extend(&t->validated_keys, a);
+                if (r < 0)
+                        return r;
+
+                return 0;
+        }
+
+        /* This didn't work, ask for it via the network/cache then. */
+        r = dns_transaction_add_dnssec_transaction(t, key, &aux);
+        if (r == -ELOOP) /* This would result in a cyclic dependency */
+                return 0;
+        if (r < 0)
+                return r;
+
+        if (aux->state == DNS_TRANSACTION_NULL) {
+                r = dns_transaction_go(aux);
+                if (r < 0)
+                        return r;
+        }
+
+        return 1;
+}
+
+static int dns_transaction_negative_trust_anchor_lookup(DnsTransaction *t, const char *name) {
+        int r;
+
+        assert(t);
+
+        /* Check whether the specified name is in the NTA
+         * database, either in the global one, or the link-local
+         * one. */
+
+        r = dns_trust_anchor_lookup_negative(&t->scope->manager->trust_anchor, name);
+        if (r != 0)
+                return r;
+
+        if (!t->scope->link)
+                return 0;
+
+        return link_negative_trust_anchor_lookup(t->scope->link, name);
+}
+
+static int dns_transaction_has_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(dns_transaction_key(t)));
+        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. */
+
+        return !dns_answer_contains_nsec_or_nsec3(t->answer);
+}
+
+static int dns_transaction_is_primary_response(DnsTransaction *t, DnsResourceRecord *rr) {
+        int r;
+
+        assert(t);
+        assert(rr);
+
+        /* Check if the specified RR is the "primary" response,
+         * i.e. either matches the question precisely or is a
+         * CNAME/DNAME for it. */
+
+        r = dns_resource_key_match_rr(dns_transaction_key(t), rr, NULL);
+        if (r != 0)
+                return r;
+
+        return dns_resource_key_match_cname_or_dname(dns_transaction_key(t), rr->key, NULL);
+}
+
+static bool dns_transaction_dnssec_supported(DnsTransaction *t) {
+        assert(t);
+
+        /* Checks whether our transaction's DNS server is assumed to be compatible with DNSSEC. Returns false as soon
+         * as we changed our mind about a server, and now believe it is incompatible with DNSSEC. */
+
+        if (t->scope->protocol != DNS_PROTOCOL_DNS)
+                return false;
+
+        /* If we have picked no server, then we are working from the cache or some other source, and DNSSEC might well
+         * be supported, hence return true. */
+        if (!t->server)
+                return true;
+
+        /* Note that we do not check the feature level actually used for the transaction but instead the feature level
+         * the server is known to support currently, as the transaction feature level might be lower than what the
+         * server actually supports, since we might have downgraded this transaction's feature level because we got a
+         * SERVFAIL earlier and wanted to check whether downgrading fixes it. */
+
+        return dns_server_dnssec_supported(t->server);
+}
+
+static bool dns_transaction_dnssec_supported_full(DnsTransaction *t) {
+        DnsTransaction *dt;
+
+        assert(t);
+
+        /* Checks whether our transaction our any of the auxiliary transactions couldn't do DNSSEC. */
+
+        if (!dns_transaction_dnssec_supported(t))
+                return false;
+
+        SET_FOREACH(dt, t->dnssec_transactions)
+                if (!dns_transaction_dnssec_supported(dt))
+                        return false;
+
+        return true;
+}
+
+int dns_transaction_request_dnssec_keys(DnsTransaction *t) {
+        DnsResourceRecord *rr;
+
+        int r;
+
+        assert(t);
+
+        /*
+         * Retrieve all auxiliary RRs for the answer we got, so that
+         * we can verify signatures or prove that RRs are rightfully
+         * unsigned. Specifically:
+         *
+         * - For RRSIG we get the matching DNSKEY
+         * - For DNSKEY we get the matching DS
+         * - For unsigned SOA/NS we get the matching DS
+         * - For unsigned CNAME/DNAME/DS we get the parent SOA RR
+         * - For other unsigned RRs we get the matching SOA RR
+         * - For SOA/NS queries with no matching response RR, and no NSEC/NSEC3, the DS RR
+         * - For DS queries with no matching response RRs, and no NSEC/NSEC3, the parent's SOA RR
+         * - For other queries with no matching response RRs, and no NSEC/NSEC3, the SOA RR
+         */
+
+        if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) || t->scope->dnssec_mode == DNSSEC_NO)
+                return 0;
+        if (t->answer_source != DNS_TRANSACTION_NETWORK)
+                return 0; /* We only need to validate stuff from the network */
+        if (!dns_transaction_dnssec_supported(t))
+                return 0; /* If we can't do DNSSEC anyway there's no point in getting the auxiliary RRs */
+
+        DNS_ANSWER_FOREACH(rr, t->answer) {
+
+                if (dns_type_is_pseudo(rr->key->type))
+                        continue;
+
+                /* If this RR is in the negative trust anchor, we don't need to validate it. */
+                r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(rr->key));
+                if (r < 0)
+                        return r;
+                if (r > 0)
+                        continue;
+
+                switch (rr->key->type) {
+
+                case DNS_TYPE_RRSIG: {
+                        /* For each RRSIG we request the matching DNSKEY */
+                        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *dnskey = NULL;
+
+                        /* If this RRSIG is about a DNSKEY RR and the
+                         * signer is the same as the owner, then we
+                         * already have the DNSKEY, and we don't have
+                         * to look for more. */
+                        if (rr->rrsig.type_covered == DNS_TYPE_DNSKEY) {
+                                r = dns_name_equal(rr->rrsig.signer, dns_resource_key_name(rr->key));
+                                if (r < 0)
+                                        return r;
+                                if (r > 0)
+                                        continue;
+                        }
+
+                        /* If the signer is not a parent of our
+                         * original query, then this is about an
+                         * auxiliary RRset, but not anything we asked
+                         * for. In this case we aren't interested,
+                         * because we don't want to request additional
+                         * RRs for stuff we didn't really ask for, and
+                         * also to avoid request loops, where
+                         * additional RRs from one transaction result
+                         * in another transaction whose additional RRs
+                         * point back to the original transaction, and
+                         * we deadlock. */
+                        r = dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), rr->rrsig.signer);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        dnskey = dns_resource_key_new(rr->key->class, DNS_TYPE_DNSKEY, rr->rrsig.signer);
+                        if (!dnskey)
+                                return -ENOMEM;
+
+                        log_debug("Requesting DNSKEY to validate transaction %" PRIu16" (%s, RRSIG with key tag: %" PRIu16 ").",
+                                  t->id, dns_resource_key_name(rr->key), rr->rrsig.key_tag);
+                        r = dns_transaction_request_dnssec_rr(t, dnskey);
+                        if (r < 0)
+                                return r;
+                        break;
+                }
+
+                case DNS_TYPE_DNSKEY: {
+                        /* For each DNSKEY we request the matching DS */
+                        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *ds = NULL;
+
+                        /* If the DNSKEY we are looking at is not for
+                         * zone we are interested in, nor any of its
+                         * parents, we aren't interested, and don't
+                         * request it. After all, we don't want to end
+                         * up in request loops, and want to keep
+                         * additional traffic down. */
+
+                        r = dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), dns_resource_key_name(rr->key));
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        ds = dns_resource_key_new(rr->key->class, DNS_TYPE_DS, dns_resource_key_name(rr->key));
+                        if (!ds)
+                                return -ENOMEM;
+
+                        log_debug("Requesting DS to validate transaction %" PRIu16" (%s, DNSKEY with key tag: %" PRIu16 ").",
+                                  t->id, dns_resource_key_name(rr->key), dnssec_keytag(rr, false));
+                        r = dns_transaction_request_dnssec_rr(t, ds);
+                        if (r < 0)
+                                return r;
+
+                        break;
+                }
+
+                case DNS_TYPE_SOA:
+                case DNS_TYPE_NS: {
+                        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *ds = NULL;
+
+                        /* For an unsigned SOA or NS, try to acquire
+                         * the matching DS RR, as we are at a zone cut
+                         * then, and whether a DS exists tells us
+                         * whether the zone is signed. Do so only if
+                         * this RR matches our original question,
+                         * however. */
+
+                        r = dns_resource_key_match_rr(dns_transaction_key(t), rr, NULL);
+                        if (r < 0)
+                                return r;
+                        if (r == 0) {
+                                /* Hmm, so this SOA RR doesn't match our original question. In this case, maybe this is
+                                 * a negative reply, and we need the SOA RR's TTL in order to cache a negative entry?
+                                 * If so, we need to validate it, too. */
+
+                                r = dns_answer_match_key(t->answer, dns_transaction_key(t), NULL);
+                                if (r < 0)
+                                        return r;
+                                if (r > 0) /* positive reply, we won't need the SOA and hence don't need to validate
+                                            * it. */
+                                        continue;
+
+                                /* Only bother with this if the SOA/NS RR we are looking at is actually a parent of
+                                 * what we are looking for, otherwise there's no value in it for us. */
+                                r = dns_name_endswith(dns_resource_key_name(dns_transaction_key(t)), dns_resource_key_name(rr->key));
+                                if (r < 0)
+                                        return r;
+                                if (r == 0)
+                                        continue;
+                        }
+
+                        r = dnssec_has_rrsig(t->answer, rr->key);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                continue;
+
+                        ds = dns_resource_key_new(rr->key->class, DNS_TYPE_DS, dns_resource_key_name(rr->key));
+                        if (!ds)
+                                return -ENOMEM;
+
+                        log_debug("Requesting DS to validate transaction %" PRIu16 " (%s, unsigned SOA/NS RRset).",
+                                  t->id, dns_resource_key_name(rr->key));
+                        r = dns_transaction_request_dnssec_rr(t, ds);
+                        if (r < 0)
+                                return r;
+
+                        break;
+                }
+
+                case DNS_TYPE_DS:
+                case DNS_TYPE_CNAME:
+                case DNS_TYPE_DNAME: {
+                        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *soa = NULL;
+                        const char *name;
+
+                        /* CNAMEs and DNAMEs cannot be located at a
+                         * zone apex, hence ask for the parent SOA for
+                         * unsigned CNAME/DNAME RRs, maybe that's the
+                         * apex. But do all that only if this is
+                         * actually a response to our original
+                         * question.
+                         *
+                         * Similar for DS RRs, which are signed when
+                         * the parent SOA is signed. */
+
+                        r = dns_transaction_is_primary_response(t, rr);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        r = dnssec_has_rrsig(t->answer, rr->key);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                continue;
+
+                        r = dns_answer_has_dname_for_cname(t->answer, rr);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                continue;
+
+                        name = dns_resource_key_name(rr->key);
+                        r = dns_name_parent(&name);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        soa = dns_resource_key_new(rr->key->class, DNS_TYPE_SOA, name);
+                        if (!soa)
+                                return -ENOMEM;
+
+                        log_debug("Requesting parent SOA to validate transaction %" PRIu16 " (%s, unsigned CNAME/DNAME/DS RRset).",
+                                  t->id, dns_resource_key_name(rr->key));
+                        r = dns_transaction_request_dnssec_rr(t, soa);
+                        if (r < 0)
+                                return r;
+
+                        break;
+                }
+
+                default: {
+                        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *soa = NULL;
+
+                        /* For other unsigned RRsets (including
+                         * NSEC/NSEC3!), look for proof the zone is
+                         * unsigned, by requesting the SOA RR of the
+                         * zone. However, do so only if they are
+                         * directly relevant to our original
+                         * question. */
+
+                        r = dns_transaction_is_primary_response(t, rr);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        r = dnssec_has_rrsig(t->answer, rr->key);
+                        if (r < 0)
+                                return r;
+                        if (r > 0)
+                                continue;
+
+                        soa = dns_resource_key_new(rr->key->class, DNS_TYPE_SOA, dns_resource_key_name(rr->key));
+                        if (!soa)
+                                return -ENOMEM;
+
+                        log_debug("Requesting SOA to validate transaction %" PRIu16 " (%s, unsigned non-SOA/NS RRset <%s>).",
+                                  t->id, dns_resource_key_name(rr->key), dns_resource_record_to_string(rr));
+                        r = dns_transaction_request_dnssec_rr(t, soa);
+                        if (r < 0)
+                                return r;
+                        break;
+                }}
+        }
+
+        /* Above, we requested everything necessary to validate what
+         * we got. Now, let's request what we need to validate what we
+         * didn't get... */
+
+        r = dns_transaction_has_unsigned_negative_answer(t);
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                const char *name;
+                uint16_t type = 0;
+
+                name = dns_resource_key_name(dns_transaction_key(t));
+
+                /* If this was a SOA or NS request, then check if there's a DS RR for the same domain. Note that this
+                 * could also be used as indication that we are not at a zone apex, but in real world setups there are
+                 * too many broken DNS servers (Hello, incapdns.net!) where non-terminal zones return NXDOMAIN even
+                 * though they have further children. If this was a DS request, then it's signed when the parent zone
+                 * is signed, hence ask the parent SOA in that case. If this was any other RR then ask for the SOA RR,
+                 * to see if that is signed. */
+
+                if (dns_transaction_key(t)->type == DNS_TYPE_DS) {
+                        r = dns_name_parent(&name);
+                        if (r > 0) {
+                                type = DNS_TYPE_SOA;
+                                log_debug("Requesting parent SOA (→ %s) to validate transaction %" PRIu16 " (%s, unsigned empty DS response).",
+                                          name, t->id, dns_resource_key_name(dns_transaction_key(t)));
+                        } else
+                                name = NULL;
+
+                } else if (IN_SET(dns_transaction_key(t)->type, DNS_TYPE_SOA, DNS_TYPE_NS)) {
+
+                        type = DNS_TYPE_DS;
+                        log_debug("Requesting DS (→ %s) 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(dns_transaction_key(t)->class, type, name);
+                        if (!soa)
+                                return -ENOMEM;
+
+                        r = dns_transaction_request_dnssec_rr(t, soa);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        return dns_transaction_dnssec_is_live(t);
+}
+
+void dns_transaction_notify(DnsTransaction *t, DnsTransaction *source) {
+        assert(t);
+        assert(source);
+
+        /* Invoked whenever any of our auxiliary DNSSEC transactions completed its work. If the state is still PENDING,
+           we are still in the loop that adds further DNSSEC transactions, hence don't check if we are ready yet. If
+           the state is VALIDATING however, we should check if we are complete now. */
+
+        if (t->state == DNS_TRANSACTION_VALIDATING)
+                dns_transaction_process_dnssec(t);
+}
+
+static int dns_transaction_validate_dnskey_by_ds(DnsTransaction *t) {
+        DnsAnswerItem *item;
+        int r;
+
+        assert(t);
+
+        /* Add all DNSKEY RRs from the answer that are validated by DS
+         * RRs from the list of validated keys to the list of
+         * validated keys. */
+
+        DNS_ANSWER_FOREACH_ITEM(item, t->answer) {
+
+                r = dnssec_verify_dnskey_by_ds_search(item->rr, t->validated_keys);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                /* If so, the DNSKEY is validated too. */
+                r = dns_answer_add_extend(&t->validated_keys, item->rr, item->ifindex, item->flags|DNS_ANSWER_AUTHENTICATED, item->rrsig);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_transaction_requires_rrsig(DnsTransaction *t, DnsResourceRecord *rr) {
+        int r;
+
+        assert(t);
+        assert(rr);
+
+        /* Checks if the RR we are looking for must be signed with an
+         * RRSIG. This is used for positive responses. */
+
+        if (t->scope->dnssec_mode == DNSSEC_NO)
+                return false;
+
+        if (dns_type_is_pseudo(rr->key->type))
+                return -EINVAL;
+
+        r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(rr->key));
+        if (r < 0)
+                return r;
+        if (r > 0)
+                return false;
+
+        switch (rr->key->type) {
+
+        case DNS_TYPE_RRSIG:
+                /* RRSIGs are the signatures themselves, they need no signing. */
+                return false;
+
+        case DNS_TYPE_SOA:
+        case DNS_TYPE_NS: {
+                DnsTransaction *dt;
+
+                /* For SOA or NS RRs we look for a matching DS transaction */
+
+                SET_FOREACH(dt, t->dnssec_transactions) {
+
+                        if (dns_transaction_key(dt)->class != rr->key->class)
+                                continue;
+                        if (dns_transaction_key(dt)->type != DNS_TYPE_DS)
+                                continue;
+
+                        r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), dns_resource_key_name(rr->key));
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        /* We found a DS transactions for the SOA/NS
+                         * RRs we are looking at. If it discovered signed DS
+                         * RRs, then we need to be signed, too. */
+
+                        if (!FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
+                                return false;
+
+                        return dns_answer_match_key(dt->answer, dns_transaction_key(dt), NULL);
+                }
+
+                /* We found nothing that proves this is safe to leave
+                 * this unauthenticated, hence ask inist on
+                 * authentication. */
+                return true;
+        }
+
+        case DNS_TYPE_DS:
+        case DNS_TYPE_CNAME:
+        case DNS_TYPE_DNAME: {
+                const char *parent = NULL;
+                DnsTransaction *dt;
+
+                /*
+                 * CNAME/DNAME RRs cannot be located at a zone apex, hence look directly for the parent SOA.
+                 *
+                 * DS RRs are signed if the parent is signed, hence also look at the parent SOA
+                 */
+
+                SET_FOREACH(dt, t->dnssec_transactions) {
+
+                        if (dns_transaction_key(dt)->class != rr->key->class)
+                                continue;
+                        if (dns_transaction_key(dt)->type != DNS_TYPE_SOA)
+                                continue;
+
+                        if (!parent) {
+                                parent = dns_resource_key_name(rr->key);
+                                r = dns_name_parent(&parent);
+                                if (r < 0)
+                                        return r;
+                                if (r == 0) {
+                                        if (rr->key->type == DNS_TYPE_DS)
+                                                return true;
+
+                                        /* A CNAME/DNAME without a parent? That's sooo weird. */
+                                        return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
+                                                               "Transaction %" PRIu16 " claims CNAME/DNAME at root. Refusing.", t->id);
+                                }
+                        }
+
+                        r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), parent);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        return FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED);
+                }
+
+                return true;
+        }
+
+        default: {
+                DnsTransaction *dt;
+
+                /* Any other kind of RR (including DNSKEY/NSEC/NSEC3). Let's see if our SOA lookup was authenticated */
+
+                SET_FOREACH(dt, t->dnssec_transactions) {
+
+                        if (dns_transaction_key(dt)->class != rr->key->class)
+                                continue;
+                        if (dns_transaction_key(dt)->type != DNS_TYPE_SOA)
+                                continue;
+
+                        r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), dns_resource_key_name(rr->key));
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                continue;
+
+                        /* We found the transaction that was supposed to find the SOA RR for us. It was
+                         * successful, but found no RR for us. This means we are not at a zone cut. In this
+                         * case, we require authentication if the SOA lookup was authenticated too. */
+                        return FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED);
+                }
+
+                return true;
+        }}
+}
+
+static int dns_transaction_in_private_tld(DnsTransaction *t, const DnsResourceKey *key) {
+        DnsTransaction *dt;
+        const char *tld;
+        int r;
+
+        /* If DNSSEC downgrade mode is on, checks whether the
+         * specified RR is one level below a TLD we have proven not to
+         * exist. In such a case we assume that this is a private
+         * domain, and permit it.
+         *
+         * This detects cases like the Fritz!Box router networks. Each
+         * Fritz!Box router serves a private "fritz.box" zone, in the
+         * non-existing TLD "box". Requests for the "fritz.box" domain
+         * are served by the router itself, while requests for the
+         * "box" domain will result in NXDOMAIN.
+         *
+         * Note that this logic is unable to detect cases where a
+         * router serves a private DNS zone directly under
+         * non-existing TLD. In such a case we cannot detect whether
+         * the TLD is supposed to exist or not, as all requests we
+         * make for it will be answered by the router's zone, and not
+         * by the root zone. */
+
+        assert(t);
+
+        if (t->scope->dnssec_mode != DNSSEC_ALLOW_DOWNGRADE)
+                return false; /* In strict DNSSEC mode what doesn't exist, doesn't exist */
+
+        tld = dns_resource_key_name(key);
+        r = dns_name_parent(&tld);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return false; /* Already the root domain */
+
+        if (!dns_name_is_single_label(tld))
+                return false;
+
+        SET_FOREACH(dt, t->dnssec_transactions) {
+
+                if (dns_transaction_key(dt)->class != key->class)
+                        continue;
+
+                r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), tld);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                /* We found an auxiliary lookup we did for the TLD. If
+                 * that returned with NXDOMAIN, we know the TLD didn't
+                 * exist, and hence this might be a private zone. */
+
+                return dt->answer_rcode == DNS_RCODE_NXDOMAIN;
+        }
+
+        return false;
+}
+
+static int dns_transaction_requires_nsec(DnsTransaction *t) {
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+        DnsTransaction *dt;
+        const char *name;
+        uint16_t type = 0;
+        int r;
+
+        assert(t);
+
+        /* Checks if we need to insist on NSEC/NSEC3 RRs for proving
+         * this negative reply */
+
+        if (t->scope->dnssec_mode == DNSSEC_NO)
+                return false;
+
+        if (dns_type_is_pseudo(dns_transaction_key(t)->type))
+                return -EINVAL;
+
+        r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(dns_transaction_key(t)));
+        if (r < 0)
+                return r;
+        if (r > 0)
+                return false;
+
+        r = dns_transaction_in_private_tld(t, dns_transaction_key(t));
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                /* The lookup is from a TLD that is proven not to
+                 * exist, and we are in downgrade mode, hence ignore
+                 * that fact that we didn't get any NSEC RRs. */
+
+                log_info("Detected a negative query %s in a private DNS zone, permitting unsigned response.",
+                         dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str));
+                return false;
+        }
+
+        name = dns_resource_key_name(dns_transaction_key(t));
+
+        if (dns_transaction_key(t)->type == DNS_TYPE_DS) {
+
+                /* We got a negative reply for this DS lookup? DS RRs are signed when their parent zone is signed,
+                 * hence check the parent SOA in this case. */
+
+                r = dns_name_parent(&name);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return true;
+
+                type = DNS_TYPE_SOA;
+
+        } else if (IN_SET(dns_transaction_key(t)->type, DNS_TYPE_SOA, DNS_TYPE_NS))
+                /* We got a negative reply for this SOA/NS lookup? If so, check if there's a DS RR for this */
+                type = DNS_TYPE_DS;
+        else
+                /* For all other negative replies, check for the SOA lookup */
+                type = DNS_TYPE_SOA;
+
+        /* For all other RRs we check the SOA on the same level to see
+         * if it's signed. */
+
+        SET_FOREACH(dt, t->dnssec_transactions) {
+
+                if (dns_transaction_key(dt)->class != dns_transaction_key(t)->class)
+                        continue;
+                if (dns_transaction_key(dt)->type != type)
+                        continue;
+
+                r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), name);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                return FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED);
+        }
+
+        /* If in doubt, require NSEC/NSEC3 */
+        return true;
+}
+
+static int dns_transaction_dnskey_authenticated(DnsTransaction *t, DnsResourceRecord *rr) {
+        DnsResourceRecord *rrsig;
+        bool found = false;
+        int r;
+
+        /* Checks whether any of the DNSKEYs used for the RRSIGs for
+         * the specified RRset is authenticated (i.e. has a matching
+         * DS RR). */
+
+        r = dns_transaction_negative_trust_anchor_lookup(t, dns_resource_key_name(rr->key));
+        if (r < 0)
+                return r;
+        if (r > 0)
+                return false;
+
+        DNS_ANSWER_FOREACH(rrsig, t->answer) {
+                DnsTransaction *dt;
+
+                r = dnssec_key_match_rrsig(rr->key, rrsig);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                SET_FOREACH(dt, t->dnssec_transactions) {
+
+                        if (dns_transaction_key(dt)->class != rr->key->class)
+                                continue;
+
+                        if (dns_transaction_key(dt)->type == DNS_TYPE_DNSKEY) {
+
+                                r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), rrsig->rrsig.signer);
+                                if (r < 0)
+                                        return r;
+                                if (r == 0)
+                                        continue;
+
+                                /* OK, we found an auxiliary DNSKEY lookup. If that lookup is authenticated,
+                                 * report this. */
+
+                                if (FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
+                                        return true;
+
+                                found = true;
+
+                        } else if (dns_transaction_key(dt)->type == DNS_TYPE_DS) {
+
+                                r = dns_name_equal(dns_resource_key_name(dns_transaction_key(dt)), rrsig->rrsig.signer);
+                                if (r < 0)
+                                        return r;
+                                if (r == 0)
+                                        continue;
+
+                                /* OK, we found an auxiliary DS lookup. If that lookup is authenticated and
+                                 * non-zero, we won! */
+
+                                if (!FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
+                                        return false;
+
+                                return dns_answer_match_key(dt->answer, dns_transaction_key(dt), NULL);
+                        }
+                }
+        }
+
+        return found ? false : -ENXIO;
+}
+
+static int dns_transaction_known_signed(DnsTransaction *t, DnsResourceRecord *rr) {
+        assert(t);
+        assert(rr);
+
+        /* We know that the root domain is signed, hence if it appears
+         * not to be signed, there's a problem with the DNS server */
+
+        return rr->key->class == DNS_CLASS_IN &&
+                dns_name_is_root(dns_resource_key_name(rr->key));
+}
+
+static int dns_transaction_check_revoked_trust_anchors(DnsTransaction *t) {
+        DnsResourceRecord *rr;
+        int r;
+
+        assert(t);
+
+        /* Maybe warn the user that we encountered a revoked DNSKEY
+         * for a key from our trust anchor. Note that we don't care
+         * whether the DNSKEY can be authenticated or not. It's
+         * sufficient if it is self-signed. */
+
+        DNS_ANSWER_FOREACH(rr, t->answer) {
+                r = dns_trust_anchor_check_revoked(&t->scope->manager->trust_anchor, rr, t->answer);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_transaction_invalidate_revoked_keys(DnsTransaction *t) {
+        bool changed;
+        int r;
+
+        assert(t);
+
+        /* Removes all DNSKEY/DS objects from t->validated_keys that
+         * our trust anchors database considers revoked. */
+
+        do {
+                DnsResourceRecord *rr;
+
+                changed = false;
+
+                DNS_ANSWER_FOREACH(rr, t->validated_keys) {
+                        r = dns_trust_anchor_is_revoked(&t->scope->manager->trust_anchor, rr);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                r = dns_answer_remove_by_rr(&t->validated_keys, rr);
+                                if (r < 0)
+                                        return r;
+
+                                assert(r > 0);
+                                changed = true;
+                                break;
+                        }
+                }
+        } while (changed);
+
+        return 0;
+}
+
+static int dns_transaction_copy_validated(DnsTransaction *t) {
+        DnsTransaction *dt;
+        int r;
+
+        assert(t);
+
+        /* Copy all validated RRs from the auxiliary DNSSEC transactions into our set of validated RRs */
+
+        SET_FOREACH(dt, t->dnssec_transactions) {
+
+                if (DNS_TRANSACTION_IS_LIVE(dt->state))
+                        continue;
+
+                if (!FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
+                        continue;
+
+                r = dns_answer_extend(&t->validated_keys, dt->answer);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+typedef enum {
+        DNSSEC_PHASE_DNSKEY,   /* Phase #1, only validate DNSKEYs */
+        DNSSEC_PHASE_NSEC,     /* Phase #2, only validate NSEC+NSEC3 */
+        DNSSEC_PHASE_ALL,      /* Phase #3, validate everything else */
+} Phase;
+
+static int dnssec_validate_records(
+                DnsTransaction *t,
+                Phase phase,
+                bool *have_nsec,
+                DnsAnswer **validated) {
+
+        DnsResourceRecord *rr;
+        int r;
+
+        /* Returns negative on error, 0 if validation failed, 1 to restart validation, 2 when finished. */
+
+        DNS_ANSWER_FOREACH(rr, t->answer) {
+                DnsResourceRecord *rrsig = NULL;
+                DnssecResult result;
+
+                switch (rr->key->type) {
+                case DNS_TYPE_RRSIG:
+                        continue;
+
+                case DNS_TYPE_DNSKEY:
+                        /* We validate DNSKEYs only in the DNSKEY and ALL phases */
+                        if (phase == DNSSEC_PHASE_NSEC)
+                                continue;
+                        break;
+
+                case DNS_TYPE_NSEC:
+                case DNS_TYPE_NSEC3:
+                        *have_nsec = true;
+
+                        /* We validate NSEC/NSEC3 only in the NSEC and ALL phases */
+                        if (phase == DNSSEC_PHASE_DNSKEY)
+                                continue;
+                        break;
+
+                default:
+                        /* We validate all other RRs only in the ALL phases */
+                        if (phase != DNSSEC_PHASE_ALL)
+                                continue;
+                }
+
+                r = dnssec_verify_rrset_search(
+                                t->answer,
+                                rr->key,
+                                t->validated_keys,
+                                USEC_INFINITY,
+                                &result,
+                                &rrsig);
+                if (r < 0)
+                        return r;
+
+                log_debug("Looking at %s: %s", strna(dns_resource_record_to_string(rr)), dnssec_result_to_string(result));
+
+                if (result == DNSSEC_VALIDATED) {
+                        assert(rrsig);
+
+                        if (rr->key->type == DNS_TYPE_DNSKEY) {
+                                /* If we just validated a DNSKEY RRset, then let's add these keys to
+                                 * the set of validated keys for this transaction. */
+
+                                r = dns_answer_copy_by_key(&t->validated_keys, t->answer, rr->key, DNS_ANSWER_AUTHENTICATED, rrsig);
+                                if (r < 0)
+                                        return r;
+
+                                /* Some of the DNSKEYs we just added might already have been revoked,
+                                 * remove them again in that case. */
+                                r = dns_transaction_invalidate_revoked_keys(t);
+                                if (r < 0)
+                                        return r;
+                        }
+
+                        /* Add the validated RRset to the new list of validated RRsets, and remove it from
+                         * the unvalidated RRsets.  We mark the RRset as authenticated and cacheable. */
+                        r = dns_answer_move_by_key(validated, &t->answer, rr->key, DNS_ANSWER_AUTHENTICATED|DNS_ANSWER_CACHEABLE, rrsig);
+                        if (r < 0)
+                                return r;
+
+                        manager_dnssec_verdict(t->scope->manager, DNSSEC_SECURE, rr->key);
+
+                        /* Exit the loop, we dropped something from the answer, start from the beginning */
+                        return 1;
+                }
+
+                /* If we haven't read all DNSKEYs yet a negative result of the validation is irrelevant, as
+                 * there might be more DNSKEYs coming. Similar, if we haven't read all NSEC/NSEC3 RRs yet,
+                 * we cannot do positive wildcard proofs yet, as those require the NSEC/NSEC3 RRs. */
+                if (phase != DNSSEC_PHASE_ALL)
+                        continue;
+
+                if (result == DNSSEC_VALIDATED_WILDCARD) {
+                        bool authenticated = false;
+                        const char *source;
+
+                        assert(rrsig);
+
+                        /* This RRset validated, but as a wildcard. This means we need
+                         * to prove via NSEC/NSEC3 that no matching non-wildcard RR exists. */
+
+                        /* First step, determine the source of synthesis */
+                        r = dns_resource_record_source(rrsig, &source);
+                        if (r < 0)
+                                return r;
+
+                        r = dnssec_test_positive_wildcard(*validated,
+                                                          dns_resource_key_name(rr->key),
+                                                          source,
+                                                          rrsig->rrsig.signer,
+                                                          &authenticated);
+
+                        /* Unless the NSEC proof showed that the key really doesn't exist something is off. */
+                        if (r == 0)
+                                result = DNSSEC_INVALID;
+                        else {
+                                r = dns_answer_move_by_key(
+                                                validated,
+                                                &t->answer,
+                                                rr->key,
+                                                authenticated ? (DNS_ANSWER_AUTHENTICATED|DNS_ANSWER_CACHEABLE) : 0,
+                                                rrsig);
+                                if (r < 0)
+                                        return r;
+
+                                manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, rr->key);
+
+                                /* Exit the loop, we dropped something from the answer, start from the beginning */
+                                return 1;
+                        }
+                }
+
+                if (result == DNSSEC_NO_SIGNATURE) {
+                        r = dns_transaction_requires_rrsig(t, rr);
+                        if (r < 0)
+                                return r;
+                        if (r == 0) {
+                                /* Data does not require signing. In that case, just copy it over,
+                                 * but remember that this is by no means authenticated. */
+                                r = dns_answer_move_by_key(
+                                                validated,
+                                                &t->answer,
+                                                rr->key,
+                                                0,
+                                                NULL);
+                                if (r < 0)
+                                        return r;
+
+                                manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key);
+                                return 1;
+                        }
+
+                        r = dns_transaction_known_signed(t, rr);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                /* This is an RR we know has to be signed. If it isn't this means
+                                 * the server is not attaching RRSIGs, hence complain. */
+
+                                dns_server_packet_rrsig_missing(t->server, t->current_feature_level);
+
+                                if (t->scope->dnssec_mode == DNSSEC_ALLOW_DOWNGRADE) {
+
+                                        /* Downgrading is OK? If so, just consider the information unsigned */
+
+                                        r = dns_answer_move_by_key(validated, &t->answer, rr->key, 0, NULL);
+                                        if (r < 0)
+                                                return r;
+
+                                        manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key);
+                                        return 1;
+                                }
+
+                                /* Otherwise, fail */
+                                t->answer_dnssec_result = DNSSEC_INCOMPATIBLE_SERVER;
+                                return 0;
+                        }
+
+                        r = dns_transaction_in_private_tld(t, rr->key);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                char s[DNS_RESOURCE_KEY_STRING_MAX];
+
+                                /* The data is from a TLD that is proven not to exist, and we are in downgrade
+                                 * mode, hence ignore the fact that this was not signed. */
+
+                                log_info("Detected RRset %s is in a private DNS zone, permitting unsigned RRs.",
+                                         dns_resource_key_to_string(rr->key, s, sizeof s));
+
+                                r = dns_answer_move_by_key(validated, &t->answer, rr->key, 0, NULL);
+                                if (r < 0)
+                                        return r;
+
+                                manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key);
+                                return 1;
+                        }
+                }
+
+                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, NULL);
+                                if (r < 0)
+                                        return r;
+
+                                manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key);
+                                return 1;
+                        }
+                }
+
+                r = dns_transaction_is_primary_response(t, rr);
+                if (r < 0)
+                        return r;
+                if (r > 0) {
+                        /* Look for a matching DNAME for this CNAME */
+                        r = dns_answer_has_dname_for_cname(t->answer, rr);
+                        if (r < 0)
+                                return r;
+                        if (r == 0) {
+                                /* Also look among the stuff we already validated */
+                                r = dns_answer_has_dname_for_cname(*validated, rr);
+                                if (r < 0)
+                                        return r;
+                        }
+
+                        if (r == 0) {
+                                if (IN_SET(result,
+                                           DNSSEC_INVALID,
+                                           DNSSEC_SIGNATURE_EXPIRED,
+                                           DNSSEC_NO_SIGNATURE))
+                                        manager_dnssec_verdict(t->scope->manager, DNSSEC_BOGUS, rr->key);
+                                else /* DNSSEC_MISSING_KEY or DNSSEC_UNSUPPORTED_ALGORITHM */
+                                        manager_dnssec_verdict(t->scope->manager, DNSSEC_INDETERMINATE, rr->key);
+
+                                /* This is a primary response to our question, and it failed validation.
+                                 * That's fatal. */
+                                t->answer_dnssec_result = result;
+                                return 0;
+                        }
+
+                        /* This is a primary response, but we do have a DNAME RR
+                         * in the RR that can replay this CNAME, hence rely on
+                         * that, and we can remove the CNAME in favour of it. */
+                }
+
+                /* This is just some auxiliary data. Just remove the RRset and continue. */
+                r = dns_answer_remove_by_key(&t->answer, rr->key);
+                if (r < 0)
+                        return r;
+
+                /* We dropped something from the answer, start from the beginning. */
+                return 1;
+        }
+
+        return 2; /* Finito. */
+}
+
+int dns_transaction_validate_dnssec(DnsTransaction *t) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *validated = NULL;
+        Phase phase;
+        DnsAnswerFlags flags;
+        int r;
+        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+
+        assert(t);
+
+        /* We have now collected all DS and DNSKEY RRs in t->validated_keys, let's see which RRs we can now
+         * authenticate with that. */
+
+        if (FLAGS_SET(t->query_flags, SD_RESOLVED_NO_VALIDATE) || t->scope->dnssec_mode == DNSSEC_NO)
+                return 0;
+
+        /* Already validated */
+        if (t->answer_dnssec_result != _DNSSEC_RESULT_INVALID)
+                return 0;
+
+        /* Our own stuff needs no validation */
+        if (IN_SET(t->answer_source, DNS_TRANSACTION_ZONE, DNS_TRANSACTION_TRUST_ANCHOR)) {
+                t->answer_dnssec_result = DNSSEC_VALIDATED;
+                SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, true);
+                return 0;
+        }
+
+        /* Cached stuff is not affected by validation. */
+        if (t->answer_source != DNS_TRANSACTION_NETWORK)
+                return 0;
+
+        if (!dns_transaction_dnssec_supported_full(t)) {
+                /* The server does not support DNSSEC, or doesn't augment responses with RRSIGs. */
+                t->answer_dnssec_result = DNSSEC_INCOMPATIBLE_SERVER;
+                log_debug("Not validating response for %" PRIu16 ", used server feature level does not support DNSSEC.", t->id);
+                return 0;
+        }
+
+        log_debug("Validating response from transaction %" PRIu16 " (%s).",
+                  t->id,
+                  dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str));
+
+        /* First, see if this response contains any revoked trust
+         * anchors we care about */
+        r = dns_transaction_check_revoked_trust_anchors(t);
+        if (r < 0)
+                return r;
+
+        /* Third, copy all RRs we acquired successfully from auxiliary RRs over. */
+        r = dns_transaction_copy_validated(t);
+        if (r < 0)
+                return r;
+
+        /* Second, see if there are DNSKEYs we already know a
+         * validated DS for. */
+        r = dns_transaction_validate_dnskey_by_ds(t);
+        if (r < 0)
+                return r;
+
+        /* Fourth, remove all DNSKEY and DS RRs again that our trust
+         * anchor says are revoked. After all we might have marked
+         * some keys revoked above, but they might still be lingering
+         * in our validated_keys list. */
+        r = dns_transaction_invalidate_revoked_keys(t);
+        if (r < 0)
+                return r;
+
+        phase = DNSSEC_PHASE_DNSKEY;
+        for (;;) {
+                bool have_nsec = false;
+
+                r = dnssec_validate_records(t, phase, &have_nsec, &validated);
+                if (r <= 0)
+                        return r;
+
+                /* Try again as long as we managed to achieve something */
+                if (r == 1)
+                        continue;
+
+                if (phase == DNSSEC_PHASE_DNSKEY && have_nsec) {
+                        /* OK, we processed all DNSKEYs, and there are NSEC/NSEC3 RRs, look at those now. */
+                        phase = DNSSEC_PHASE_NSEC;
+                        continue;
+                }
+
+                if (phase != DNSSEC_PHASE_ALL) {
+                        /* OK, we processed all DNSKEYs and NSEC/NSEC3 RRs, look at all the rest now.
+                         * Note that in this third phase we start to remove RRs we couldn't validate. */
+                        phase = DNSSEC_PHASE_ALL;
+                        continue;
+                }
+
+                /* We're done */
+                break;
+        }
+
+        dns_answer_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;
+                        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, true);
+                } else {
+                        /* The answer is not fully authenticated. */
+                        t->answer_dnssec_result = DNSSEC_UNSIGNED;
+                        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false);
+                }
+
+        } else if (r == 0) {
+                DnssecNsecResult nr;
+                bool authenticated = false;
+
+                /* Bummer! Let's check NSEC/NSEC3 */
+                r = dnssec_nsec_test(t->answer, dns_transaction_key(t), &nr, &authenticated, &t->answer_nsec_ttl);
+                if (r < 0)
+                        return r;
+
+                switch (nr) {
+
+                case DNSSEC_NSEC_NXDOMAIN:
+                        /* NSEC proves the domain doesn't exist. Very good. */
+                        log_debug("Proved NXDOMAIN via NSEC/NSEC3 for transaction %u (%s)", t->id, key_str);
+                        t->answer_dnssec_result = DNSSEC_VALIDATED;
+                        t->answer_rcode = DNS_RCODE_NXDOMAIN;
+                        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, authenticated);
+
+                        manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, dns_transaction_key(t));
+                        break;
+
+                case DNSSEC_NSEC_NODATA:
+                        /* NSEC proves that there's no data here, very good. */
+                        log_debug("Proved NODATA via NSEC/NSEC3 for transaction %u (%s)", t->id, key_str);
+                        t->answer_dnssec_result = DNSSEC_VALIDATED;
+                        t->answer_rcode = DNS_RCODE_SUCCESS;
+                        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, authenticated);
+
+                        manager_dnssec_verdict(t->scope->manager, authenticated ? DNSSEC_SECURE : DNSSEC_INSECURE, dns_transaction_key(t));
+                        break;
+
+                case DNSSEC_NSEC_OPTOUT:
+                        /* NSEC3 says the data might not be signed */
+                        log_debug("Data is NSEC3 opt-out via NSEC/NSEC3 for transaction %u (%s)", t->id, key_str);
+                        t->answer_dnssec_result = DNSSEC_UNSIGNED;
+                        SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false);
+
+                        manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, dns_transaction_key(t));
+                        break;
+
+                case DNSSEC_NSEC_NO_RR:
+                        /* No NSEC data? Bummer! */
+
+                        r = dns_transaction_requires_nsec(t);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                t->answer_dnssec_result = DNSSEC_NO_SIGNATURE;
+                                manager_dnssec_verdict(t->scope->manager, DNSSEC_BOGUS, dns_transaction_key(t));
+                        } else {
+                                t->answer_dnssec_result = DNSSEC_UNSIGNED;
+                                SET_FLAG(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED, false);
+                                manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, dns_transaction_key(t));
+                        }
+
+                        break;
+
+                case DNSSEC_NSEC_UNSUPPORTED_ALGORITHM:
+                        /* We don't know the NSEC3 algorithm used? */
+                        t->answer_dnssec_result = DNSSEC_UNSUPPORTED_ALGORITHM;
+                        manager_dnssec_verdict(t->scope->manager, DNSSEC_INDETERMINATE, dns_transaction_key(t));
+                        break;
+
+                case DNSSEC_NSEC_FOUND:
+                case DNSSEC_NSEC_CNAME:
+                        /* NSEC says it needs to be there, but we couldn't find it? Bummer! */
+                        t->answer_dnssec_result = DNSSEC_NSEC_MISMATCH;
+                        manager_dnssec_verdict(t->scope->manager, DNSSEC_BOGUS, dns_transaction_key(t));
+                        break;
+
+                default:
+                        assert_not_reached("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",
+        [DNS_TRANSACTION_NO_SOURCE]            = "no-source",
+        [DNS_TRANSACTION_STUB_LOOP]            = "stub-loop",
+};
+DEFINE_STRING_TABLE_LOOKUP(dns_transaction_state, DnsTransactionState);
+
+static const char* const dns_transaction_source_table[_DNS_TRANSACTION_SOURCE_MAX] = {
+        [DNS_TRANSACTION_NETWORK]      = "network",
+        [DNS_TRANSACTION_CACHE]        = "cache",
+        [DNS_TRANSACTION_ZONE]         = "zone",
+        [DNS_TRANSACTION_TRUST_ANCHOR] = "trust-anchor",
+};
+DEFINE_STRING_TABLE_LOOKUP(dns_transaction_source, DnsTransactionSource);
diff --git a/src/resolve/resolved-dns-transaction.h b/src/resolve/resolved-dns-transaction.h
new file mode 100644
index 0000000..498cabb
--- /dev/null
+++ b/src/resolve/resolved-dns-transaction.h
@@ -0,0 +1,224 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-event.h"
+#include "in-addr-util.h"
+
+typedef struct DnsTransaction DnsTransaction;
+typedef struct DnsTransactionFinder DnsTransactionFinder;
+typedef enum DnsTransactionState DnsTransactionState;
+typedef enum DnsTransactionSource DnsTransactionSource;
+
+#include "resolved-dns-answer.h"
+#include "resolved-dns-dnssec.h"
+#include "resolved-dns-packet.h"
+#include "resolved-dns-question.h"
+#include "resolved-dns-server.h"
+
+enum DnsTransactionState {
+        DNS_TRANSACTION_NULL,
+        DNS_TRANSACTION_PENDING,
+        DNS_TRANSACTION_VALIDATING,
+        DNS_TRANSACTION_RCODE_FAILURE,
+        DNS_TRANSACTION_SUCCESS,
+        DNS_TRANSACTION_NO_SERVERS,
+        DNS_TRANSACTION_TIMEOUT,
+        DNS_TRANSACTION_ATTEMPTS_MAX_REACHED,
+        DNS_TRANSACTION_INVALID_REPLY,
+        DNS_TRANSACTION_ERRNO,
+        DNS_TRANSACTION_ABORTED,
+        DNS_TRANSACTION_DNSSEC_FAILED,
+        DNS_TRANSACTION_NO_TRUST_ANCHOR,
+        DNS_TRANSACTION_RR_TYPE_UNSUPPORTED,
+        DNS_TRANSACTION_NETWORK_DOWN,
+        DNS_TRANSACTION_NOT_FOUND, /* like NXDOMAIN, but when LLMNR/TCP connections fail */
+        DNS_TRANSACTION_NO_SOURCE, /* All suitable DnsTransactionSource turned off */
+        DNS_TRANSACTION_STUB_LOOP,
+        _DNS_TRANSACTION_STATE_MAX,
+        _DNS_TRANSACTION_STATE_INVALID = -EINVAL,
+};
+
+#define DNS_TRANSACTION_IS_LIVE(state) IN_SET((state), DNS_TRANSACTION_NULL, DNS_TRANSACTION_PENDING, DNS_TRANSACTION_VALIDATING)
+
+enum DnsTransactionSource {
+        DNS_TRANSACTION_NETWORK,
+        DNS_TRANSACTION_CACHE,
+        DNS_TRANSACTION_ZONE,
+        DNS_TRANSACTION_TRUST_ANCHOR,
+        _DNS_TRANSACTION_SOURCE_MAX,
+        _DNS_TRANSACTION_SOURCE_INVALID = -EINVAL,
+};
+
+struct DnsTransaction {
+        DnsScope *scope;
+
+        DnsResourceKey *key;         /* For regular lookups the RR key to look for */
+        DnsPacket *bypass;           /* For bypass lookups the full original request packet */
+
+        uint64_t query_flags;
+
+        DnsPacket *sent, *received;
+
+        DnsAnswer *answer;
+        int answer_rcode;
+        DnssecResult answer_dnssec_result;
+        DnsTransactionSource answer_source;
+        uint32_t answer_nsec_ttl;
+        int answer_errno; /* if state is DNS_TRANSACTION_ERRNO */
+
+        DnsTransactionState state;
+
+        /* SD_RESOLVED_AUTHENTICATED here indicates whether the primary answer is authenticated, i.e. whether
+         * the RRs from answer which directly match the question are authenticated, or, if there are none,
+         * whether the NODATA or NXDOMAIN case is. It says nothing about additional RRs listed in the answer,
+         * however they have their own DNS_ANSWER_AUTHORIZED FLAGS. Note that this bit is defined different
+         * than the AD bit in DNS packets, as that covers more than just the actual primary answer. */
+        uint64_t answer_query_flags;
+
+        /* Contains DNSKEY, DS, SOA RRs we already verified and need
+         * to authenticate this reply */
+        DnsAnswer *validated_keys;
+
+        usec_t start_usec;
+        usec_t next_attempt_after;
+        sd_event_source *timeout_event_source;
+        unsigned n_attempts;
+
+        /* UDP connection logic, if we need it */
+        int dns_udp_fd;
+        sd_event_source *dns_udp_event_source;
+
+        /* TCP connection logic, if we need it */
+        DnsStream *stream;
+
+        /* The active server */
+        DnsServer *server;
+
+        /* The features of the DNS server at time of transaction start */
+        DnsServerFeatureLevel current_feature_level;
+
+        /* If we got SERVFAIL back, we retry the lookup, using a lower feature level than we used
+         * before. Similar, if we get NXDOMAIN in pure EDNS0 mode, we check in EDNS0-less mode before giving
+         * up (as mitigation for DVE-2018-0001). */
+        DnsServerFeatureLevel clamp_feature_level_servfail;
+        DnsServerFeatureLevel clamp_feature_level_nxdomain;
+
+        uint16_t id;
+
+        bool tried_stream:1;
+
+        bool initial_jitter_scheduled:1;
+        bool initial_jitter_elapsed:1;
+
+        bool probing:1;
+
+        /* Query candidates this transaction is referenced by and that
+         * shall be notified about this specific transaction
+         * completing. */
+        Set *notify_query_candidates, *notify_query_candidates_done;
+
+        /* Zone items this transaction is referenced by and that shall
+         * be notified about completion. */
+        Set *notify_zone_items, *notify_zone_items_done;
+
+        /* Other transactions that this transactions is referenced by
+         * and that shall be notified about completion. This is used
+         * when transactions want to validate their RRsets, but need
+         * another DNSKEY or DS RR to do so. */
+        Set *notify_transactions, *notify_transactions_done;
+
+        /* The opposite direction: the transactions this transaction
+         * created in order to request DNSKEY or DS RRs. */
+        Set *dnssec_transactions;
+
+        unsigned n_picked_servers;
+
+        unsigned block_gc;
+
+        LIST_FIELDS(DnsTransaction, transactions_by_scope);
+        LIST_FIELDS(DnsTransaction, transactions_by_stream);
+        LIST_FIELDS(DnsTransaction, transactions_by_key);
+
+        /* Note: fields should be ordered to minimize alignment gaps. Use pahole! */
+};
+
+int dns_transaction_new(DnsTransaction **ret, DnsScope *s, DnsResourceKey *key, DnsPacket *bypass, uint64_t flags);
+DnsTransaction* dns_transaction_free(DnsTransaction *t);
+
+DnsTransaction* dns_transaction_gc(DnsTransaction *t);
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsTransaction*, dns_transaction_gc);
+
+int dns_transaction_go(DnsTransaction *t);
+
+void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypted);
+void dns_transaction_complete(DnsTransaction *t, DnsTransactionState state);
+
+void dns_transaction_notify(DnsTransaction *t, DnsTransaction *source);
+int dns_transaction_validate_dnssec(DnsTransaction *t);
+int dns_transaction_request_dnssec_keys(DnsTransaction *t);
+
+static inline DnsResourceKey *dns_transaction_key(DnsTransaction *t) {
+        assert(t);
+
+        /* Return the lookup key of this transaction. Either takes the lookup key from the bypass packet if
+         * we are a bypass transaction. Or take the configured key for regular transactions. */
+
+        if (t->key)
+                return t->key;
+
+        assert(t->bypass);
+
+        return dns_question_first_key(t->bypass->question);
+}
+
+static inline uint64_t dns_transaction_source_to_query_flags(DnsTransactionSource s) {
+
+        switch (s) {
+
+        case DNS_TRANSACTION_NETWORK:
+                return SD_RESOLVED_FROM_NETWORK;
+
+        case DNS_TRANSACTION_CACHE:
+                return SD_RESOLVED_FROM_CACHE;
+
+        case DNS_TRANSACTION_ZONE:
+                return SD_RESOLVED_FROM_ZONE;
+
+        case DNS_TRANSACTION_TRUST_ANCHOR:
+                return SD_RESOLVED_FROM_TRUST_ANCHOR;
+
+        default:
+                return 0;
+        }
+}
+
+const char* dns_transaction_state_to_string(DnsTransactionState p) _const_;
+DnsTransactionState dns_transaction_state_from_string(const char *s) _pure_;
+
+const char* dns_transaction_source_to_string(DnsTransactionSource p) _const_;
+DnsTransactionSource dns_transaction_source_from_string(const char *s) _pure_;
+
+/* LLMNR Jitter interval, see RFC 4795 Section 7 */
+#define LLMNR_JITTER_INTERVAL_USEC (100 * USEC_PER_MSEC)
+
+/* mDNS 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..b036aa4
--- /dev/null
+++ b/src/resolve/resolved-dns-trust-anchor.c
@@ -0,0 +1,773 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-messages.h"
+
+#include "alloc-util.h"
+#include "conf-files.h"
+#include "def.h"
+#include "dns-domain.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "hexdecoct.h"
+#include "nulstr-util.h"
+#include "parse-util.h"
+#include "resolved-dns-dnssec.h"
+#include "resolved-dns-trust-anchor.h"
+#include "set.h"
+#include "sort-util.h"
+#include "string-util.h"
+#include "strv.h"
+
+static const char trust_anchor_dirs[] = CONF_PATHS_NULSTR("dnssec-trust-anchors.d");
+
+/* The second DS RR from https://data.iana.org/root-anchors/root-anchors.xml, retrieved February 2017 */
+static const uint8_t root_digest2[] =
+        { 0xE0, 0x6D, 0x44, 0xB8, 0x0B, 0x8F, 0x1D, 0x39, 0xA9, 0x5C, 0x0B, 0x0D, 0x7C, 0x65, 0xD0, 0x84,
+          0x58, 0xE8, 0x80, 0x40, 0x9B, 0xBC, 0x68, 0x34, 0x57, 0x10, 0x42, 0x37, 0xC7, 0xF8, 0xEC, 0x8D };
+
+static bool dns_trust_anchor_knows_domain_positive(DnsTrustAnchor *d, const char *name) {
+        assert(d);
+
+        /* Returns true if there's an entry for the specified domain
+         * name in our trust anchor */
+
+        return
+                hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DNSKEY, name)) ||
+                hashmap_contains(d->positive_by_key, &DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_DS, name));
+}
+
+static int add_root_ksk(
+                DnsAnswer *answer,
+                DnsResourceKey *key,
+                uint16_t key_tag,
+                uint8_t algorithm,
+                uint8_t digest_type,
+                const void *digest,
+                size_t digest_size) {
+
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+        int r;
+
+        rr = dns_resource_record_new(key);
+        if (!rr)
+                return -ENOMEM;
+
+        rr->ds.key_tag = key_tag;
+        rr->ds.algorithm = algorithm;
+        rr->ds.digest_type = digest_type;
+        rr->ds.digest_size = digest_size;
+        rr->ds.digest = memdup(digest, rr->ds.digest_size);
+        if (!rr->ds.digest)
+                return  -ENOMEM;
+
+        r = dns_answer_add(answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int dns_trust_anchor_add_builtin_positive(DnsTrustAnchor *d) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+        int r;
+
+        assert(d);
+
+        r = hashmap_ensure_allocated(&d->positive_by_key, &dns_resource_key_hash_ops);
+        if (r < 0)
+                return r;
+
+        /* Only add the built-in trust anchor if there's neither a DS nor a DNSKEY defined for the root domain. That
+         * way users have an easy way to override the root domain DS/DNSKEY data. */
+        if (dns_trust_anchor_knows_domain_positive(d, "."))
+                return 0;
+
+        key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_DS, "");
+        if (!key)
+                return -ENOMEM;
+
+        answer = dns_answer_new(2);
+        if (!answer)
+                return -ENOMEM;
+
+        /* Add the currently valid RRs from https://data.iana.org/root-anchors/root-anchors.xml */
+        r = add_root_ksk(answer, key, 20326, DNSSEC_ALGORITHM_RSASHA256, DNSSEC_DIGEST_SHA256, root_digest2, sizeof(root_digest2));
+        if (r < 0)
+                return r;
+
+        r = hashmap_put(d->positive_by_key, key, answer);
+        if (r < 0)
+                return r;
+
+        answer = NULL;
+        return 0;
+}
+
+static int dns_trust_anchor_add_builtin_negative(DnsTrustAnchor *d) {
+
+        static const char private_domains[] =
+                /* RFC 6761 says that .test is a special domain for
+                 * testing and not to be installed in the root zone */
+                "test\0"
+
+                /* RFC 6761 says that these reverse IP lookup ranges
+                 * are for private addresses, and hence should not
+                 * show up in the root zone */
+                "10.in-addr.arpa\0"
+                "16.172.in-addr.arpa\0"
+                "17.172.in-addr.arpa\0"
+                "18.172.in-addr.arpa\0"
+                "19.172.in-addr.arpa\0"
+                "20.172.in-addr.arpa\0"
+                "21.172.in-addr.arpa\0"
+                "22.172.in-addr.arpa\0"
+                "23.172.in-addr.arpa\0"
+                "24.172.in-addr.arpa\0"
+                "25.172.in-addr.arpa\0"
+                "26.172.in-addr.arpa\0"
+                "27.172.in-addr.arpa\0"
+                "28.172.in-addr.arpa\0"
+                "29.172.in-addr.arpa\0"
+                "30.172.in-addr.arpa\0"
+                "31.172.in-addr.arpa\0"
+                "168.192.in-addr.arpa\0"
+
+                /* The same, but for IPv6. */
+                "d.f.ip6.arpa\0"
+
+                /* RFC 6762 reserves the .local domain for Multicast
+                 * DNS, it hence cannot appear in the root zone. (Note
+                 * that we by default do not route .local traffic to
+                 * DNS anyway, except when a configured search domain
+                 * suggests so.) */
+                "local\0"
+
+                /* These two are well known, popular private zone
+                 * TLDs, that are blocked from delegation, according
+                 * to:
+                 * http://icannwiki.com/Name_Collision#NGPC_Resolution
+                 *
+                 * There's also ongoing work on making this official
+                 * in an RRC:
+                 * https://www.ietf.org/archive/id/draft-chapin-additional-reserved-tlds-02.txt */
+                "home\0"
+                "corp\0"
+
+                /* The following four TLDs are suggested for private
+                 * zones in RFC 6762, Appendix G, and are hence very
+                 * unlikely to be made official TLDs any day soon */
+                "lan\0"
+                "intranet\0"
+                "internal\0"
+                "private\0"
+
+                /* Defined by RFC 8375. The most official choice. */
+                "home.arpa\0";
+
+        const char *name;
+        int r;
+
+        assert(d);
+
+        /* Only add the built-in trust anchor if there's no negative
+         * trust anchor defined at all. This enables easy overriding
+         * of negative trust anchors. */
+
+        if (set_size(d->negative_by_name) > 0)
+                return 0;
+
+        r = set_ensure_allocated(&d->negative_by_name, &dns_name_hash_ops);
+        if (r < 0)
+                return r;
+
+        /* We add a couple of domains as default negative trust
+         * anchors, where it's very unlikely they will be installed in
+         * the root zone. If they exist they must be private, and thus
+         * unsigned. */
+
+        NULSTR_FOREACH(name, private_domains) {
+                if (dns_trust_anchor_knows_domain_positive(d, name))
+                        continue;
+
+                r = set_put_strdup(&d->negative_by_name, name);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_trust_anchor_load_positive(DnsTrustAnchor *d, const char *path, unsigned line, const char *s) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+        _cleanup_free_ char *domain = NULL, *class = NULL, *type = NULL;
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        DnsAnswer *old_answer = NULL;
+        const char *p = s;
+        int r;
+
+        assert(d);
+        assert(line);
+
+        r = extract_first_word(&p, &domain, NULL, EXTRACT_UNQUOTE);
+        if (r < 0)
+                return log_warning_errno(r, "Unable to parse domain in line %s:%u: %m", path, line);
+
+        r = dns_name_is_valid(domain);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to check validity of domain name '%s', at line %s:%u, ignoring line: %m", domain, path, line);
+        if (r == 0) {
+                log_warning("Domain name %s is invalid, at line %s:%u, ignoring line.", domain, path, line);
+                return -EINVAL;
+        }
+
+        r = extract_many_words(&p, NULL, 0, &class, &type, NULL);
+        if (r < 0)
+                return log_warning_errno(r, "Unable to parse class and type in line %s:%u: %m", path, line);
+        if (r != 2) {
+                log_warning("Missing class or type in line %s:%u", path, line);
+                return -EINVAL;
+        }
+
+        if (!strcaseeq(class, "IN")) {
+                log_warning("RR class %s is not supported, ignoring line %s:%u.", class, path, line);
+                return -EINVAL;
+        }
+
+        if (strcaseeq(type, "DS")) {
+                _cleanup_free_ char *key_tag = NULL, *algorithm = NULL, *digest_type = NULL;
+                _cleanup_free_ void *dd = NULL;
+                uint16_t kt;
+                int a, dt;
+                size_t l;
+
+                r = extract_many_words(&p, NULL, 0, &key_tag, &algorithm, &digest_type, NULL);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to parse DS parameters on line %s:%u: %m", path, line);
+                        return -EINVAL;
+                }
+                if (r != 3) {
+                        log_warning("Missing DS parameters on line %s:%u", path, line);
+                        return -EINVAL;
+                }
+
+                r = safe_atou16(key_tag, &kt);
+                if (r < 0)
+                        return log_warning_errno(r, "Failed to parse DS key tag %s on line %s:%u: %m", key_tag, path, line);
+
+                a = dnssec_algorithm_from_string(algorithm);
+                if (a < 0) {
+                        log_warning("Failed to parse DS algorithm %s on line %s:%u", algorithm, path, line);
+                        return -EINVAL;
+                }
+
+                dt = dnssec_digest_from_string(digest_type);
+                if (dt < 0) {
+                        log_warning("Failed to parse DS digest type %s on line %s:%u", digest_type, path, line);
+                        return -EINVAL;
+                }
+
+                if (isempty(p)) {
+                        log_warning("Missing DS digest on line %s:%u", path, line);
+                        return -EINVAL;
+                }
+
+                r = unhexmem(p, strlen(p), &dd, &l);
+                if (r < 0) {
+                        log_warning("Failed to parse DS digest %s on line %s:%u", p, path, line);
+                        return -EINVAL;
+                }
+
+                rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DS, domain);
+                if (!rr)
+                        return log_oom();
+
+                rr->ds.key_tag = kt;
+                rr->ds.algorithm = a;
+                rr->ds.digest_type = dt;
+                rr->ds.digest_size = l;
+                rr->ds.digest = TAKE_PTR(dd);
+
+        } else if (strcaseeq(type, "DNSKEY")) {
+                _cleanup_free_ char *flags = NULL, *protocol = NULL, *algorithm = NULL;
+                _cleanup_free_ void *k = NULL;
+                uint16_t f;
+                size_t l;
+                int a;
+
+                r = extract_many_words(&p, NULL, 0, &flags, &protocol, &algorithm, NULL);
+                if (r < 0)
+                        return log_warning_errno(r, "Failed to parse DNSKEY parameters on line %s:%u: %m", path, line);
+                if (r != 3) {
+                        log_warning("Missing DNSKEY parameters on line %s:%u", path, line);
+                        return -EINVAL;
+                }
+
+                if (!streq(protocol, "3")) {
+                        log_warning("DNSKEY Protocol is not 3 on line %s:%u", path, line);
+                        return -EINVAL;
+                }
+
+                r = safe_atou16(flags, &f);
+                if (r < 0)
+                        return log_warning_errno(r, "Failed to parse DNSKEY flags field %s on line %s:%u", flags, path, line);
+                if ((f & DNSKEY_FLAG_ZONE_KEY) == 0) {
+                        log_warning("DNSKEY lacks zone key bit set on line %s:%u", path, line);
+                        return -EINVAL;
+                }
+                if ((f & DNSKEY_FLAG_REVOKE)) {
+                        log_warning("DNSKEY is already revoked on line %s:%u", path, line);
+                        return -EINVAL;
+                }
+
+                a = dnssec_algorithm_from_string(algorithm);
+                if (a < 0) {
+                        log_warning("Failed to parse DNSKEY algorithm %s on line %s:%u", algorithm, path, line);
+                        return -EINVAL;
+                }
+
+                if (isempty(p)) {
+                        log_warning("Missing DNSKEY key on line %s:%u", path, line);
+                        return -EINVAL;
+                }
+
+                r = unbase64mem(p, strlen(p), &k, &l);
+                if (r < 0)
+                        return log_warning_errno(r, "Failed to parse DNSKEY key data %s on line %s:%u", p, path, line);
+
+                rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, domain);
+                if (!rr)
+                        return log_oom();
+
+                rr->dnskey.flags = f;
+                rr->dnskey.protocol = 3;
+                rr->dnskey.algorithm = a;
+                rr->dnskey.key_size = l;
+                rr->dnskey.key = TAKE_PTR(k);
+
+        } else {
+                log_warning("RR type %s is not supported, ignoring line %s:%u.", type, path, line);
+                return -EINVAL;
+        }
+
+        r = hashmap_ensure_allocated(&d->positive_by_key, &dns_resource_key_hash_ops);
+        if (r < 0)
+                return log_oom();
+
+        old_answer = hashmap_get(d->positive_by_key, rr->key);
+        answer = dns_answer_ref(old_answer);
+
+        r = dns_answer_add_extend(&answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add trust anchor RR: %m");
+
+        r = hashmap_replace(d->positive_by_key, rr->key, answer);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add answer to trust anchor: %m");
+
+        old_answer = dns_answer_unref(old_answer);
+        answer = NULL;
+
+        return 0;
+}
+
+static int dns_trust_anchor_load_negative(DnsTrustAnchor *d, const char *path, unsigned line, const char *s) {
+        _cleanup_free_ char *domain = NULL;
+        const char *p = s;
+        int r;
+
+        assert(d);
+        assert(line);
+
+        r = extract_first_word(&p, &domain, NULL, EXTRACT_UNQUOTE);
+        if (r < 0)
+                return log_warning_errno(r, "Unable to parse line %s:%u: %m", path, line);
+
+        r = dns_name_is_valid(domain);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to check validity of domain name '%s', at line %s:%u, ignoring line: %m", domain, path, line);
+        if (r == 0) {
+                log_warning("Domain name %s is invalid, at line %s:%u, ignoring line.", domain, path, line);
+                return -EINVAL;
+        }
+
+        if (!isempty(p)) {
+                log_warning("Trailing garbage at line %s:%u, ignoring line.", path, line);
+                return -EINVAL;
+        }
+
+        r = set_ensure_consume(&d->negative_by_name, &dns_name_hash_ops, TAKE_PTR(domain));
+        if (r < 0)
+                return log_oom();
+
+        return 0;
+}
+
+static int dns_trust_anchor_load_files(
+                DnsTrustAnchor *d,
+                const char *suffix,
+                int (*loader)(DnsTrustAnchor *d, const char *path, unsigned n, const char *line)) {
+
+        _cleanup_strv_free_ char **files = NULL;
+        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..6b3f5f7
--- /dev/null
+++ b/src/resolve/resolved-dns-zone.c
@@ -0,0 +1,700 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "list.h"
+#include "resolved-dns-packet.h"
+#include "resolved-dns-zone.h"
+#include "resolved-dnssd.h"
+#include "resolved-manager.h"
+#include "string-util.h"
+
+/* Never allow more than 1K entries */
+#define ZONE_MAX 1024
+
+void dns_zone_item_probe_stop(DnsZoneItem *i) {
+        DnsTransaction *t;
+        assert(i);
+
+        if (!i->probe_transaction)
+                return;
+
+        t = TAKE_PTR(i->probe_transaction);
+
+        set_remove(t->notify_zone_items, i);
+        set_remove(t->notify_zone_items_done, i);
+        dns_transaction_gc(t);
+}
+
+static DnsZoneItem* dns_zone_item_free(DnsZoneItem *i) {
+        if (!i)
+                return NULL;
+
+        dns_zone_item_probe_stop(i);
+        dns_resource_record_unref(i->rr);
+
+        return mfree(i);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsZoneItem*, dns_zone_item_free);
+
+static void dns_zone_item_remove_and_free(DnsZone *z, DnsZoneItem *i) {
+        DnsZoneItem *first;
+
+        assert(z);
+
+        if (!i)
+                return;
+
+        first = hashmap_get(z->by_key, i->rr->key);
+        LIST_REMOVE(by_key, first, i);
+        if (first)
+                assert_se(hashmap_replace(z->by_key, first->rr->key, first) >= 0);
+        else
+                hashmap_remove(z->by_key, i->rr->key);
+
+        first = hashmap_get(z->by_name, dns_resource_key_name(i->rr->key));
+        LIST_REMOVE(by_name, first, i);
+        if (first)
+                assert_se(hashmap_replace(z->by_name, dns_resource_key_name(first->rr->key), first) >= 0);
+        else
+                hashmap_remove(z->by_name, dns_resource_key_name(i->rr->key));
+
+        dns_zone_item_free(i);
+}
+
+void dns_zone_flush(DnsZone *z) {
+        DnsZoneItem *i;
+
+        assert(z);
+
+        while ((i = hashmap_first(z->by_key)))
+                dns_zone_item_remove_and_free(z, i);
+
+        assert(hashmap_size(z->by_key) == 0);
+        assert(hashmap_size(z->by_name) == 0);
+
+        z->by_key = hashmap_free(z->by_key);
+        z->by_name = hashmap_free(z->by_name);
+}
+
+DnsZoneItem* dns_zone_get(DnsZone *z, DnsResourceRecord *rr) {
+        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);
+
+        if (!rr)
+                return;
+
+        i = dns_zone_get(z, rr);
+        if (i)
+                dns_zone_item_remove_and_free(z, i);
+}
+
+int dns_zone_remove_rrs_by_key(DnsZone *z, DnsResourceKey *key) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL;
+        DnsResourceRecord *rr;
+        bool tentative;
+        int r;
+
+        r = dns_zone_lookup(z, key, 0, &answer, &soa, &tentative);
+        if (r < 0)
+                return r;
+
+        DNS_ANSWER_FOREACH(rr, answer)
+                dns_zone_remove_rr(z, rr);
+
+        return 0;
+}
+
+static int dns_zone_init(DnsZone *z) {
+        int r;
+
+        assert(z);
+
+        r = hashmap_ensure_allocated(&z->by_key, &dns_resource_key_hash_ops);
+        if (r < 0)
+                return r;
+
+        r = hashmap_ensure_allocated(&z->by_name, &dns_name_hash_ops);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int dns_zone_link_item(DnsZone *z, DnsZoneItem *i) {
+        DnsZoneItem *first;
+        int r;
+
+        first = hashmap_get(z->by_key, i->rr->key);
+        if (first) {
+                LIST_PREPEND(by_key, first, i);
+                assert_se(hashmap_replace(z->by_key, first->rr->key, first) >= 0);
+        } else {
+                r = hashmap_put(z->by_key, i->rr->key, i);
+                if (r < 0)
+                        return r;
+        }
+
+        first = hashmap_get(z->by_name, dns_resource_key_name(i->rr->key));
+        if (first) {
+                LIST_PREPEND(by_name, first, i);
+                assert_se(hashmap_replace(z->by_name, dns_resource_key_name(first->rr->key), first) >= 0);
+        } else {
+                r = hashmap_put(z->by_name, dns_resource_key_name(i->rr->key), i);
+                if (r < 0)
+                        return r;
+        }
+
+        return 0;
+}
+
+static int dns_zone_item_probe_start(DnsZoneItem *i)  {
+        _cleanup_(dns_transaction_gcp) DnsTransaction *t = NULL;
+        int r;
+
+        assert(i);
+
+        if (i->probe_transaction)
+                return 0;
+
+        t = dns_scope_find_transaction(
+                        i->scope,
+                        &DNS_RESOURCE_KEY_CONST(i->rr->key->class, DNS_TYPE_ANY, dns_resource_key_name(i->rr->key)),
+                        SD_RESOLVED_NO_CACHE|SD_RESOLVED_NO_ZONE);
+        if (!t) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+
+                key = dns_resource_key_new(i->rr->key->class, DNS_TYPE_ANY, dns_resource_key_name(i->rr->key));
+                if (!key)
+                        return -ENOMEM;
+
+                r = dns_transaction_new(&t, i->scope, key, NULL, SD_RESOLVED_NO_CACHE|SD_RESOLVED_NO_ZONE);
+                if (r < 0)
+                        return r;
+        }
+
+        r = set_ensure_allocated(&t->notify_zone_items_done, NULL);
+        if (r < 0)
+                return r;
+
+        r = set_ensure_put(&t->notify_zone_items, NULL, i);
+        if (r < 0)
+                return r;
+
+        t->probing = true;
+        i->probe_transaction = TAKE_PTR(t);
+
+        if (i->probe_transaction->state == DNS_TRANSACTION_NULL) {
+                i->block_ready++;
+                r = dns_transaction_go(i->probe_transaction);
+                i->block_ready--;
+
+                if (r < 0) {
+                        dns_zone_item_probe_stop(i);
+                        return r;
+                }
+        }
+
+        dns_zone_item_notify(i);
+        return 0;
+}
+
+int dns_zone_put(DnsZone *z, DnsScope *s, DnsResourceRecord *rr, bool probe) {
+        _cleanup_(dns_zone_item_freep) DnsZoneItem *i = NULL;
+        DnsZoneItem *existing;
+        int r;
+
+        assert(z);
+        assert(s);
+        assert(rr);
+
+        if (dns_class_is_pseudo(rr->key->class))
+                return -EINVAL;
+        if (dns_type_is_pseudo(rr->key->type))
+                return -EINVAL;
+
+        existing = dns_zone_get(z, rr);
+        if (existing)
+                return 0;
+
+        r = dns_zone_init(z);
+        if (r < 0)
+                return r;
+
+        i = new(DnsZoneItem, 1);
+        if (!i)
+                return -ENOMEM;
+
+        *i = (DnsZoneItem) {
+                .scope = s,
+                .rr = dns_resource_record_ref(rr),
+                .probing_enabled = probe,
+        };
+
+        r = dns_zone_link_item(z, i);
+        if (r < 0)
+                return r;
+
+        if (probe) {
+                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, NULL);
+}
+
+int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **ret_answer, DnsAnswer **ret_soa, bool *ret_tentative) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL;
+        unsigned n_answer = 0;
+        DnsZoneItem *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;
+
+        (void) dnssd_signal_conflict(i->scope->manager, dns_resource_key_name(i->rr->key));
+
+        /* Maybe change the hostname */
+        if (manager_is_own_hostname(i->scope->manager, dns_resource_key_name(i->rr->key)) > 0)
+                manager_next_hostname(i->scope->manager);
+}
+
+void dns_zone_item_notify(DnsZoneItem *i) {
+        assert(i);
+        assert(i->probe_transaction);
+
+        if (i->block_ready > 0)
+                return;
+
+        if (IN_SET(i->probe_transaction->state, DNS_TRANSACTION_NULL, DNS_TRANSACTION_PENDING, DNS_TRANSACTION_VALIDATING))
+                return;
+
+        if (i->probe_transaction->state == DNS_TRANSACTION_SUCCESS) {
+                bool we_lost = false;
+
+                /* The probe got a successful reply. If we so far
+                 * weren't established we just give up.
+                 *
+                 * In LLMNR case if we already
+                 * were established, and the peer has the
+                 * lexicographically larger IP address we continue
+                 * and defend it. */
+
+                if (!IN_SET(i->state, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) {
+                        log_debug("Got a successful probe for not yet established RR, we lost.");
+                        we_lost = true;
+                } else if (i->probe_transaction->scope->protocol == DNS_PROTOCOL_LLMNR) {
+                        assert(i->probe_transaction->received);
+                        we_lost = memcmp(&i->probe_transaction->received->sender, &i->probe_transaction->received->destination, FAMILY_ADDRESS_SIZE(i->probe_transaction->received->family)) < 0;
+                        if (we_lost)
+                                log_debug("Got a successful probe reply for an established RR, and we have a lexicographically larger IP address and thus lost.");
+                }
+
+                if (we_lost) {
+                        dns_zone_item_conflict(i);
+                        return;
+                }
+
+                log_debug("Got a successful probe reply, but peer has lexicographically lower IP address and thus lost.");
+        }
+
+        log_debug("Record %s successfully probed.", strna(dns_resource_record_to_string(i->rr)));
+
+        dns_zone_item_probe_stop(i);
+        i->state = DNS_ZONE_ITEM_ESTABLISHED;
+}
+
+static int dns_zone_item_verify(DnsZoneItem *i) {
+        int r;
+
+        assert(i);
+
+        if (i->state != DNS_ZONE_ITEM_ESTABLISHED)
+                return 0;
+
+        log_debug("Verifying RR %s", strna(dns_resource_record_to_string(i->rr)));
+
+        i->state = DNS_ZONE_ITEM_VERIFYING;
+        r = dns_zone_item_probe_start(i);
+        if (r < 0) {
+                log_error_errno(r, "Failed to start probing for verifying RR: %m");
+                i->state = DNS_ZONE_ITEM_ESTABLISHED;
+                return r;
+        }
+
+        return 0;
+}
+
+int dns_zone_check_conflicts(DnsZone *zone, DnsResourceRecord *rr) {
+        DnsZoneItem *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..f10eae3
--- /dev/null
+++ b/src/resolve/resolved-dnssd-gperf.gperf
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+%{
+#include <stddef.h>
+#include "conf-parser.h"
+#include "resolved-conf.h"
+#include "resolved-dnssd.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name resolved_dnssd_gperf_hash
+%define lookup-function-name resolved_dnssd_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Service.Name,     config_parse_dnssd_service_name, 0,                 0
+Service.Type,     config_parse_dnssd_service_type, 0,                 0
+Service.Port,     config_parse_ip_port,            0,                 offsetof(DnssdService, port)
+Service.Priority, config_parse_uint16,             0,                 offsetof(DnssdService, priority)
+Service.Weight,   config_parse_uint16,             0,                 offsetof(DnssdService, weight)
+Service.TxtText,  config_parse_dnssd_txt,          DNS_TXT_ITEM_TEXT, 0
+Service.TxtData,  config_parse_dnssd_txt,          DNS_TXT_ITEM_DATA, 0
diff --git a/src/resolve/resolved-dnssd.c b/src/resolve/resolved-dnssd.c
new file mode 100644
index 0000000..ab2773e
--- /dev/null
+++ b/src/resolve/resolved-dnssd.c
@@ -0,0 +1,365 @@
+/* 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(
+                        STRV_MAKE_CONST(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);
+                TAKE_PTR(txt_data);
+        }
+
+        r = hashmap_ensure_put(&manager->dnssd_services, &string_hash_ops, service->name, service);
+        if (r < 0)
+                return r;
+
+        service->manager = manager;
+
+        r = dnssd_update_rrs(service);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(service);
+
+        return 0;
+}
+
+static int specifier_dnssd_host_name(char specifier, const void *data, const char *root, 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, DNS_LABEL_MAX, specifier_table, NULL, 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;
+}
+
+int dnssd_signal_conflict(Manager *manager, const char *name) {
+        DnssdService *s;
+        int r;
+
+        if (sd_bus_is_ready(manager->bus) <= 0)
+                return 0;
+
+        HASHMAP_FOREACH(s, manager->dnssd_services) {
+                if (s->withdrawn)
+                        continue;
+
+                if (dns_name_equal(dns_resource_key_name(s->srv_rr->key), name)) {
+                        _cleanup_free_ char *path = NULL;
+
+                        s->withdrawn = true;
+
+                        r = sd_bus_path_encode("/org/freedesktop/resolve1/dnssd", s->name, &path);
+                        if (r < 0)
+                                return log_error_errno(r, "Can't get D-BUS object path: %m");
+
+                        r = sd_bus_emit_signal(manager->bus,
+                                               path,
+                                               "org.freedesktop.resolve1.DnssdService",
+                                               "Conflicted",
+                                               NULL);
+                        if (r < 0)
+                                return log_error_errno(r, "Cannot emit signal: %m");
+
+                        break;
+                }
+        }
+
+        return 0;
+}
diff --git a/src/resolve/resolved-dnssd.h b/src/resolve/resolved-dnssd.h
new file mode 100644
index 0000000..d020bb5
--- /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);
+int dnssd_signal_conflict(Manager *manager, const char *name);
diff --git a/src/resolve/resolved-dnstls-gnutls.c b/src/resolve/resolved-dnstls-gnutls.c
new file mode 100644
index 0000000..e7ccba9
--- /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_FULL(gnutls_session_t, gnutls_deinit, NULL);
+
+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..17af90d
--- /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_FULL(SSL*, SSL_free, NULL);
+DEFINE_TRIVIAL_CLEANUP_FUNC_FULL(BIO*, BIO_free, NULL);
+
+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..a8da6c3
--- /dev/null
+++ b/src/resolve/resolved-etc-hosts.c
@@ -0,0 +1,470 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "hostname-util.h"
+#include "resolved-dns-synthesize.h"
+#include "resolved-etc-hosts.h"
+#include "socket-netlink.h"
+#include "stat-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "time-util.h"
+
+/* Recheck /etc/hosts at most once every 2s */
+#define ETC_HOSTS_RECHECK_USEC (2*USEC_PER_SEC)
+
+static void etc_hosts_item_free(EtcHostsItem *item) {
+        strv_free(item->names);
+        free(item);
+}
+
+static void etc_hosts_item_by_name_free(EtcHostsItemByName *item) {
+        free(item->name);
+        free(item->addresses);
+        free(item);
+}
+
+void etc_hosts_free(EtcHosts *hosts) {
+        hosts->by_address = hashmap_free_with_destructor(hosts->by_address, etc_hosts_item_free);
+        hosts->by_name = hashmap_free_with_destructor(hosts->by_name, etc_hosts_item_by_name_free);
+        hosts->no_address = set_free_free(hosts->no_address);
+}
+
+void manager_etc_hosts_flush(Manager *m) {
+        etc_hosts_free(&m->etc_hosts);
+        m->etc_hosts_stat = (struct stat) {};
+}
+
+static int parse_line(EtcHosts *hosts, unsigned nr, const char *line) {
+        _cleanup_free_ char *address_str = NULL;
+        struct in_addr_data address = {};
+        bool found = false;
+        EtcHostsItem *item;
+        int r;
+
+        assert(hosts);
+        assert(line);
+
+        r = extract_first_word(&line, &address_str, NULL, EXTRACT_RELAX);
+        if (r < 0)
+                return log_error_errno(r, "/etc/hosts:%u: failed to extract address: %m", nr);
+        assert(r > 0); /* We already checked that the line is not empty, so it should contain *something* */
+
+        r = in_addr_ifindex_from_string_auto(address_str, &address.family, &address.address, NULL);
+        if (r < 0) {
+                log_warning_errno(r, "/etc/hosts:%u: address '%s' is invalid, ignoring: %m", nr, address_str);
+                return 0;
+        }
+
+        r = in_addr_data_is_null(&address);
+        if (r < 0) {
+                log_warning_errno(r, "/etc/hosts:%u: address '%s' is invalid, ignoring: %m", nr, address_str);
+                return 0;
+        }
+        if (r > 0)
+                /* This is an 0.0.0.0 or :: item, which we assume means that we shall map the specified hostname to
+                 * nothing. */
+                item = NULL;
+        else {
+                /* If this is a normal address, then simply add entry mapping it to the specified names */
+
+                item = hashmap_get(hosts->by_address, &address);
+                if (!item) {
+                        r = hashmap_ensure_allocated(&hosts->by_address, &in_addr_data_hash_ops);
+                        if (r < 0)
+                                return log_oom();
+
+                        item = new(EtcHostsItem, 1);
+                        if (!item)
+                                return log_oom();
+
+                        *item = (EtcHostsItem) {
+                                .address = address,
+                        };
+
+                        r = hashmap_put(hosts->by_address, &item->address, item);
+                        if (r < 0) {
+                                free(item);
+                                return log_oom();
+                        }
+                }
+        }
+
+        for (;;) {
+                _cleanup_free_ char *name = NULL;
+                EtcHostsItemByName *bn;
+
+                r = extract_first_word(&line, &name, NULL, EXTRACT_RELAX);
+                if (r < 0)
+                        return log_error_errno(r, "/etc/hosts:%u: couldn't extract hostname: %m", nr);
+                if (r == 0)
+                        break;
+
+                found = true;
+
+                r = dns_name_is_valid_ldh(name);
+                if (r <= 0) {
+                        if (r < 0)
+                                log_warning_errno(r, "/etc/hosts:%u: Failed to check the validity of hostname \"%s\", ignoring: %m", nr, name);
+                        else
+                                log_warning("/etc/hosts:%u: hostname \"%s\" is not valid, ignoring.", nr, name);
+                        continue;
+                }
+
+                if (!item) {
+                        /* Optimize the case where we don't need to store any addresses, by storing
+                         * only the name in a dedicated Set instead of the hashmap */
+
+                        r = set_ensure_consume(&hosts->no_address, &dns_name_hash_ops, TAKE_PTR(name));
+                        if (r < 0)
+                                return r;
+
+                        continue;
+                }
+
+                r = strv_extend(&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_addresses + 1))
+                        return log_oom();
+
+                bn->addresses[bn->n_addresses++] = &item->address;
+        }
+
+        if (!found)
+                log_warning("/etc/hosts:%u: line is missing any hostnames", nr);
+
+        return 0;
+}
+
+static void strip_localhost(EtcHosts *hosts) {
+        static const struct in_addr_data local_in_addrs[] = {
+                {
+                        .family = AF_INET,
+#if __BYTE_ORDER == __LITTLE_ENDIAN
+                        /* We want constant expressions here, that's why we don't use htole32() here */
+                        .address.in.s_addr = UINT32_C(0x0100007F),
+#else
+                        .address.in.s_addr = UINT32_C(0x7F000001),
+#endif
+                },
+                {
+                        .family = AF_INET6,
+                        .address.in6 = IN6ADDR_LOOPBACK_INIT,
+                },
+        };
+
+        EtcHostsItem *item;
+
+        assert(hosts);
+
+        /* Removes the 'localhost' entry from what we loaded. But only if the mapping is exclusively between
+         * 127.0.0.1 and localhost (or aliases to that we recognize). If there's any other name assigned to
+         * it, we leave the entry in.
+         *
+         * This way our regular synthesizing can take over, but only if it would result in the exact same
+         * mappings.  */
+
+        for (size_t j = 0; j < ELEMENTSOF(local_in_addrs); j++) {
+                bool all_localhost, in_order;
+                char **i;
+
+                item = hashmap_get(hosts->by_address, local_in_addrs + j);
+                if (!item)
+                        continue;
+
+                /* Check whether all hostnames the loopback address points to are localhost ones */
+                all_localhost = true;
+                STRV_FOREACH(i, item->names)
+                        if (!is_localhost(*i)) {
+                                all_localhost = false;
+                                break;
+                        }
+
+                if (!all_localhost) /* Not all names are localhost, hence keep the entries for this address. */
+                        continue;
+
+                /* Now check if the names listed for this address actually all point back just to this
+                 * address (or the other loopback address). If not, let's stay away from this too. */
+                in_order = true;
+                STRV_FOREACH(i, item->names) {
+                        EtcHostsItemByName *n;
+                        bool all_local_address;
+
+                        n = hashmap_get(hosts->by_name, *i);
+                        if (!n) /* No reverse entry? Then almost certainly the entry already got deleted from
+                                 * the previous iteration of this loop, i.e. via the other protocol */
+                                break;
+
+                        /* Now check if the addresses of this item are all localhost addresses */
+                        all_local_address = true;
+                        for (size_t m = 0; m < n->n_addresses; m++)
+                                if (!in_addr_is_localhost(n->addresses[m]->family, &n->addresses[m]->address)) {
+                                        all_local_address = false;
+                                        break;
+                                }
+
+                        if (!all_local_address) {
+                                in_order = false;
+                                break;
+                        }
+                }
+
+                if (!in_order)
+                        continue;
+
+                STRV_FOREACH(i, item->names) {
+                        EtcHostsItemByName *n;
+
+                        n = hashmap_remove(hosts->by_name, *i);
+                        if (n)
+                                etc_hosts_item_by_name_free(n);
+                }
+
+                assert_se(hashmap_remove(hosts->by_address, local_in_addrs + j) == item);
+                etc_hosts_item_free(item);
+        }
+}
+
+int etc_hosts_parse(EtcHosts *hosts, FILE *f) {
+        _cleanup_(etc_hosts_free) EtcHosts t = {};
+        unsigned nr = 0;
+        int r;
+
+        for (;;) {
+                _cleanup_free_ char *line = NULL;
+                char *l;
+
+                r = read_line(f, LONG_LINE_MAX, &line);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to read /etc/hosts: %m");
+                if (r == 0)
+                        break;
+
+                nr++;
+
+                l = strchr(line, '#');
+                if (l)
+                        *l = '\0';
+
+                l = strstrip(line);
+                if (isempty(l))
+                        continue;
+
+                r = parse_line(&t, nr, l);
+                if (r < 0)
+                        return r;
+        }
+
+        strip_localhost(&t);
+
+        etc_hosts_free(hosts);
+        *hosts = t;
+        t = (EtcHosts) {}; /* prevent cleanup */
+        return 0;
+}
+
+static int manager_etc_hosts_read(Manager *m) {
+        _cleanup_fclose_ FILE *f = NULL;
+        struct stat st;
+        usec_t ts;
+        int r;
+
+        assert_se(sd_event_now(m->event, clock_boottime_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_stat.st_mode != 0) {
+                if (stat("/etc/hosts", &st) < 0) {
+                        if (errno != ENOENT)
+                                return log_error_errno(errno, "Failed to stat /etc/hosts: %m");
+
+                        manager_etc_hosts_flush(m);
+                        return 0;
+                }
+
+                /* Did the mtime or ino/dev change? If not, there's no point in re-reading the file. */
+                if (stat_inode_unmodified(&m->etc_hosts_stat, &st))
+                        return 0;
+        }
+
+        f = fopen("/etc/hosts", "re");
+        if (!f) {
+                if (errno != ENOENT)
+                        return log_error_errno(errno, "Failed to open /etc/hosts: %m");
+
+                manager_etc_hosts_flush(m);
+                return 0;
+        }
+
+        /* Take the timestamp at the beginning of processing, so that any changes made later are read on the next
+         * invocation */
+        r = fstat(fileno(f), &st);
+        if (r < 0)
+                return log_error_errno(errno, "Failed to fstat() /etc/hosts: %m");
+
+        r = etc_hosts_parse(&m->etc_hosts, f);
+        if (r < 0)
+                return r;
+
+        m->etc_hosts_stat = st;
+        m->etc_hosts_last = ts;
+
+        return 1;
+}
+
+int manager_etc_hosts_lookup(Manager *m, DnsQuestion* q, DnsAnswer **answer) {
+        bool found_a = false, found_aaaa = false;
+        struct in_addr_data k = {};
+        EtcHostsItemByName *bn;
+        DnsResourceKey *t;
+        const char *name;
+        unsigned i;
+        int r;
+
+        assert(m);
+        assert(q);
+        assert(answer);
+
+        if (!m->read_etc_hosts)
+                return 0;
+
+        (void) manager_etc_hosts_read(m);
+
+        name = dns_question_first_name(q);
+        if (!name)
+                return 0;
+
+        r = dns_name_address(name, &k.family, &k.address);
+        if (r > 0) {
+                EtcHostsItem *item;
+                DnsResourceKey *found_ptr = NULL;
+
+                item = hashmap_get(m->etc_hosts.by_address, &k);
+                if (!item)
+                        return 0;
+
+                /* We have an address in /etc/hosts that matches the queried name. Let's return successful. Actual data
+                 * we'll only return if the request was for PTR. */
+
+                DNS_QUESTION_FOREACH(t, q) {
+                        if (!IN_SET(t->type, DNS_TYPE_PTR, DNS_TYPE_ANY))
+                                continue;
+                        if (!IN_SET(t->class, DNS_CLASS_IN, DNS_CLASS_ANY))
+                                continue;
+
+                        r = dns_name_equal(dns_resource_key_name(t), name);
+                        if (r < 0)
+                                return r;
+                        if (r > 0) {
+                                found_ptr = t;
+                                break;
+                        }
+                }
+
+                if (found_ptr) {
+                        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, NULL);
+                                if (r < 0)
+                                        return r;
+                        }
+                }
+
+                return 1;
+        }
+
+        bn = hashmap_get(m->etc_hosts.by_name, name);
+        if (bn) {
+                r = dns_answer_reserve(answer, bn->n_addresses);
+                if (r < 0)
+                        return r;
+        } else {
+                /* Check if name was listed with no address. If yes, continue to return an answer. */
+                if (!set_contains(m->etc_hosts.no_address, name))
+                        return 0;
+        }
+
+        DNS_QUESTION_FOREACH(t, q) {
+                if (!IN_SET(t->type, DNS_TYPE_A, DNS_TYPE_AAAA, DNS_TYPE_ANY))
+                        continue;
+                if (!IN_SET(t->class, DNS_CLASS_IN, DNS_CLASS_ANY))
+                        continue;
+
+                r = dns_name_equal(dns_resource_key_name(t), name);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        continue;
+
+                if (IN_SET(t->type, DNS_TYPE_A, DNS_TYPE_ANY))
+                        found_a = true;
+                if (IN_SET(t->type, DNS_TYPE_AAAA, DNS_TYPE_ANY))
+                        found_aaaa = true;
+
+                if (found_a && found_aaaa)
+                        break;
+        }
+
+        for (i = 0; bn && i < bn->n_addresses; i++) {
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+
+                if ((!found_a && bn->addresses[i]->family == AF_INET) ||
+                    (!found_aaaa && bn->addresses[i]->family == AF_INET6))
+                        continue;
+
+                r = dns_resource_record_new_address(&rr, bn->addresses[i]->family, &bn->addresses[i]->address, bn->name);
+                if (r < 0)
+                        return r;
+
+                r = dns_answer_add(*answer, rr, 0, DNS_ANSWER_AUTHENTICATED, NULL);
+                if (r < 0)
+                        return r;
+        }
+
+        return found_a || found_aaaa;
+}
diff --git a/src/resolve/resolved-etc-hosts.h b/src/resolve/resolved-etc-hosts.h
new file mode 100644
index 0000000..c85926a
--- /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;
+} EtcHostsItemByName;
+
+int etc_hosts_parse(EtcHosts *hosts, FILE *f);
+void etc_hosts_free(EtcHosts *hosts);
+
+void manager_etc_hosts_flush(Manager *m);
+int manager_etc_hosts_lookup(Manager *m, DnsQuestion* q, DnsAnswer **answer);
diff --git a/src/resolve/resolved-gperf.gperf b/src/resolve/resolved-gperf.gperf
new file mode 100644
index 0000000..eab4c7e
--- /dev/null
+++ b/src/resolve/resolved-gperf.gperf
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+%{
+#if __GNUC__ >= 7
+_Pragma("GCC diagnostic ignored \"-Wimplicit-fallthrough\"")
+#endif
+#include <stddef.h>
+#include "conf-parser.h"
+#include "resolved-conf.h"
+#include "resolved-manager.h"
+%}
+struct ConfigPerfItem;
+%null_strings
+%language=ANSI-C
+%define slot-name section_and_lvalue
+%define hash-function-name resolved_gperf_hash
+%define lookup-function-name resolved_gperf_lookup
+%readonly-tables
+%omit-struct-type
+%struct-type
+%includes
+%%
+Resolve.DNS,                       config_parse_dns_servers,             DNS_SERVER_SYSTEM,   0
+Resolve.FallbackDNS,               config_parse_dns_servers,             DNS_SERVER_FALLBACK, 0
+Resolve.Domains,                   config_parse_search_domains,          0,                   0
+Resolve.LLMNR,                     config_parse_resolve_support,         0,                   offsetof(Manager, llmnr_support)
+Resolve.MulticastDNS,              config_parse_resolve_support,         0,                   offsetof(Manager, mdns_support)
+Resolve.DNSSEC,                    config_parse_dnssec_mode,             0,                   offsetof(Manager, dnssec_mode)
+Resolve.DNSOverTLS,                config_parse_dns_over_tls_mode,       0,                   offsetof(Manager, dns_over_tls_mode)
+Resolve.Cache,                     config_parse_dns_cache_mode,          DNS_CACHE_MODE_YES,  offsetof(Manager, enable_cache)
+Resolve.DNSStubListener,           config_parse_dns_stub_listener_mode,  0,                   offsetof(Manager, dns_stub_listener_mode)
+Resolve.ReadEtcHosts,              config_parse_bool,                    0,                   offsetof(Manager, read_etc_hosts)
+Resolve.ResolveUnicastSingleLabel, config_parse_bool,                    0,                   offsetof(Manager, resolve_unicast_single_label)
+Resolve.DNSStubListenerExtra,      config_parse_dns_stub_listener_extra, 0,                   offsetof(Manager, dns_extra_stub_listeners)
+Resolve.CacheFromLocalhost,        config_parse_bool,                    0,                   offsetof(Manager, cache_from_localhost)
diff --git a/src/resolve/resolved-link-bus.c b/src/resolve/resolved-link-bus.c
new file mode 100644
index 0000000..8d533d7
--- /dev/null
+++ b/src/resolve/resolved-link-bus.c
@@ -0,0 +1,951 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <sys/capability.h>
+
+#include "alloc-util.h"
+#include "bus-common-errors.h"
+#include "bus-get-properties.h"
+#include "bus-message-util.h"
+#include "bus-polkit.h"
+#include "log-link.h"
+#include "parse-util.h"
+#include "resolve-util.h"
+#include "resolved-bus.h"
+#include "resolved-link-bus.h"
+#include "resolved-resolv-conf.h"
+#include "socket-netlink.h"
+#include "stdio-util.h"
+#include "strv.h"
+#include "user-util.h"
+
+static BUS_DEFINE_PROPERTY_GET(property_get_dnssec_supported, "b", Link, link_dnssec_supported);
+static BUS_DEFINE_PROPERTY_GET2(property_get_dnssec_mode, "s", Link, link_get_dnssec_mode, dnssec_mode_to_string);
+
+static int property_get_dns_over_tls_mode(
+                sd_bus *bus,
+                const char *path,
+                const char *interface,
+                const char *property,
+                sd_bus_message *reply,
+                void *userdata,
+                sd_bus_error *error) {
+
+        Link *l = 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) {
+        _cleanup_free_ char *j = NULL;
+        struct in_addr_full **dns;
+        bool changed = false;
+        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;
+        }
+
+        for (size_t i = 0; i < n; i++) {
+                const char *s;
+
+                s = in_addr_full_to_string(dns[i]);
+                if (!s) {
+                        r = -ENOMEM;
+                        goto finalize;
+                }
+
+                if (!strextend_with_separator(&j, ", ", s)) {
+                        r = -ENOMEM;
+                        goto finalize;
+                }
+        }
+
+        bus_client_log(message, "DNS server change");
+
+        dns_server_mark_all(l->dns_servers);
+
+        for (size_t i = 0; i < n; i++) {
+                DnsServer *s;
+
+                s = dns_server_find(l->dns_servers, dns[i]->family, &dns[i]->address, dns[i]->port, 0, dns[i]->server_name);
+                if (s)
+                        dns_server_move_back_and_unmark(s);
+                else {
+                        r = dns_server_new(l->manager, NULL, DNS_SERVER_LINK, l, dns[i]->family, &dns[i]->address, dns[i]->port, 0, dns[i]->server_name);
+                        if (r < 0) {
+                                dns_server_unlink_all(l->dns_servers);
+                                goto finalize;
+                        }
+
+                        changed = true;
+                }
+
+        }
+
+        changed = dns_server_unlink_marked(l->dns_servers) || changed;
+
+        if (changed) {
+                link_allocate_scopes(l);
+
+                (void) link_save_user(l);
+                (void) manager_write_resolv_conf(l->manager);
+                (void) manager_send_changed(l->manager, "DNS");
+
+                if (j)
+                        log_link_info(l, "Bus client set DNS server list to: %s", j);
+                else
+                        log_link_info(l, "Bus client reset DNS server list.");
+        }
+
+        r = sd_bus_reply_method_return(message, NULL);
+
+finalize:
+        for (size_t i = 0; i < n; i++)
+                in_addr_full_free(dns[i]);
+        free(dns);
+
+        return r;
+}
+
+int bus_link_method_set_dns_servers(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return bus_link_method_set_dns_servers_internal(message, userdata, error, false);
+}
+
+int bus_link_method_set_dns_servers_ex(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        return bus_link_method_set_dns_servers_internal(message, userdata, error, true);
+}
+
+int bus_link_method_set_domains(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_free_ char *j = NULL;
+        Link *l = userdata;
+        bool changed = false;
+        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 (;;) {
+                _cleanup_free_ char *prefixed = NULL;
+                const char *name;
+                int route_only;
+
+                r = sd_bus_message_read(message, "(sb)", &name, &route_only);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        break;
+
+                r = dns_name_is_valid(name);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid search domain %s", name);
+                if (!route_only && dns_name_is_root(name))
+                        return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Root domain is not suitable as search domain");
+
+                if (route_only) {
+                        prefixed = strjoin("~", name);
+                        if (!prefixed)
+                                return -ENOMEM;
+
+                        name = prefixed;
+                }
+
+                if (!strextend_with_separator(&j, ", ", name))
+                        return -ENOMEM;
+        }
+
+        r = sd_bus_message_rewind(message, false);
+        if (r < 0)
+                return r;
+
+        r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+                                    "org.freedesktop.resolve1.set-domains",
+                                    NULL, true, UID_INVALID,
+                                    &l->manager->polkit_registry, error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Polkit will call us back */
+
+        bus_client_log(message, "dns domains change");
+
+        dns_search_domain_mark_all(l->search_domains);
+
+        for (;;) {
+                DnsSearchDomain *d;
+                const char *name;
+                int route_only;
+
+                r = sd_bus_message_read(message, "(sb)", &name, &route_only);
+                if (r < 0)
+                        goto clear;
+                if (r == 0)
+                        break;
+
+                r = dns_search_domain_find(l->search_domains, name, &d);
+                if (r < 0)
+                        goto clear;
+
+                if (r > 0)
+                        dns_search_domain_move_back_and_unmark(d);
+                else {
+                        r = dns_search_domain_new(l->manager, &d, DNS_SEARCH_DOMAIN_LINK, l, name);
+                        if (r < 0)
+                                goto clear;
+
+                        changed = true;
+                }
+
+                d->route_only = route_only;
+        }
+
+        r = sd_bus_message_exit_container(message);
+        if (r < 0)
+                goto clear;
+
+        changed = dns_search_domain_unlink_marked(l->search_domains) || changed;
+
+        if (changed) {
+                (void) link_save_user(l);
+                (void) manager_write_resolv_conf(l->manager);
+
+                if (j)
+                        log_link_info(l, "Bus client set search domain list to: %s", j);
+                else
+                        log_link_info(l, "Bus client reset search domain list.");
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+
+clear:
+        dns_search_domain_unlink_all(l->search_domains);
+        return r;
+}
+
+int bus_link_method_set_default_route(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Link *l = 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 */
+
+        bus_client_log(message, "dns default route change");
+
+        if (l->default_route != b) {
+                l->default_route = b;
+
+                (void) link_save_user(l);
+                (void) manager_write_resolv_conf(l->manager);
+
+                log_link_info(l, "Bus client set default route setting: %s", yes_no(b));
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_llmnr(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Link *l = 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 */
+
+        bus_client_log(message, "LLMNR change");
+
+        if (l->llmnr_support != mode) {
+                l->llmnr_support = mode;
+                link_allocate_scopes(l);
+                link_add_rrs(l, false);
+
+                (void) link_save_user(l);
+
+                log_link_info(l, "Bus client set LLMNR setting: %s", resolve_support_to_string(mode));
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_mdns(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Link *l = 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 */
+
+        bus_client_log(message, "mDNS change");
+
+        if (l->mdns_support != mode) {
+                l->mdns_support = mode;
+                link_allocate_scopes(l);
+                link_add_rrs(l, false);
+
+                (void) link_save_user(l);
+
+                log_link_info(l, "Bus client set MulticastDNS setting: %s", resolve_support_to_string(mode));
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_dns_over_tls(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Link *l = 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 */
+
+        bus_client_log(message, "D-o-T change");
+
+        if (l->dns_over_tls_mode != mode) {
+                link_set_dns_over_tls_mode(l, mode);
+
+                (void) link_save_user(l);
+
+                log_link_info(l, "Bus client set DNSOverTLS setting: %s",
+                              mode < 0 ? "default" : dns_over_tls_mode_to_string(mode));
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_dnssec(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Link *l = 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 */
+
+        bus_client_log(message, "DNSSEC change");
+
+        if (l->dnssec_mode != mode) {
+                link_set_dnssec_mode(l, mode);
+
+                (void) link_save_user(l);
+
+                log_link_info(l, "Bus client set DNSSEC setting: %s",
+                              mode < 0 ? "default" : dnssec_mode_to_string(mode));
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_set_dnssec_negative_trust_anchors(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        _cleanup_set_free_free_ Set *ns = NULL;
+        _cleanup_strv_free_ char **ntas = NULL;
+        _cleanup_free_ char *j = NULL;
+        Link *l = 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;
+
+                if (!strextend_with_separator(&j, ", ", *i))
+                        return -ENOMEM;
+        }
+
+        r = bus_verify_polkit_async(message, CAP_NET_ADMIN,
+                                    "org.freedesktop.resolve1.set-dnssec-negative-trust-anchors",
+                                    NULL, true, UID_INVALID,
+                                    &l->manager->polkit_registry, error);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return 1; /* Polkit will call us back */
+
+        bus_client_log(message, "DNSSEC NTA change");
+
+        if (!set_equal(ns, l->dnssec_negative_trust_anchors)) {
+                set_free_free(l->dnssec_negative_trust_anchors);
+                l->dnssec_negative_trust_anchors = TAKE_PTR(ns);
+
+                (void) link_save_user(l);
+
+                if (j)
+                        log_link_info(l, "Bus client set NTA list to: %s", j);
+                else
+                        log_link_info(l, "Bus client reset NTA list.");
+        }
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+int bus_link_method_revert(sd_bus_message *message, void *userdata, sd_bus_error *error) {
+        Link *l = 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 */
+
+        bus_client_log(message, "revert");
+
+        link_flush_settings(l);
+        link_allocate_scopes(l);
+        link_add_rrs(l, false);
+
+        (void) link_save_user(l);
+        (void) manager_write_resolv_conf(l->manager);
+        (void) manager_send_changed(l->manager, "DNS");
+
+        return sd_bus_reply_method_return(message, NULL);
+}
+
+static int link_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) {
+        _cleanup_free_ char *e = NULL;
+        Manager *m = 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..18dc3d2
--- /dev/null
+++ b/src/resolve/resolved-link.c
@@ -0,0 +1,1417 @@
+/* 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);
+
+        l = new(Link, 1);
+        if (!l)
+                return -ENOMEM;
+
+        *l = (Link) {
+                .ifindex = ifindex,
+                .default_route = -1,
+                .llmnr_support = RESOLVE_SUPPORT_YES,
+                .mdns_support = RESOLVE_SUPPORT_NO,
+                .dnssec_mode = _DNSSEC_MODE_INVALID,
+                .dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID,
+                .operstate = IF_OPER_UNKNOWN,
+        };
+
+        if (asprintf(&l->state_file, "/run/systemd/resolve/netif/%i", ifindex) < 0)
+                return -ENOMEM;
+
+        r = hashmap_ensure_put(&m->links, NULL, INT_TO_PTR(ifindex), l);
+        if (r < 0)
+                return r;
+
+        l->manager = m;
+
+        if (ret)
+                *ret = l;
+        TAKE_PTR(l);
+
+        return 0;
+}
+
+void link_flush_settings(Link *l) {
+        assert(l);
+
+        l->default_route = -1;
+        l->llmnr_support = RESOLVE_SUPPORT_YES;
+        l->mdns_support = RESOLVE_SUPPORT_NO;
+        l->dnssec_mode = _DNSSEC_MODE_INVALID;
+        l->dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID;
+
+        dns_server_unlink_all(l->dns_servers);
+        dns_search_domain_unlink_all(l->search_domains);
+
+        l->dnssec_negative_trust_anchors = set_free_free(l->dnssec_negative_trust_anchors);
+}
+
+Link *link_free(Link *l) {
+        if (!l)
+                return NULL;
+
+        /* Send goodbye messages. */
+        dns_scope_announce(l->mdns_ipv4_scope, true);
+        dns_scope_announce(l->mdns_ipv6_scope, true);
+
+        link_flush_settings(l);
+
+        while (l->addresses)
+                (void) link_address_free(l->addresses);
+
+        if (l->manager)
+                hashmap_remove(l->manager->links, INT_TO_PTR(l->ifindex));
+
+        dns_scope_free(l->unicast_scope);
+        dns_scope_free(l->llmnr_ipv4_scope);
+        dns_scope_free(l->llmnr_ipv6_scope);
+        dns_scope_free(l->mdns_ipv4_scope);
+        dns_scope_free(l->mdns_ipv6_scope);
+
+        free(l->state_file);
+        free(l->ifname);
+
+        return mfree(l);
+}
+
+void link_allocate_scopes(Link *l) {
+        bool unicast_relevant;
+        int r;
+
+        assert(l);
+
+        /* If a link that used to be relevant is no longer, or a link that did not use to be relevant now becomes
+         * relevant, let's reinit the learnt global DNS server information, since we might talk to different servers
+         * now, even if they have the same addresses as before. */
+
+        unicast_relevant = link_relevant(l, AF_UNSPEC, false);
+        if (unicast_relevant != l->unicast_relevant) {
+                l->unicast_relevant = unicast_relevant;
+
+                dns_server_reset_features_all(l->manager->fallback_dns_servers);
+                dns_server_reset_features_all(l->manager->dns_servers);
+
+                /* Also, flush the global unicast scope, to deal with split horizon setups, where talking through one
+                 * interface reveals different DNS zones than through others. */
+                if (l->manager->unicast_scope)
+                        dns_cache_flush(&l->manager->unicast_scope->cache);
+        }
+
+        /* And now, allocate all scopes that makes sense now if we didn't have them yet, and drop those which we don't
+         * need anymore */
+
+        if (unicast_relevant && l->dns_servers) {
+                if (!l->unicast_scope) {
+                        dns_server_reset_features_all(l->dns_servers);
+
+                        r = dns_scope_new(l->manager, &l->unicast_scope, l, DNS_PROTOCOL_DNS, AF_UNSPEC);
+                        if (r < 0)
+                                log_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);
+
+        l->llmnr_support = RESOLVE_SUPPORT_YES; /* yes, yes, we set it twice which is ugly */
+
+        r = sd_network_link_get_llmnr(l->ifindex, &b);
+        if (r == -ENODATA)
+                return 0;
+        if (r < 0)
+                return r;
+
+        r = resolve_support_from_string(b);
+        if (r < 0)
+                return r;
+
+        l->llmnr_support = r;
+        return 0;
+}
+
+static int link_update_mdns_support(Link *l) {
+        _cleanup_free_ char *b = NULL;
+        int r;
+
+        assert(l);
+
+        l->mdns_support = RESOLVE_SUPPORT_NO;
+
+        r = sd_network_link_get_mdns(l->ifindex, &b);
+        if (r == -ENODATA)
+                return 0;
+        if (r < 0)
+                return r;
+
+        r = resolve_support_from_string(b);
+        if (r < 0)
+                return r;
+
+        l->mdns_support = r;
+        return 0;
+}
+
+void link_set_dns_over_tls_mode(Link *l, DnsOverTlsMode mode) {
+
+        assert(l);
+
+#if ! ENABLE_DNS_OVER_TLS
+        if (mode != DNS_OVER_TLS_NO)
+                log_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);
+
+        l->dns_over_tls_mode = _DNS_OVER_TLS_MODE_INVALID;
+
+        r = sd_network_link_get_dns_over_tls(l->ifindex, &b);
+        if (r == -ENODATA)
+                return 0;
+        if (r < 0)
+                return r;
+
+        r = dns_over_tls_mode_from_string(b);
+        if (r < 0)
+                return r;
+
+        l->dns_over_tls_mode = r;
+        return 0;
+}
+
+void link_set_dnssec_mode(Link *l, DnssecMode mode) {
+
+        assert(l);
+
+#if ! HAVE_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);
+
+        l->dnssec_mode = _DNSSEC_MODE_INVALID;
+
+        r = sd_network_link_get_dnssec(l->ifindex, &m);
+        if (r == -ENODATA)
+                return 0;
+        if (r < 0)
+                return r;
+
+        mode = dnssec_mode_from_string(m);
+        if (mode < 0)
+                return mode;
+
+        link_set_dnssec_mode(l, mode);
+        return 0;
+}
+
+static int link_update_dnssec_negative_trust_anchors(Link *l) {
+        _cleanup_strv_free_ char **ntas = NULL;
+        _cleanup_set_free_free_ Set *ns = NULL;
+        int r;
+
+        assert(l);
+
+        l->dnssec_negative_trust_anchors = set_free_free(l->dnssec_negative_trust_anchors);
+
+        r = sd_network_link_get_dnssec_negative_trust_anchors(l->ifindex, &ntas);
+        if (r == -ENODATA)
+                return r;
+        if (r < 0)
+                return r;
+
+        ns = set_new(&dns_name_hash_ops);
+        if (!ns)
+                return -ENOMEM;
+
+        r = set_put_strdupv(&ns, ntas);
+        if (r < 0)
+                return r;
+
+        l->dnssec_negative_trust_anchors = TAKE_PTR(ns);
+        return 0;
+}
+
+static int link_update_search_domain_one(Link *l, const char *name, bool route_only) {
+        DnsSearchDomain *d;
+        int r;
+
+        assert(l);
+        assert(name);
+
+        r = dns_search_domain_find(l->search_domains, name, &d);
+        if (r < 0)
+                return r;
+        if (r > 0)
+                dns_search_domain_move_back_and_unmark(d);
+        else {
+                r = dns_search_domain_new(l->manager, &d, DNS_SEARCH_DOMAIN_LINK, l, name);
+                if (r < 0)
+                        return r;
+        }
+
+        d->route_only = route_only;
+        return 0;
+}
+
+static int link_update_search_domains(Link *l) {
+        _cleanup_strv_free_ char **sdomains = NULL, **rdomains = NULL;
+        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);
+
+        if (!IN_SET(family, AF_INET, AF_INET6))
+                return NULL;
+
+        if (!in_addr)
+                return NULL;
+
+        LIST_FOREACH(addresses, a, l->addresses)
+                if (a->family == family && in_addr_equal(family, &a->in_addr, in_addr))
+                        return a;
+
+        return NULL;
+}
+
+DnsServer* link_set_dns_server(Link *l, DnsServer *s) {
+        assert(l);
+
+        if (l->current_dns_server == s)
+                return s;
+
+        if (s)
+                log_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, DnsServer *if_current) {
+        assert(l);
+
+        /* If the current server of the transaction is specified, and we already are at a different one,
+         * don't do anything */
+        if (if_current && l->current_dns_server != if_current)
+                return;
+
+        /* If currently have no DNS server, then don't do anything, we'll pick it lazily the next time a DNS
+         * server is needed. */
+        if (!l->current_dns_server)
+                return;
+
+        /* Change to the next one, but make sure to follow the linked list only if this server is actually
+         * still linked. */
+        if (l->current_dns_server->linked && l->current_dns_server->servers_next) {
+                link_set_dns_server(l, l->current_dns_server->servers_next);
+                return;
+        }
+
+        /* Pick the first one again, after we reached the end */
+        link_set_dns_server(l, l->dns_servers);
+}
+
+DnsOverTlsMode link_get_dns_over_tls_mode(Link *l) {
+        assert(l);
+
+        if (l->dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID)
+                return l->dns_over_tls_mode;
+
+        return manager_get_dns_over_tls_mode(l->manager);
+}
+
+DnssecMode link_get_dnssec_mode(Link *l) {
+        assert(l);
+
+        if (l->dnssec_mode != _DNSSEC_MODE_INVALID)
+                return l->dnssec_mode;
+
+        return manager_get_dnssec_mode(l->manager);
+}
+
+bool link_dnssec_supported(Link *l) {
+        DnsServer *server;
+
+        assert(l);
+
+        if (link_get_dnssec_mode(l) == DNSSEC_NO)
+                return false;
+
+        server = link_get_dns_server(l);
+        if (server)
+                return dns_server_dnssec_supported(server);
+
+        return true;
+}
+
+int link_address_new(Link *l, LinkAddress **ret, int family, const union in_addr_union *in_addr) {
+        LinkAddress *a;
+
+        assert(l);
+        assert(in_addr);
+
+        a = new(LinkAddress, 1);
+        if (!a)
+                return -ENOMEM;
+
+        *a = (LinkAddress) {
+                .family = family,
+                .in_addr = *in_addr,
+                .link = l,
+                .prefixlen = UCHAR_MAX,
+        };
+
+        LIST_PREPEND(addresses, l->addresses, a);
+        l->n_addresses++;
+
+        if (ret)
+                *ret = a;
+
+        return 0;
+}
+
+LinkAddress *link_address_free(LinkAddress *a) {
+        if (!a)
+                return NULL;
+
+        if (a->link) {
+                LIST_REMOVE(addresses, a->link->addresses, a);
+
+                assert(a->link->n_addresses > 0);
+                a->link->n_addresses--;
+
+                if (a->llmnr_address_rr) {
+                        if (a->family == AF_INET && a->link->llmnr_ipv4_scope)
+                                dns_zone_remove_rr(&a->link->llmnr_ipv4_scope->zone, a->llmnr_address_rr);
+                        else if (a->family == AF_INET6 && a->link->llmnr_ipv6_scope)
+                                dns_zone_remove_rr(&a->link->llmnr_ipv6_scope->zone, a->llmnr_address_rr);
+                }
+
+                if (a->llmnr_ptr_rr) {
+                        if (a->family == AF_INET && a->link->llmnr_ipv4_scope)
+                                dns_zone_remove_rr(&a->link->llmnr_ipv4_scope->zone, a->llmnr_ptr_rr);
+                        else if (a->family == AF_INET6 && a->link->llmnr_ipv6_scope)
+                                dns_zone_remove_rr(&a->link->llmnr_ipv6_scope->zone, a->llmnr_ptr_rr);
+                }
+
+                if (a->mdns_address_rr) {
+                        if (a->family == AF_INET && a->link->mdns_ipv4_scope)
+                                dns_zone_remove_rr(&a->link->mdns_ipv4_scope->zone, a->mdns_address_rr);
+                        else if (a->family == AF_INET6 && a->link->mdns_ipv6_scope)
+                                dns_zone_remove_rr(&a->link->mdns_ipv6_scope->zone, a->mdns_address_rr);
+                }
+
+                if (a->mdns_ptr_rr) {
+                        if (a->family == AF_INET && a->link->mdns_ipv4_scope)
+                                dns_zone_remove_rr(&a->link->mdns_ipv4_scope->zone, a->mdns_ptr_rr);
+                        else if (a->family == AF_INET6 && a->link->mdns_ipv6_scope)
+                                dns_zone_remove_rr(&a->link->mdns_ipv6_scope->zone, a->mdns_ptr_rr);
+                }
+        }
+
+        dns_resource_record_unref(a->llmnr_address_rr);
+        dns_resource_record_unref(a->llmnr_ptr_rr);
+        dns_resource_record_unref(a->mdns_address_rr);
+        dns_resource_record_unref(a->mdns_ptr_rr);
+
+        return mfree(a);
+}
+
+void link_address_add_rrs(LinkAddress *a, bool force_remove) {
+        int r;
+
+        assert(a);
+
+        if (a->family == AF_INET) {
+
+                if (!force_remove &&
+                    link_address_relevant(a, true) &&
+                    a->link->llmnr_ipv4_scope &&
+                    a->link->llmnr_support == RESOLVE_SUPPORT_YES &&
+                    a->link->manager->llmnr_support == RESOLVE_SUPPORT_YES) {
+
+                        if (!a->link->manager->llmnr_host_ipv4_key) {
+                                a->link->manager->llmnr_host_ipv4_key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, a->link->manager->llmnr_hostname);
+                                if (!a->link->manager->llmnr_host_ipv4_key) {
+                                        r = -ENOMEM;
+                                        goto fail;
+                                }
+                        }
+
+                        if (!a->llmnr_address_rr) {
+                                a->llmnr_address_rr = dns_resource_record_new(a->link->manager->llmnr_host_ipv4_key);
+                                if (!a->llmnr_address_rr) {
+                                        r = -ENOMEM;
+                                        goto fail;
+                                }
+
+                                a->llmnr_address_rr->a.in_addr = a->in_addr.in;
+                                a->llmnr_address_rr->ttl = LLMNR_DEFAULT_TTL;
+                        }
+
+                        if (!a->llmnr_ptr_rr) {
+                                r = dns_resource_record_new_reverse(&a->llmnr_ptr_rr, a->family, &a->in_addr, a->link->manager->llmnr_hostname);
+                                if (r < 0)
+                                        goto fail;
+
+                                a->llmnr_ptr_rr->ttl = LLMNR_DEFAULT_TTL;
+                        }
+
+                        r = dns_zone_put(&a->link->llmnr_ipv4_scope->zone, a->link->llmnr_ipv4_scope, a->llmnr_address_rr, true);
+                        if (r < 0)
+                                log_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;
+
+        (void) sd_rtnl_message_addr_get_prefixlen(m, &a->prefixlen);
+        (void) sd_rtnl_message_addr_get_scope(m, &a->scope);
+
+        link_allocate_scopes(a->link);
+        link_add_rrs(a->link, false);
+
+        return 0;
+}
+
+bool link_address_relevant(LinkAddress *a, bool local_multicast) {
+        assert(a);
+
+        if (a->flags & (IFA_F_DEPRECATED|IFA_F_TENTATIVE))
+                return false;
+
+        if (a->scope >= (local_multicast ? RT_SCOPE_HOST : RT_SCOPE_LINK))
+                return false;
+
+        return true;
+}
+
+static bool link_needs_save(Link *l) {
+        assert(l);
+
+        /* Returns true if any of the settings where set different from the default */
+
+        if (l->is_managed)
+                return false;
+
+        if (l->llmnr_support != RESOLVE_SUPPORT_YES ||
+            l->mdns_support != RESOLVE_SUPPORT_NO ||
+            l->dnssec_mode != _DNSSEC_MODE_INVALID ||
+            l->dns_over_tls_mode != _DNS_OVER_TLS_MODE_INVALID)
+                return true;
+
+        if (l->dns_servers ||
+            l->search_domains)
+                return true;
+
+        if (!set_isempty(l->dnssec_negative_trust_anchors))
+                return true;
+
+        if (l->default_route >= 0)
+                return true;
+
+        return false;
+}
+
+int link_save_user(Link *l) {
+        _cleanup_free_ char *temp_path = NULL;
+        _cleanup_fclose_ FILE *f = NULL;
+        const char *v;
+        int r;
+
+        assert(l);
+        assert(l->state_file);
+
+        if (!link_needs_save(l)) {
+                (void) unlink(l->state_file);
+                return 0;
+        }
+
+        r = mkdir_parents(l->state_file, 0700);
+        if (r < 0)
+                goto fail;
+
+        r = fopen_temporary(l->state_file, &f, &temp_path);
+        if (r < 0)
+                goto fail;
+
+        (void) fchmod(fileno(f), 0644);
+
+        fputs("# This is private data. Do not parse.\n", f);
+
+        v = resolve_support_to_string(l->llmnr_support);
+        if (v)
+                fprintf(f, "LLMNR=%s\n", v);
+
+        v = resolve_support_to_string(l->mdns_support);
+        if (v)
+                fprintf(f, "MDNS=%s\n", v);
+
+        v = dnssec_mode_to_string(l->dnssec_mode);
+        if (v)
+                fprintf(f, "DNSSEC=%s\n", v);
+
+        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..3c36486
--- /dev/null
+++ b/src/resolve/resolved-link.h
@@ -0,0 +1,114 @@
+/* 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 prefixlen;
+
+        unsigned char flags, scope;
+
+        DnsResourceRecord *llmnr_address_rr;
+        DnsResourceRecord *llmnr_ptr_rr;
+        DnsResourceRecord *mdns_address_rr;
+        DnsResourceRecord *mdns_ptr_rr;
+
+        LIST_FIELDS(LinkAddress, addresses);
+};
+
+struct Link {
+        Manager *manager;
+
+        int ifindex;
+        unsigned flags;
+
+        LIST_HEAD(LinkAddress, addresses);
+        unsigned n_addresses;
+
+        LIST_HEAD(DnsServer, dns_servers);
+        DnsServer *current_dns_server;
+        unsigned n_dns_servers;
+
+        LIST_HEAD(DnsSearchDomain, search_domains);
+        unsigned n_search_domains;
+
+        int default_route;
+
+        ResolveSupport llmnr_support;
+        ResolveSupport mdns_support;
+        DnsOverTlsMode dns_over_tls_mode;
+        DnssecMode dnssec_mode;
+        Set *dnssec_negative_trust_anchors;
+
+        DnsScope *unicast_scope;
+        DnsScope *llmnr_ipv4_scope;
+        DnsScope *llmnr_ipv6_scope;
+        DnsScope *mdns_ipv4_scope;
+        DnsScope *mdns_ipv6_scope;
+
+        bool is_managed;
+
+        char *ifname;
+        uint32_t mtu;
+        uint8_t operstate;
+
+        bool loaded;
+        char *state_file;
+
+        bool unicast_relevant;
+};
+
+int link_new(Manager *m, Link **ret, int ifindex);
+Link *link_free(Link *l);
+int link_process_rtnl(Link *l, sd_netlink_message *m);
+int link_update(Link *l);
+bool link_relevant(Link *l, int family, bool local_multicast);
+LinkAddress* link_find_address(Link *l, int family, const union in_addr_union *in_addr);
+void link_add_rrs(Link *l, bool force_remove);
+
+void link_flush_settings(Link *l);
+void link_set_dnssec_mode(Link *l, DnssecMode mode);
+void link_set_dns_over_tls_mode(Link *l, DnsOverTlsMode mode);
+void link_allocate_scopes(Link *l);
+
+DnsServer* link_set_dns_server(Link *l, DnsServer *s);
+DnsServer* link_get_dns_server(Link *l);
+void link_next_dns_server(Link *l, DnsServer *if_current);
+
+DnssecMode link_get_dnssec_mode(Link *l);
+bool link_dnssec_supported(Link *l);
+
+DnsOverTlsMode link_get_dns_over_tls_mode(Link *l);
+
+int link_save_user(Link *l);
+int link_load_user(Link *l);
+void link_remove_user(Link *l);
+
+int link_address_new(Link *l, LinkAddress **ret, int family, const union in_addr_union *in_addr);
+LinkAddress *link_address_free(LinkAddress *a);
+int link_address_update_rtnl(LinkAddress *a, sd_netlink_message *m);
+bool link_address_relevant(LinkAddress *l, bool local_multicast);
+void link_address_add_rrs(LinkAddress *a, bool force_remove);
+
+bool link_negative_trust_anchor_lookup(Link *l, const char *name);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Link*, link_free);
diff --git a/src/resolve/resolved-llmnr.c b/src/resolve/resolved-llmnr.c
new file mode 100644
index 0000000..ce2db7d
--- /dev/null
+++ b/src/resolve/resolved-llmnr.c
@@ -0,0 +1,476 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <resolv.h>
+
+#include "errno-util.h"
+#include "fd-util.h"
+#include "resolved-llmnr.h"
+#include "resolved-manager.h"
+
+void manager_llmnr_stop(Manager *m) {
+        assert(m);
+
+        m->llmnr_ipv4_udp_event_source = sd_event_source_disable_unref(m->llmnr_ipv4_udp_event_source);
+        m->llmnr_ipv4_udp_fd = safe_close(m->llmnr_ipv4_udp_fd);
+
+        m->llmnr_ipv6_udp_event_source = sd_event_source_disable_unref(m->llmnr_ipv6_udp_event_source);
+        m->llmnr_ipv6_udp_fd = safe_close(m->llmnr_ipv6_udp_fd);
+
+        m->llmnr_ipv4_tcp_event_source = sd_event_source_disable_unref(m->llmnr_ipv4_tcp_event_source);
+        m->llmnr_ipv4_tcp_fd = safe_close(m->llmnr_ipv4_tcp_fd);
+
+        m->llmnr_ipv6_tcp_event_source = sd_event_source_disable_unref(m->llmnr_ipv6_tcp_event_source);
+        m->llmnr_ipv6_tcp_fd = safe_close(m->llmnr_ipv6_tcp_fd);
+}
+
+int manager_llmnr_start(Manager *m) {
+        int r;
+
+        assert(m);
+
+        if (m->llmnr_support == RESOLVE_SUPPORT_NO)
+                return 0;
+
+        r = manager_llmnr_ipv4_udp_fd(m);
+        if (r == -EADDRINUSE)
+                goto eaddrinuse;
+        if (r < 0)
+                return r;
+
+        r = manager_llmnr_ipv4_tcp_fd(m);
+        if (r == -EADDRINUSE)
+                goto eaddrinuse;
+        if (r < 0)
+                return r;
+
+        if (socket_ipv6_is_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_packet_from_local_address(m, p))
+                return 0;
+
+        scope = manager_find_scope(m, p);
+        if (!scope) {
+                log_debug("Got LLMNR UDP packet on unknown scope. Ignoring.");
+                return 0;
+        }
+
+        if (dns_packet_validate_reply(p) > 0) {
+                log_debug("Got LLMNR UDP reply packet for id %u", DNS_PACKET_ID(p));
+
+                dns_scope_check_conflicts(scope, p);
+
+                t = hashmap_get(m->dns_transactions, UINT_TO_PTR(DNS_PACKET_ID(p)));
+                if (t)
+                        dns_transaction_process_reply(t, p, false);
+
+        } else if (dns_packet_validate_query(p) > 0)  {
+                log_debug("Got LLMNR UDP query packet for id %u", DNS_PACKET_ID(p));
+
+                dns_scope_process_query(scope, NULL, p);
+        } else
+                log_debug("Invalid LLMNR UDP packet, ignoring.");
+
+        return 0;
+}
+
+static int set_llmnr_common_socket_options(int fd, int family) {
+        int r;
+
+        r = socket_set_recvpktinfo(fd, family, true);
+        if (r < 0)
+                return r;
+
+        r = socket_set_recvttl(fd, family, true);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+static int set_llmnr_common_udp_socket_options(int fd, int family) {
+        int r;
+
+        /* RFC 4795, section 2.5 recommends setting the TTL of UDP packets to 255. */
+        r = socket_set_ttl(fd, family, 255);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+int manager_llmnr_ipv4_udp_fd(Manager *m) {
+        union sockaddr_union sa = {
+                .in.sin_family = AF_INET,
+                .in.sin_port = htobe16(LLMNR_PORT),
+        };
+        _cleanup_close_ int s = -1;
+        int r;
+
+        assert(m);
+
+        if (m->llmnr_ipv4_udp_fd >= 0)
+                return m->llmnr_ipv4_udp_fd;
+
+        s = socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (s < 0)
+                return log_error_errno(errno, "LLMNR-IPv4(UDP): Failed to create socket: %m");
+
+        r = set_llmnr_common_socket_options(s, AF_INET);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set common socket options: %m");
+
+        r = set_llmnr_common_udp_socket_options(s, AF_INET);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set common UDP socket options: %m");
+
+        r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_TTL, 255);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_MULTICAST_TTL: %m");
+
+        r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_LOOP, true);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_MULTICAST_LOOP: %m");
+
+        /* Disable Don't-Fragment bit in the IP header */
+        r = setsockopt_int(s, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DONT);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set IP_MTU_DISCOVER: %m");
+
+        /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */
+        r = bind(s, &sa.sa, sizeof(sa.in));
+        if (r < 0) {
+                if (errno != EADDRINUSE)
+                        return log_error_errno(errno, "LLMNR-IPv4(UDP): Failed to bind socket: %m");
+
+                log_warning("LLMNR-IPv4(UDP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers.");
+
+                /* try again with SO_REUSEADDR */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set SO_REUSEADDR: %m");
+
+                r = bind(s, &sa.sa, sizeof(sa.in));
+                if (r < 0)
+                        return log_error_errno(errno, "LLMNR-IPv4(UDP): Failed to bind socket: %m");
+        } else {
+                /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to set SO_REUSEADDR: %m");
+        }
+
+        r = sd_event_add_io(m->event, &m->llmnr_ipv4_udp_event_source, s, EPOLLIN, on_llmnr_packet, m);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(UDP): Failed to create event source: %m");
+
+        (void) sd_event_source_set_description(m->llmnr_ipv4_udp_event_source, "llmnr-ipv4-udp");
+
+        return m->llmnr_ipv4_udp_fd = TAKE_FD(s);
+}
+
+int manager_llmnr_ipv6_udp_fd(Manager *m) {
+        union sockaddr_union sa = {
+                .in6.sin6_family = AF_INET6,
+                .in6.sin6_port = htobe16(LLMNR_PORT),
+        };
+        _cleanup_close_ int s = -1;
+        int r;
+
+        assert(m);
+
+        if (m->llmnr_ipv6_udp_fd >= 0)
+                return m->llmnr_ipv6_udp_fd;
+
+        s = socket(AF_INET6, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (s < 0)
+                return log_error_errno(errno, "LLMNR-IPv6(UDP): Failed to create socket: %m");
+
+        r = set_llmnr_common_socket_options(s, AF_INET6);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set common socket options: %m");
+
+        r = set_llmnr_common_udp_socket_options(s, AF_INET6);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set common UDP socket options: %m");
+
+        /* RFC 4795, section 2.5 recommends setting the TTL of UDP packets to 255. */
+        r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, 255);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_MULTICAST_HOPS: %m");
+
+        r = setsockopt_int(s, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, true);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_MULTICAST_LOOP: %m");
+
+        r = setsockopt_int(s, IPPROTO_IPV6, IPV6_V6ONLY, true);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set IPV6_V6ONLY: %m");
+
+        /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */
+        r = bind(s, &sa.sa, sizeof(sa.in6));
+        if (r < 0) {
+                if (errno != EADDRINUSE)
+                        return log_error_errno(errno, "LLMNR-IPv6(UDP): Failed to bind socket: %m");
+
+                log_warning("LLMNR-IPv6(UDP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers.");
+
+                /* try again with SO_REUSEADDR */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set SO_REUSEADDR: %m");
+
+                r = bind(s, &sa.sa, sizeof(sa.in6));
+                if (r < 0)
+                        return log_error_errno(errno, "LLMNR-IPv6(UDP): Failed to bind socket: %m");
+        } else {
+                /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to set SO_REUSEADDR: %m");
+        }
+
+        r = sd_event_add_io(m->event, &m->llmnr_ipv6_udp_event_source, s, EPOLLIN, on_llmnr_packet, m);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(UDP): Failed to create event source: %m");
+
+        (void) sd_event_source_set_description(m->llmnr_ipv6_udp_event_source, "llmnr-ipv6-udp");
+
+        return m->llmnr_ipv6_udp_fd = TAKE_FD(s);
+}
+
+static int on_llmnr_stream_packet(DnsStream *s) {
+        _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;
+}
+
+static int set_llmnr_common_tcp_socket_options(int fd, int family) {
+        int r;
+
+        /* RFC 4795, section 2.5. requires setting the TTL of TCP streams to 1 */
+        r = socket_set_ttl(fd, family, 1);
+        if (r < 0)
+                return r;
+
+        r = setsockopt_int(fd, IPPROTO_TCP, TCP_FASTOPEN, 5); /* Everybody appears to pick qlen=5, let's do the same here. */
+        if (r < 0)
+                log_debug_errno(r, "Failed to enable TCP_FASTOPEN on TCP listening socket, ignoring: %m");
+
+        r = setsockopt_int(fd, IPPROTO_TCP, TCP_NODELAY, true);
+        if (r < 0)
+                log_debug_errno(r, "Failed to enable TCP_NODELAY mode, ignoring: %m");
+
+        return 0;
+}
+
+int manager_llmnr_ipv4_tcp_fd(Manager *m) {
+        union sockaddr_union sa = {
+                .in.sin_family = AF_INET,
+                .in.sin_port = htobe16(LLMNR_PORT),
+        };
+        _cleanup_close_ int s = -1;
+        int r;
+
+        assert(m);
+
+        if (m->llmnr_ipv4_tcp_fd >= 0)
+                return m->llmnr_ipv4_tcp_fd;
+
+        s = socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (s < 0)
+                return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to create socket: %m");
+
+        r = set_llmnr_common_socket_options(s, AF_INET);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set common socket options: %m");
+
+        r = set_llmnr_common_tcp_socket_options(s, AF_INET);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set common TCP socket options: %m");
+
+        /* Disable Don't-Fragment bit in the IP header */
+        r = setsockopt_int(s, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DONT);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set IP_MTU_DISCOVER: %m");
+
+        /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */
+        r = bind(s, &sa.sa, sizeof(sa.in));
+        if (r < 0) {
+                if (errno != EADDRINUSE)
+                        return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to bind socket: %m");
+
+                log_warning("LLMNR-IPv4(TCP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers.");
+
+                /* try again with SO_REUSEADDR */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set SO_REUSEADDR: %m");
+
+                r = bind(s, &sa.sa, sizeof(sa.in));
+                if (r < 0)
+                        return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to bind socket: %m");
+        } else {
+                /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to set SO_REUSEADDR: %m");
+        }
+
+        r = listen(s, SOMAXCONN);
+        if (r < 0)
+                return log_error_errno(errno, "LLMNR-IPv4(TCP): Failed to listen the stream: %m");
+
+        r = sd_event_add_io(m->event, &m->llmnr_ipv4_tcp_event_source, s, EPOLLIN, on_llmnr_stream, m);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv4(TCP): Failed to create event source: %m");
+
+        (void) sd_event_source_set_description(m->llmnr_ipv4_tcp_event_source, "llmnr-ipv4-tcp");
+
+        return m->llmnr_ipv4_tcp_fd = TAKE_FD(s);
+}
+
+int manager_llmnr_ipv6_tcp_fd(Manager *m) {
+        union sockaddr_union sa = {
+                .in6.sin6_family = AF_INET6,
+                .in6.sin6_port = htobe16(LLMNR_PORT),
+        };
+        _cleanup_close_ int s = -1;
+        int r;
+
+        assert(m);
+
+        if (m->llmnr_ipv6_tcp_fd >= 0)
+                return m->llmnr_ipv6_tcp_fd;
+
+        s = socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (s < 0)
+                return log_error_errno(errno, "LLMNR-IPv6(TCP): Failed to create socket: %m");
+
+        r = setsockopt_int(s, IPPROTO_IPV6, IPV6_V6ONLY, true);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set IPV6_V6ONLY: %m");
+
+        r = set_llmnr_common_socket_options(s, AF_INET6);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set common socket options: %m");
+
+        r = set_llmnr_common_tcp_socket_options(s, AF_INET6);
+        if (r < 0)
+                return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set common TCP socket options: %m");
+
+        /* first try to bind without SO_REUSEADDR to detect another LLMNR responder */
+        r = bind(s, &sa.sa, sizeof(sa.in6));
+        if (r < 0) {
+                if (errno != EADDRINUSE)
+                        return log_error_errno(errno, "LLMNR-IPv6(TCP): Failed to bind socket: %m");
+
+                log_warning("LLMNR-IPv6(TCP): There appears to be another LLMNR responder running, or previously systemd-resolved crashed with some outstanding transfers.");
+
+                /* try again with SO_REUSEADDR */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set SO_REUSEADDR: %m");
+
+                r = bind(s, &sa.sa, sizeof(sa.in6));
+                if (r < 0)
+                        return log_error_errno(errno, "LLMNR-IPv6(TCP): Failed to bind socket: %m");
+        } else {
+                /* enable SO_REUSEADDR for the case that the user really wants multiple LLMNR responders */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "LLMNR-IPv6(TCP): Failed to set SO_REUSEADDR: %m");
+        }
+
+        r = listen(s, SOMAXCONN);
+        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..246e2d9
--- /dev/null
+++ b/src/resolve/resolved-manager.c
@@ -0,0 +1,1694 @@
+/* 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 "missing_socket.h"
+#include "netlink-util.h"
+#include "ordered-set.h"
+#include "parse-util.h"
+#include "random-util.h"
+#include "resolved-bus.h"
+#include "resolved-conf.h"
+#include "resolved-dns-stub.h"
+#include "resolved-dnssd.h"
+#include "resolved-etc-hosts.h"
+#include "resolved-llmnr.h"
+#include "resolved-manager.h"
+#include "resolved-mdns.h"
+#include "resolved-resolv-conf.h"
+#include "resolved-util.h"
+#include "resolved-varlink.h"
+#include "socket-util.h"
+#include "string-table.h"
+#include "string-util.h"
+#include "utf8.h"
+
+#define SEND_TIMEOUT_USEC (200 * USEC_PER_MSEC)
+
+static int manager_process_link(sd_netlink *rtnl, sd_netlink_message *mm, void *userdata) {
+        Manager *m = 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 manager_clock_change_listen(Manager *m);
+
+static int on_clock_change(sd_event_source *source, int fd, uint32_t revents, void *userdata) {
+        Manager *m = userdata;
+
+        assert(m);
+
+        /* The clock has changed, let's flush all caches. Why that? That's because DNSSEC validation takes
+         * the system clock into consideration, and if the clock changes the old validations might have been
+         * wrong. Let's redo all validation with the new, correct time.
+         *
+         * (Also, this is triggered after system suspend, which is also a good reason to drop caches, since
+         * we might be connected to a different network now without this being visible in a dropped link
+         * carrier or so.) */
+
+        log_info("Clock change detected. Flushing caches.");
+        manager_flush_caches(m, LOG_DEBUG /* downgrade the functions own log message, since we already logged here at LOG_INFO level */);
+
+        /* The clock change timerfd is unusable after it triggered once, create a new one. */
+        return manager_clock_change_listen(m);
+}
+
+static int manager_clock_change_listen(Manager *m) {
+        _cleanup_close_ int fd = -1;
+        int r;
+
+        assert(m);
+
+        m->clock_change_event_source = sd_event_source_disable_unref(m->clock_change_event_source);
+
+        fd = time_change_fd();
+        if (fd < 0)
+                return log_error_errno(fd, "Failed to allocate clock change timer fd: %m");
+
+        r = sd_event_add_io(m->event, &m->clock_change_event_source, fd, EPOLLIN, on_clock_change, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to create clock change event source: %m");
+
+        r = sd_event_source_set_io_fd_own(m->clock_change_event_source, true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to pass ownership of clock fd to event source: %m");
+        TAKE_FD(fd);
+
+        (void) sd_event_source_set_description(m->clock_change_event_source, "clock-change");
+
+        return 0;
+}
+
+static int determine_hostnames(char **full_hostname, char **llmnr_hostname, char **mdns_hostname) {
+        _cleanup_free_ char *h = NULL, *n = NULL;
+        int r;
+
+        assert(full_hostname);
+        assert(llmnr_hostname);
+        assert(mdns_hostname);
+
+        r = resolve_system_hostname(&h, &n);
+        if (r < 0)
+                return r;
+
+        r = dns_name_concat(n, "local", 0, mdns_hostname);
+        if (r < 0)
+                return log_error_errno(r, "Failed to determine mDNS hostname: %m");
+
+        *llmnr_hostname = TAKE_PTR(n);
+        *full_hostname = TAKE_PTR(h);
+
+        return 0;
+}
+
+static char* fallback_hostname(void) {
+
+        /* Determine the fall back hostname. For exposing this system to the outside world, we cannot have it
+         * to be "localhost" even if that's the default hostname. In this case, let's revert to "linux"
+         * instead. */
+
+        _cleanup_free_ char *n = get_default_hostname();
+        if (!n)
+                return NULL;
+
+        if (is_localhost(n))
+                return strdup("linux");
+
+        return TAKE_PTR(n);
+}
+
+static int make_fallback_hostnames(char **full_hostname, char **llmnr_hostname, char **mdns_hostname) {
+        _cleanup_free_ char *h = NULL, *n = NULL, *m = NULL;
+        char label[DNS_LABEL_MAX];
+        const char *p;
+        int r;
+
+        assert(full_hostname);
+        assert(llmnr_hostname);
+        assert(mdns_hostname);
+
+        p = h = fallback_hostname();
+        if (!h)
+                return log_oom();
+
+        r = dns_label_unescape(&p, label, sizeof label, 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to unescape fallback hostname: %m");
+
+        assert(r > 0); /* The fallback hostname must have at least one label */
+
+        r = dns_label_escape_new(label, r, &n);
+        if (r < 0)
+                return log_error_errno(r, "Failed to escape fallback hostname: %m");
+
+        r = dns_name_concat(n, "local", 0, &m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to concatenate mDNS hostname: %m");
+
+        *llmnr_hostname = TAKE_PTR(n);
+        *mdns_hostname = TAKE_PTR(m);
+        *full_hostname = TAKE_PTR(h);
+
+        return 0;
+}
+
+static int on_hostname_change(sd_event_source *es, int fd, uint32_t revents, void *userdata) {
+        _cleanup_free_ char *full_hostname = NULL, *llmnr_hostname = NULL, *mdns_hostname = NULL;
+        Manager *m = userdata;
+        bool llmnr_hostname_changed;
+        int r;
+
+        assert(m);
+
+        r = determine_hostnames(&full_hostname, &llmnr_hostname, &mdns_hostname);
+        if (r < 0) {
+                log_warning_errno(r, "Failed to determine the local hostname and LLMNR/mDNS names, ignoring: %m");
+                return 0; /* ignore invalid hostnames */
+        }
+
+        llmnr_hostname_changed = !streq(llmnr_hostname, m->llmnr_hostname);
+        if (streq(full_hostname, m->full_hostname) &&
+            !llmnr_hostname_changed &&
+            streq(mdns_hostname, m->mdns_hostname))
+                return 0;
+
+        log_info("System hostname changed to '%s'.", full_hostname);
+
+        free_and_replace(m->full_hostname, full_hostname);
+        free_and_replace(m->llmnr_hostname, llmnr_hostname);
+        free_and_replace(m->mdns_hostname, mdns_hostname);
+
+        manager_refresh_rrs(m);
+        (void) manager_send_changed(m, "LLMNRHostname");
+
+        return 0;
+}
+
+static int manager_watch_hostname(Manager *m) {
+        int r;
+
+        assert(m);
+
+        m->hostname_fd = open("/proc/sys/kernel/hostname",
+                              O_RDONLY|O_CLOEXEC|O_NONBLOCK|O_NOCTTY);
+        if (m->hostname_fd < 0) {
+                log_warning_errno(errno, "Failed to watch hostname: %m");
+                return 0;
+        }
+
+        r = sd_event_add_io(m->event, &m->hostname_event_source, m->hostname_fd, 0, on_hostname_change, m);
+        if (r < 0) {
+                if (r == -EPERM)
+                        /* kernels prior to 3.2 don't support polling this file. Ignore the failure. */
+                        m->hostname_fd = safe_close(m->hostname_fd);
+                else
+                        return log_error_errno(r, "Failed to add hostname event source: %m");
+        }
+
+        (void) sd_event_source_set_description(m->hostname_event_source, "hostname");
+
+        r = determine_hostnames(&m->full_hostname, &m->llmnr_hostname, &m->mdns_hostname);
+        if (r < 0) {
+                _cleanup_free_ char *d = NULL;
+
+                d = fallback_hostname();
+                if (!d)
+                        return log_oom();
+
+                log_info("Defaulting to hostname '%s'.", d);
+
+                r = make_fallback_hostnames(&m->full_hostname, &m->llmnr_hostname, &m->mdns_hostname);
+                if (r < 0)
+                        return r;
+        } else
+                log_info("Using system hostname '%s'.", m->full_hostname);
+
+        return 0;
+}
+
+static int manager_sigusr1(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) {
+        _cleanup_free_ char *buffer = NULL;
+        _cleanup_fclose_ FILE *f = NULL;
+        Manager *m = 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, LOG_INFO);
+
+        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,
+                .read_etc_hosts = true,
+        };
+
+        r = dns_trust_anchor_load(&m->trust_anchor);
+        if (r < 0)
+                return r;
+
+        r = manager_parse_config_file(m);
+        if (r < 0)
+                log_warning_errno(r, "Failed to parse configuration file: %m");
+
+#if ENABLE_DNS_OVER_TLS
+        r = dnstls_manager_init(m);
+        if (r < 0)
+                return r;
+#endif
+
+        r = sd_event_default(&m->event);
+        if (r < 0)
+                return r;
+
+        (void) sd_event_add_signal(m->event, NULL, SIGTERM, NULL,  NULL);
+        (void) sd_event_add_signal(m->event, NULL, SIGINT, NULL, NULL);
+
+        (void) sd_event_set_watchdog(m->event, true);
+
+        r = manager_watch_hostname(m);
+        if (r < 0)
+                return r;
+
+        r = dnssd_load(m);
+        if (r < 0)
+                log_warning_errno(r, "Failed to load DNS-SD configuration files: %m");
+
+        r = dns_scope_new(m, &m->unicast_scope, NULL, DNS_PROTOCOL_DNS, AF_UNSPEC);
+        if (r < 0)
+                return r;
+
+        r = manager_network_monitor_listen(m);
+        if (r < 0)
+                return r;
+
+        r = manager_rtnl_listen(m);
+        if (r < 0)
+                return r;
+
+        r = manager_clock_change_listen(m);
+        if (r < 0)
+                return r;
+
+        r = manager_connect_bus(m);
+        if (r < 0)
+                return r;
+
+        (void) sd_event_add_signal(m->event, &m->sigusr1_event_source, SIGUSR1, manager_sigusr1, m);
+        (void) sd_event_add_signal(m->event, &m->sigusr2_event_source, SIGUSR2, manager_sigusr2, m);
+        (void) sd_event_add_signal(m->event, &m->sigrtmin1_event_source, SIGRTMIN+1, manager_sigrtmin1, m);
+
+        manager_cleanup_saved_user(m);
+
+        *ret = TAKE_PTR(m);
+
+        return 0;
+}
+
+int manager_start(Manager *m) {
+        int r;
+
+        assert(m);
+
+        r = manager_dns_stub_start(m);
+        if (r < 0)
+                return r;
+
+        r = manager_varlink_init(m);
+        if (r < 0)
+                return r;
+
+        return 0;
+}
+
+Manager *manager_free(Manager *m) {
+        Link *l;
+        DnssdService *s;
+
+        if (!m)
+                return NULL;
+
+        dns_server_unlink_all(m->dns_servers);
+        dns_server_unlink_all(m->fallback_dns_servers);
+        dns_search_domain_unlink_all(m->search_domains);
+
+        while ((l = hashmap_first(m->links)))
+               link_free(l);
+
+        while (m->dns_queries)
+                dns_query_free(m->dns_queries);
+
+        m->stub_queries_by_packet = hashmap_free(m->stub_queries_by_packet);
+
+        dns_scope_free(m->unicast_scope);
+
+        /* At this point only orphaned streams should remain. All others should have been freed already by their
+         * owners */
+        while (m->dns_streams)
+                dns_stream_unref(m->dns_streams);
+
+#if ENABLE_DNS_OVER_TLS
+        dnstls_manager_free(m);
+#endif
+
+        hashmap_free(m->links);
+        hashmap_free(m->dns_transactions);
+
+        sd_event_source_unref(m->network_event_source);
+        sd_network_monitor_unref(m->network_monitor);
+
+        sd_netlink_unref(m->rtnl);
+        sd_event_source_unref(m->rtnl_event_source);
+        sd_event_source_unref(m->clock_change_event_source);
+
+        manager_llmnr_stop(m);
+        manager_mdns_stop(m);
+        manager_dns_stub_stop(m);
+        manager_varlink_done(m);
+
+        manager_socket_graveyard_clear(m);
+
+        ordered_set_free(m->dns_extra_stub_listeners);
+
+        bus_verify_polkit_async_registry_free(m->polkit_registry);
+
+        sd_bus_flush_close_unref(m->bus);
+
+        sd_event_source_unref(m->sigusr1_event_source);
+        sd_event_source_unref(m->sigusr2_event_source);
+        sd_event_source_unref(m->sigrtmin1_event_source);
+
+        dns_resource_key_unref(m->llmnr_host_ipv4_key);
+        dns_resource_key_unref(m->llmnr_host_ipv6_key);
+        dns_resource_key_unref(m->mdns_host_ipv4_key);
+        dns_resource_key_unref(m->mdns_host_ipv6_key);
+
+        sd_event_source_unref(m->hostname_event_source);
+        safe_close(m->hostname_fd);
+
+        sd_event_unref(m->event);
+
+        free(m->full_hostname);
+        free(m->llmnr_hostname);
+        free(m->mdns_hostname);
+
+        while ((s = hashmap_first(m->dnssd_services)))
+               dnssd_service_free(s);
+        hashmap_free(m->dnssd_services);
+
+        dns_trust_anchor_flush(&m->trust_anchor);
+        manager_etc_hosts_flush(m);
+
+        return mfree(m);
+}
+
+int manager_recv(Manager *m, int fd, DnsProtocol protocol, DnsPacket **ret) {
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        CMSG_BUFFER_TYPE(CMSG_SPACE(MAXSIZE(struct in_pktinfo, struct in6_pktinfo))
+                         + CMSG_SPACE(int) /* ttl/hoplimit */
+                         + EXTRA_CMSG_SPACE /* kernel appears to require extra buffer space */) control;
+        union sockaddr_union sa;
+        struct iovec iov;
+        struct msghdr mh = {
+                .msg_name = &sa.sa,
+                .msg_namelen = sizeof(sa),
+                .msg_iov = &iov,
+                .msg_iovlen = 1,
+                .msg_control = &control,
+                .msg_controllen = sizeof(control),
+        };
+        struct cmsghdr *cmsg;
+        ssize_t ms, l;
+        int r;
+
+        assert(m);
+        assert(fd >= 0);
+        assert(ret);
+
+        ms = next_datagram_size_fd(fd);
+        if (ms < 0)
+                return ms;
+
+        r = dns_packet_new(&p, protocol, ms, DNS_PACKET_SIZE_MAX);
+        if (r < 0)
+                return r;
+
+        iov = IOVEC_MAKE(DNS_PACKET_DATA(p), p->allocated);
+
+        l = recvmsg_safe(fd, &mh, 0);
+        if (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;
+
+        p->timestamp = now(clock_boottime_or_monotonic());
+
+        CMSG_FOREACH(cmsg, &mh) {
+
+                if (cmsg->cmsg_level == IPPROTO_IPV6) {
+                        assert(p->family == AF_INET6);
+
+                        switch (cmsg->cmsg_type) {
+
+                        case IPV6_PKTINFO: {
+                                struct in6_pktinfo *i = (struct in6_pktinfo*) CMSG_DATA(cmsg);
+
+                                if (p->ifindex <= 0)
+                                        p->ifindex = i->ipi6_ifindex;
+
+                                p->destination.in6 = i->ipi6_addr;
+                                break;
+                        }
+
+                        case IPV6_HOPLIMIT:
+                                p->ttl = *(int *) CMSG_DATA(cmsg);
+                                break;
+
+                        case IPV6_RECVFRAGSIZE:
+                                p->fragsize = *(int *) CMSG_DATA(cmsg);
+                                break;
+                        }
+                } else if (cmsg->cmsg_level == IPPROTO_IP) {
+                        assert(p->family == AF_INET);
+
+                        switch (cmsg->cmsg_type) {
+
+                        case IP_PKTINFO: {
+                                struct in_pktinfo *i = (struct in_pktinfo*) CMSG_DATA(cmsg);
+
+                                if (p->ifindex <= 0)
+                                        p->ifindex = i->ipi_ifindex;
+
+                                p->destination.in = i->ipi_addr;
+                                break;
+                        }
+
+                        case IP_TTL:
+                                p->ttl = *(int *) CMSG_DATA(cmsg);
+                                break;
+
+                        case IP_RECVFRAGSIZE:
+                                p->fragsize = *(int *) CMSG_DATA(cmsg);
+                                break;
+                        }
+                }
+        }
+
+        /* The Linux kernel sets the interface index to the loopback
+         * device if the packet came from the local host since it
+         * avoids the routing table in such a case. Let's unset the
+         * interface index in such a case. */
+        if (p->ifindex == LOOPBACK_IFINDEX)
+                p->ifindex = 0;
+
+        if (protocol != DNS_PROTOCOL_DNS) {
+                /* If we don't know the interface index still, we look for the
+                 * first local interface with a matching address. Yuck! */
+                if (p->ifindex <= 0)
+                        p->ifindex = manager_find_ifindex(m, p->family, &p->destination);
+        }
+
+        log_debug("Received %s UDP packet of size %zu, ifindex=%i, ttl=%i, fragsize=%zu",
+                  dns_protocol_to_string(protocol), p->size, p->ifindex, p->ttl, p->fragsize);
+
+        *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 that have an IP address associated */
+
+        HASHMAP_FOREACH(l, m->links) {
+                /* Let's filter out links without IP addresses (e.g. AF_CAN links and suchlike) */
+                if (!l->addresses)
+                        continue;
+
+                /* Safety check: MTU shorter than what we need for the absolutely shortest DNS request? Then
+                 * let's ignore this link. */
+                if (l->mtu < MIN(UDP4_PACKET_HEADER_SIZE + DNS_PACKET_HEADER_SIZE,
+                                 UDP6_PACKET_HEADER_SIZE + DNS_PACKET_HEADER_SIZE))
+                        continue;
+
+                if (mtu <= 0 || l->mtu < mtu)
+                        mtu = l->mtu;
+        }
+
+        if (mtu == 0) /* found nothing? then let's assume the typical Ethernet MTU for lack of anything more precise */
+                return 1500;
+
+        return mtu;
+}
+
+int manager_find_ifindex(Manager *m, int family, const union in_addr_union *in_addr) {
+        LinkAddress *a;
+
+        assert(m);
+
+        if (!IN_SET(family, AF_INET, AF_INET6))
+                return 0;
+
+        if (!in_addr)
+                return 0;
+
+        a = manager_find_link_address(m, family, in_addr);
+        if (a)
+                return a->link->ifindex;
+
+        return 0;
+}
+
+void manager_refresh_rrs(Manager *m) {
+        Link *l;
+        DnssdService *s;
+
+        assert(m);
+
+        m->llmnr_host_ipv4_key = dns_resource_key_unref(m->llmnr_host_ipv4_key);
+        m->llmnr_host_ipv6_key = dns_resource_key_unref(m->llmnr_host_ipv6_key);
+        m->mdns_host_ipv4_key = dns_resource_key_unref(m->mdns_host_ipv4_key);
+        m->mdns_host_ipv6_key = dns_resource_key_unref(m->mdns_host_ipv6_key);
+
+        HASHMAP_FOREACH(l, m->links)
+                link_add_rrs(l, true);
+
+        if (m->mdns_support == RESOLVE_SUPPORT_YES)
+                HASHMAP_FOREACH(s, m->dnssd_services)
+                        if (dnssd_update_rrs(s) < 0)
+                                log_warning("Failed to refresh DNS-SD service '%s'", s->name);
+
+        HASHMAP_FOREACH(l, m->links)
+                link_add_rrs(l, false);
+}
+
+static int manager_next_random_name(const char *old, char **ret_new) {
+        const char *p;
+        uint64_t u, a;
+        char *n;
+
+        p = strchr(old, 0);
+        assert(p);
+
+        while (p > old) {
+                if (!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);
+
+        if (!IN_SET(family, AF_INET, AF_INET6))
+                return NULL;
+
+        if (!in_addr)
+                return NULL;
+
+        HASHMAP_FOREACH(l, m->links) {
+                LinkAddress *a;
+
+                a = link_find_address(l, family, in_addr);
+                if (a)
+                        return a;
+        }
+
+        return NULL;
+}
+
+bool manager_packet_from_local_address(Manager *m, DnsPacket *p) {
+        assert(m);
+        assert(p);
+
+        /* Let's see if this packet comes from an IP address we have on any local interface */
+
+        return !!manager_find_link_address(m, p->family, &p->sender);
+}
+
+bool manager_packet_from_our_transaction(Manager *m, DnsPacket *p) {
+        DnsTransaction *t;
+
+        assert(m);
+        assert(p);
+
+        /* Let's see if we have a transaction with a query message with the exact same binary contents as the
+         * one we just got. If so, it's almost definitely a packet loop of some kind. */
+
+        t = hashmap_get(m->dns_transactions, UINT_TO_PTR(DNS_PACKET_ID(p)));
+        if (!t)
+                return false;
+
+        return t->sent && dns_packet_equal(t->sent, p);
+}
+
+DnsScope* manager_find_scope(Manager *m, DnsPacket *p) {
+        Link *l;
+
+        assert(m);
+        assert(p);
+
+        l = hashmap_get(m->links, INT_TO_PTR(p->ifindex));
+        if (!l)
+                return NULL;
+
+        switch (p->protocol) {
+        case DNS_PROTOCOL_LLMNR:
+                if (p->family == AF_INET)
+                        return l->llmnr_ipv4_scope;
+                else if (p->family == AF_INET6)
+                        return l->llmnr_ipv6_scope;
+
+                break;
+
+        case DNS_PROTOCOL_MDNS:
+                if (p->family == AF_INET)
+                        return l->mdns_ipv4_scope;
+                else if (p->family == AF_INET6)
+                        return l->mdns_ipv6_scope;
+
+                break;
+
+        default:
+                break;
+        }
+
+        return NULL;
+}
+
+void manager_verify_all(Manager *m) {
+        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, int log_level) {
+        DnsScope *scope;
+
+        assert(m);
+
+        LIST_FOREACH(scopes, scope, m->dns_scopes)
+                dns_cache_flush(&scope->cache);
+
+        log_full(log_level, "Flushed all caches.");
+}
+
+void manager_reset_server_features(Manager *m) {
+        Link *l;
+
+        dns_server_reset_features_all(m->dns_servers);
+        dns_server_reset_features_all(m->fallback_dns_servers);
+
+        HASHMAP_FOREACH(l, m->links)
+                dns_server_reset_features_all(l->dns_servers);
+
+        log_info("Resetting learnt feature levels on all servers.");
+}
+
+void manager_cleanup_saved_user(Manager *m) {
+        _cleanup_closedir_ DIR *d = NULL;
+        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;
+}
+
+bool manager_server_is_stub(Manager *m, DnsServer *s) {
+        DnsStubListenerExtra *l;
+
+        assert(m);
+        assert(s);
+
+        /* Safety check: we generally already skip the main stub when parsing configuration. But let's be
+         * extra careful, and check here again */
+        if (s->family == AF_INET &&
+            s->address.in.s_addr == htobe32(INADDR_DNS_STUB) &&
+            dns_server_port(s) == 53)
+                return true;
+
+        /* Main reason to call this is to check server data against the extra listeners, and filter things
+         * out. */
+        ORDERED_SET_FOREACH(l, m->dns_extra_stub_listeners)
+                if (s->family == l->family &&
+                    in_addr_equal(s->family, &s->address, &l->address) &&
+                    dns_server_port(s) == dns_stub_listener_extra_port(l))
+                        return true;
+
+        return false;
+}
+
+int socket_disable_pmtud(int fd, int af) {
+        int r;
+
+        assert(fd >= 0);
+
+        if (af == AF_UNSPEC) {
+                r = socket_get_family(fd, &af);
+                if (r < 0)
+                        return r;
+        }
+
+        switch (af) {
+
+        case AF_INET: {
+                /* Turn off path MTU discovery, let's rather fragment on the way than to open us up against
+                 * PMTU forgery vulnerabilities.
+                 *
+                 * There appears to be no documentation about IP_PMTUDISC_OMIT, but it has the effect that
+                 * the "Don't Fragment" bit in the IPv4 header is turned off, thus enforcing fragmentation if
+                 * our datagram size exceeds the MTU of a router in the path, and turning off path MTU
+                 * discovery.
+                 *
+                 * This helps mitigating the PMTUD vulnerability described here:
+                 *
+                 * https://blog.apnic.net/2019/07/12/its-time-to-consider-avoiding-ip-fragmentation-in-the-dns/
+                 *
+                 * Similar logic is in place in most DNS servers.
+                 *
+                 * There are multiple conflicting goals: we want to allow the largest datagrams possible (for
+                 * efficiency reasons), but not have fragmentation (for security reasons), nor use PMTUD (for
+                 * security reasons, too). Our strategy to deal with this is: use large packets, turn off
+                 * PMTUD, but watch fragmentation taking place, and then size our packets to the max of the
+                 * fragments seen — and if we need larger packets always go to TCP.
+                 */
+
+                r = setsockopt_int(fd, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_OMIT);
+                if (r < 0)
+                        return r;
+
+                return 0;
+        }
+
+        case AF_INET6: {
+                /* On IPv6 fragmentation only is done by the sender — never by routers on the path. PMTUD is
+                 * mandatory. If we want to turn off PMTUD, the only way is by sending with minimal MTU only,
+                 * so that we apply maximum fragmentation locally already, and thus PMTUD doesn't happen
+                 * because there's nothing that could be fragmented further anymore. */
+
+                r = setsockopt_int(fd, IPPROTO_IPV6, IPV6_MTU, IPV6_MIN_MTU);
+                if (r < 0)
+                        return r;
+
+                return 0;
+        }
+
+        default:
+                return -EAFNOSUPPORT;
+        }
+}
diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h
new file mode 100644
index 0000000..1371c41
--- /dev/null
+++ b/src/resolve/resolved-manager.h
@@ -0,0 +1,208 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include <sys/stat.h>
+
+#include "sd-event.h"
+#include "sd-netlink.h"
+#include "sd-network.h"
+
+#include "hashmap.h"
+#include "list.h"
+#include "ordered-set.h"
+#include "resolve-util.h"
+#include "varlink.h"
+
+typedef struct Manager Manager;
+
+#include "resolved-dns-query.h"
+#include "resolved-dns-search-domain.h"
+#include "resolved-dns-stream.h"
+#include "resolved-dns-stub.h"
+#include "resolved-dns-trust-anchor.h"
+#include "resolved-link.h"
+#include "resolved-socket-graveyard.h"
+
+#define MANAGER_SEARCH_DOMAINS_MAX 256
+#define MANAGER_DNS_SERVERS_MAX 256
+
+typedef struct EtcHosts {
+        Hashmap *by_address;
+        Hashmap *by_name;
+        Set *no_address;
+} EtcHosts;
+
+struct Manager {
+        sd_event *event;
+
+        ResolveSupport llmnr_support;
+        ResolveSupport mdns_support;
+        DnssecMode dnssec_mode;
+        DnsOverTlsMode dns_over_tls_mode;
+        DnsCacheMode enable_cache;
+        bool cache_from_localhost;
+        DnsStubListenerMode dns_stub_listener_mode;
+
+#if ENABLE_DNS_OVER_TLS
+        DnsTlsManagerData dnstls_data;
+#endif
+
+        /* Network */
+        Hashmap *links;
+
+        sd_netlink *rtnl;
+        sd_event_source *rtnl_event_source;
+
+        sd_network_monitor *network_monitor;
+        sd_event_source *network_event_source;
+
+        /* DNS query management */
+        Hashmap *dns_transactions;
+        LIST_HEAD(DnsQuery, dns_queries);
+        unsigned n_dns_queries;
+        Hashmap *stub_queries_by_packet;
+
+        LIST_HEAD(DnsStream, dns_streams);
+        unsigned n_dns_streams[_DNS_STREAM_TYPE_MAX];
+
+        /* Unicast dns */
+        LIST_HEAD(DnsServer, dns_servers);
+        LIST_HEAD(DnsServer, fallback_dns_servers);
+        unsigned n_dns_servers; /* counts both main and fallback */
+        DnsServer *current_dns_server;
+
+        LIST_HEAD(DnsSearchDomain, search_domains);
+        unsigned n_search_domains;
+
+        bool need_builtin_fallbacks;
+        bool read_resolv_conf;
+        bool resolve_unicast_single_label;
+
+        struct stat resolv_conf_stat;
+
+        DnsTrustAnchor trust_anchor;
+
+        LIST_HEAD(DnsScope, dns_scopes);
+        DnsScope *unicast_scope;
+
+        /* LLMNR */
+        int llmnr_ipv4_udp_fd;
+        int llmnr_ipv6_udp_fd;
+        int llmnr_ipv4_tcp_fd;
+        int llmnr_ipv6_tcp_fd;
+
+        sd_event_source *llmnr_ipv4_udp_event_source;
+        sd_event_source *llmnr_ipv6_udp_event_source;
+        sd_event_source *llmnr_ipv4_tcp_event_source;
+        sd_event_source *llmnr_ipv6_tcp_event_source;
+
+        /* mDNS */
+        int mdns_ipv4_fd;
+        int mdns_ipv6_fd;
+        sd_event_source *mdns_ipv4_event_source;
+        sd_event_source *mdns_ipv6_event_source;
+
+        /* DNS-SD */
+        Hashmap *dnssd_services;
+
+        /* dbus */
+        sd_bus *bus;
+
+        /* The hostname we publish on LLMNR and mDNS */
+        char *full_hostname;
+        char *llmnr_hostname;
+        char *mdns_hostname;
+        DnsResourceKey *llmnr_host_ipv4_key;
+        DnsResourceKey *llmnr_host_ipv6_key;
+        DnsResourceKey *mdns_host_ipv4_key;
+        DnsResourceKey *mdns_host_ipv6_key;
+
+        /* Watch the system hostname */
+        int hostname_fd;
+        sd_event_source *hostname_event_source;
+
+        sd_event_source *sigusr1_event_source;
+        sd_event_source *sigusr2_event_source;
+        sd_event_source *sigrtmin1_event_source;
+
+        unsigned n_transactions_total;
+        unsigned n_dnssec_verdict[_DNSSEC_VERDICT_MAX];
+
+        /* Data from /etc/hosts */
+        EtcHosts etc_hosts;
+        usec_t etc_hosts_last;
+        struct stat etc_hosts_stat;
+        bool read_etc_hosts;
+
+        OrderedSet *dns_extra_stub_listeners;
+
+        /* Local DNS stub on 127.0.0.53:53 */
+        sd_event_source *dns_stub_udp_event_source;
+        sd_event_source *dns_stub_tcp_event_source;
+
+        Hashmap *polkit_registry;
+
+        VarlinkServer *varlink_server;
+
+        sd_event_source *clock_change_event_source;
+
+        LIST_HEAD(SocketGraveyard, socket_graveyard);
+        SocketGraveyard *socket_graveyard_oldest;
+        size_t n_socket_graveyard;
+};
+
+/* Manager */
+
+int manager_new(Manager **ret);
+Manager* manager_free(Manager *m);
+
+int manager_start(Manager *m);
+
+uint32_t manager_find_mtu(Manager *m);
+
+int manager_write(Manager *m, int fd, DnsPacket *p);
+int manager_send(Manager *m, int fd, int ifindex, int family, const union in_addr_union *destination, uint16_t port, const union in_addr_union *source, DnsPacket *p);
+int manager_recv(Manager *m, int fd, DnsProtocol protocol, DnsPacket **ret);
+
+int manager_find_ifindex(Manager *m, int family, const union in_addr_union *in_addr);
+LinkAddress* manager_find_link_address(Manager *m, int family, const union in_addr_union *in_addr);
+
+void manager_refresh_rrs(Manager *m);
+int manager_next_hostname(Manager *m);
+
+bool manager_packet_from_local_address(Manager *m, DnsPacket *p);
+bool manager_packet_from_our_transaction(Manager *m, DnsPacket *p);
+
+DnsScope* manager_find_scope(Manager *m, DnsPacket *p);
+
+void manager_verify_all(Manager *m);
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(Manager*, manager_free);
+
+/* For some reason we need some extra cmsg space on some kernels/archs. One of those days we need to figure out why */
+#define EXTRA_CMSG_SPACE 1024
+
+int manager_is_own_hostname(Manager *m, const char *name);
+
+int manager_compile_dns_servers(Manager *m, OrderedSet **servers);
+int manager_compile_search_domains(Manager *m, OrderedSet **domains, int filter_route);
+
+DnssecMode manager_get_dnssec_mode(Manager *m);
+bool manager_dnssec_supported(Manager *m);
+
+DnsOverTlsMode manager_get_dns_over_tls_mode(Manager *m);
+
+void manager_dnssec_verdict(Manager *m, DnssecVerdict verdict, const DnsResourceKey *key);
+
+bool manager_routable(Manager *m);
+
+void manager_flush_caches(Manager *m, int log_level);
+void manager_reset_server_features(Manager *m);
+
+void manager_cleanup_saved_user(Manager *m);
+
+bool manager_next_dnssd_names(Manager *m);
+
+bool manager_server_is_stub(Manager *m, DnsServer *s);
+
+int socket_disable_pmtud(int fd, int af);
diff --git a/src/resolve/resolved-mdns.c b/src/resolve/resolved-mdns.c
new file mode 100644
index 0000000..0d19d08
--- /dev/null
+++ b/src/resolve/resolved-mdns.c
@@ -0,0 +1,571 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <resolv.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include "alloc-util.h"
+#include "fd-util.h"
+#include "resolved-manager.h"
+#include "resolved-mdns.h"
+#include "sort-util.h"
+
+#define CLEAR_CACHE_FLUSH(x) (~MDNS_RR_CACHE_FLUSH_OR_QU & (x))
+
+void manager_mdns_stop(Manager *m) {
+        assert(m);
+
+        m->mdns_ipv4_event_source = sd_event_source_disable_unref(m->mdns_ipv4_event_source);
+        m->mdns_ipv4_fd = safe_close(m->mdns_ipv4_fd);
+
+        m->mdns_ipv6_event_source = sd_event_source_disable_unref(m->mdns_ipv6_event_source);
+        m->mdns_ipv6_fd = safe_close(m->mdns_ipv6_fd);
+}
+
+int manager_mdns_start(Manager *m) {
+        int r;
+
+        assert(m);
+
+        if (m->mdns_support == RESOLVE_SUPPORT_NO)
+                return 0;
+
+        r = manager_mdns_ipv4_fd(m);
+        if (r == -EADDRINUSE)
+                goto eaddrinuse;
+        if (r < 0)
+                return r;
+
+        if (socket_ipv6_is_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 bool mdns_should_reply_using_unicast(DnsPacket *p) {
+        DnsQuestionItem *item;
+
+        /* Work out if we should respond using multicast or unicast. */
+
+        /* The query was a legacy "one-shot mDNS query", RFC 6762, sections 5.1 and 6.7 */
+        if (p->sender_port != MDNS_PORT)
+                return true;
+
+        /* The query was a "direct unicast query", RFC 6762, section 5.5 */
+        switch (p->family) {
+        case AF_INET:
+                if (!in4_addr_equal(&p->destination.in, &MDNS_MULTICAST_IPV4_ADDRESS))
+                        return true;
+                break;
+        case AF_INET6:
+                if (!in6_addr_equal(&p->destination.in6, &MDNS_MULTICAST_IPV6_ADDRESS))
+                        return true;
+                break;
+        }
+
+        /* All the questions in the query had a QU bit set, RFC 6762, section 5.4 */
+        DNS_QUESTION_FOREACH_ITEM(item, p->question) {
+                if (!FLAGS_SET(item->flags, DNS_QUESTION_WANTS_UNICAST_REPLY))
+                        return false;
+        }
+        return true;
+}
+
+static bool sender_on_local_subnet(DnsScope *s, DnsPacket *p) {
+        LinkAddress *a;
+        int r;
+
+        /* Check whether the sender is on a local subnet. */
+
+        if (!s->link)
+                return false;
+
+        LIST_FOREACH(addresses, a, s->link->addresses) {
+                if (a->family != p->family)
+                        continue;
+                if (a->prefixlen == UCHAR_MAX) /* don't know subnet mask */
+                        continue;
+
+                r = in_addr_prefix_covers(a->family, &a->in_addr, a->prefixlen, &p->sender);
+                if (r < 0)
+                        log_debug_errno(r, "Failed to determine whether link address covers sender address: %m");
+                if (r > 0)
+                        return true;
+        }
+
+        return false;
+}
+
+
+static int mdns_scope_process_query(DnsScope *s, DnsPacket *p) {
+        _cleanup_(dns_answer_unrefp) DnsAnswer *full_answer = NULL;
+        _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL;
+        DnsResourceKey *key = NULL;
+        DnsResourceRecord *rr;
+        bool tentative = false;
+        bool legacy_query = p->sender_port != MDNS_PORT;
+        bool unicast_reply;
+        int r;
+
+        assert(s);
+        assert(p);
+
+        r = dns_packet_extract(p);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to extract resource records from incoming packet: %m");
+
+        assert_return((dns_question_size(p->question) > 0), -EINVAL);
+
+        unicast_reply = mdns_should_reply_using_unicast(p);
+        if (unicast_reply && !sender_on_local_subnet(s, p)) {
+                /* RFC 6762, section 5.5 recommends silently ignoring unicast queries
+                 * from senders outside the local network, so that we don't reveal our
+                 * internal network structure to outsiders. */
+                log_debug("Sender wants a unicast reply, but is not on a local subnet. Ignoring.");
+                return 0;
+        }
+
+        DNS_QUESTION_FOREACH(key, p->question) {
+                _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL;
+                DnsAnswerItem *item;
+
+                r = dns_zone_lookup(&s->zone, key, 0, &answer, &soa, &tentative);
+                if (r < 0)
+                        return log_debug_errno(r, "Failed to look up key: %m");
+
+                if (tentative && DNS_PACKET_NSCOUNT(p) > 0) {
+                        /*
+                         * A race condition detected with the probe packet from
+                         * a remote host.
+                         * Do simultaneous probe tiebreaking as described in
+                         * RFC 6762, Section 8.2. In case we lost don't reply
+                         * the question and withdraw conflicting RRs.
+                         */
+                        r = mdns_do_tiebreak(key, answer, p);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to do tiebreaking");
+
+                        if (r > 0) { /* we lost */
+                                DNS_ANSWER_FOREACH(rr, answer) {
+                                        DnsZoneItem *i;
+
+                                        i = dns_zone_get(&s->zone, rr);
+                                        if (i)
+                                                dns_zone_item_conflict(i);
+                                }
+
+                                continue;
+                        }
+                }
+
+                if (dns_answer_isempty(answer))
+                        continue;
+
+                /* Copy answer items from full_answer to answer, tweaking them if needed. */
+                if (full_answer) {
+                        r = dns_answer_reserve(&full_answer, dns_answer_size(answer));
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to reserve space in answer");
+                } else {
+                        full_answer = dns_answer_new(dns_answer_size(answer));
+                        if (!full_answer)
+                                return log_oom();
+                }
+
+                DNS_ANSWER_FOREACH_ITEM(item, answer) {
+                        DnsAnswerFlags flags = item->flags;
+                        /* The cache-flush bit must not be set in legacy unicast responses.
+                         * See section 6.7 of RFC 6762. */
+                        if (legacy_query)
+                                flags &= ~DNS_ANSWER_CACHE_FLUSH;
+                        r = dns_answer_add(full_answer, item->rr, item->ifindex, flags, item->rrsig);
+                        if (r < 0)
+                                return log_debug_errno(r, "Failed to extend answer: %m");
+                }
+        }
+
+        if (dns_answer_isempty(full_answer))
+                return 0;
+
+        r = dns_scope_make_reply_packet(s, DNS_PACKET_ID(p), DNS_RCODE_SUCCESS,
+                                        legacy_query ? p->question : NULL, full_answer,
+                                        NULL, false, &reply);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to build reply packet: %m");
+
+        if (!ratelimit_below(&s->ratelimit))
+                return 0;
+
+        if (unicast_reply) {
+                reply->destination = p->sender;
+                reply->destination_port = p->sender_port;
+        }
+        r = dns_scope_emit_udp(s, -1, AF_UNSPEC, reply);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to send reply packet: %m");
+
+        return 0;
+}
+
+static int on_mdns_packet(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
+        Manager *m = userdata;
+        DnsScope *scope;
+        int r;
+
+        r = manager_recv(m, fd, DNS_PROTOCOL_MDNS, &p);
+        if (r <= 0)
+                return r;
+
+        if (manager_packet_from_local_address(m, p))
+                return 0;
+
+        scope = manager_find_scope(m, p);
+        if (!scope) {
+                log_debug("Got mDNS UDP packet on unknown scope. Ignoring.");
+                return 0;
+        }
+
+        if (dns_packet_validate_reply(p) > 0) {
+                DnsResourceRecord *rr;
+                DnsTransaction *t;
+
+                log_debug("Got mDNS reply packet");
+
+                /*
+                 * mDNS is different from regular DNS and LLMNR with regard to handling responses.
+                 * While on other protocols, we can ignore every answer that doesn't match a question
+                 * we broadcast earlier, RFC6762, section 18.1 recommends looking at and caching all
+                 * incoming information, regardless of the DNS packet ID.
+                 *
+                 * Hence, extract the packet here, and try to find a transaction for answer the we got
+                 * and complete it. Also store the new information in scope's cache.
+                 */
+                r = dns_packet_extract(p);
+                if (r < 0) {
+                        log_debug("mDNS packet extraction failed.");
+                        return 0;
+                }
+
+                dns_scope_check_conflicts(scope, p);
+
+                DNS_ANSWER_FOREACH(rr, p->answer) {
+                        const char *name;
+
+                        name = dns_resource_key_name(rr->key);
+
+                        /* If the received reply packet contains ANY record that is not .local
+                         * or .in-addr.arpa or .ip6.arpa, we assume someone's playing tricks on
+                         * us and discard the packet completely. */
+                        if (!(dns_name_endswith(name, "in-addr.arpa") > 0 ||
+                              dns_name_endswith(name, "ip6.arpa") > 0 ||
+                              dns_name_endswith(name, "local") > 0))
+                                return 0;
+
+                        if (rr->ttl == 0) {
+                                log_debug("Got a goodbye packet");
+                                /* See the section 10.1 of RFC6762 */
+                                rr->ttl = 1;
+                        }
+                }
+
+                LIST_FOREACH(transactions_by_scope, t, scope->transactions) {
+                        r = dns_answer_match_key(p->answer, t->key, NULL);
+                        if (r < 0)
+                                log_debug_errno(r, "Failed to match resource key, ignoring: %m");
+                        else if (r > 0) /* This packet matches the transaction, let's pass it on as reply */
+                                dns_transaction_process_reply(t, p, false);
+                }
+
+                dns_cache_put(&scope->cache, scope->manager->enable_cache, NULL, DNS_PACKET_RCODE(p), p->answer, NULL, false, _DNSSEC_RESULT_INVALID, UINT32_MAX, p->family, &p->sender);
+
+        } else if (dns_packet_validate_query(p) > 0)  {
+                log_debug("Got mDNS query packet for id %u", DNS_PACKET_ID(p));
+
+                r = mdns_scope_process_query(scope, p);
+                if (r < 0) {
+                        log_debug_errno(r, "mDNS query processing failed: %m");
+                        return 0;
+                }
+        } else
+                log_debug("Invalid mDNS UDP packet.");
+
+        return 0;
+}
+
+int manager_mdns_ipv4_fd(Manager *m) {
+        union sockaddr_union sa = {
+                .in.sin_family = AF_INET,
+                .in.sin_port = htobe16(MDNS_PORT),
+        };
+        _cleanup_close_ int s = -1;
+        int r;
+
+        assert(m);
+
+        if (m->mdns_ipv4_fd >= 0)
+                return m->mdns_ipv4_fd;
+
+        s = socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
+        if (s < 0)
+                return log_error_errno(errno, "mDNS-IPv4: Failed to create socket: %m");
+
+        r = setsockopt_int(s, IPPROTO_IP, IP_TTL, 255);
+        if (r < 0)
+                return log_error_errno(r, "mDNS-IPv4: Failed to set IP_TTL: %m");
+
+        r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_TTL, 255);
+        if (r < 0)
+                return log_error_errno(r, "mDNS-IPv4: Failed to set IP_MULTICAST_TTL: %m");
+
+        r = setsockopt_int(s, IPPROTO_IP, IP_MULTICAST_LOOP, true);
+        if (r < 0)
+                return log_error_errno(r, "mDNS-IPv4: Failed to set IP_MULTICAST_LOOP: %m");
+
+        r = setsockopt_int(s, IPPROTO_IP, IP_PKTINFO, true);
+        if (r < 0)
+                return log_error_errno(r, "mDNS-IPv4: Failed to set IP_PKTINFO: %m");
+
+        r = setsockopt_int(s, IPPROTO_IP, IP_RECVTTL, true);
+        if (r < 0)
+                return log_error_errno(r, "mDNS-IPv4: Failed to set IP_RECVTTL: %m");
+
+        /* Disable Don't-Fragment bit in the IP header */
+        r = setsockopt_int(s, IPPROTO_IP, IP_MTU_DISCOVER, IP_PMTUDISC_DONT);
+        if (r < 0)
+                return log_error_errno(r, "mDNS-IPv4: Failed to set IP_MTU_DISCOVER: %m");
+
+        /* See the section 15.1 of RFC6762 */
+        /* first try to bind without SO_REUSEADDR to detect another mDNS responder */
+        r = bind(s, &sa.sa, sizeof(sa.in));
+        if (r < 0) {
+                if (errno != EADDRINUSE)
+                        return log_error_errno(errno, "mDNS-IPv4: Failed to bind socket: %m");
+
+                log_warning("mDNS-IPv4: There appears to be another mDNS responder running, or previously systemd-resolved crashed with some outstanding transfers.");
+
+                /* try again with SO_REUSEADDR */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "mDNS-IPv4: Failed to set SO_REUSEADDR: %m");
+
+                r = bind(s, &sa.sa, sizeof(sa.in));
+                if (r < 0)
+                        return log_error_errno(errno, "mDNS-IPv4: Failed to bind socket: %m");
+        } else {
+                /* enable SO_REUSEADDR for the case that the user really wants multiple mDNS responders */
+                r = setsockopt_int(s, SOL_SOCKET, SO_REUSEADDR, true);
+                if (r < 0)
+                        return log_error_errno(r, "mDNS-IPv4: Failed to set SO_REUSEADDR: %m");
+        }
+
+        r = sd_event_add_io(m->event, &m->mdns_ipv4_event_source, s, EPOLLIN, on_mdns_packet, m);
+        if (r < 0)
+                return log_error_errno(r, "mDNS-IPv4: Failed to create event source: %m");
+
+        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..100894d
--- /dev/null
+++ b/src/resolve/resolved-resolv-conf.c
@@ -0,0 +1,435 @@
+/* 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;
+        }
+
+        /* resolv.conf simply doesn't support any other ports than 53, hence there's nothing much we can
+         * do — we have to suppress these entries */
+        if (dns_server_port(s) != 53) {
+                log_debug("DNS server %s with non-standard UDP port number, suppressing from generated resolv.conf.", dns_server_string(s));
+                return;
+        }
+
+        /* Check if the scope this DNS server belongs to is suitable as 'default' route for lookups; resolv.conf does
+         * not have a syntax to express that, so it must not appear as a global name server to avoid routing unrelated
+         * domains to it (which is a privacy violation, will most probably fail anyway, and adds unnecessary load) */
+        scope = dns_server_scope(s);
+        if (scope && !dns_scope_is_default_route(scope)) {
+                log_debug("Scope of DNS server %s has only route-only domains, not using as global name server", dns_server_string(s));
+                return;
+        }
+
+        if (*count == MAXNS)
+                fputs("# Too many DNS servers configured, the following entries may be ignored.\n", f);
+        (*count)++;
+
+        fprintf(f, "nameserver %s\n", dns_server_string(s));
+}
+
+static void write_resolv_conf_search(
+                OrderedSet *domains,
+                FILE *f) {
+        char *domain;
+
+        assert(domains);
+        assert(f);
+
+        fputs("search", f);
+
+        ORDERED_SET_FOREACH(domain, domains) {
+                fputc(' ', f);
+                fputs(domain, f);
+        }
+
+        fputs("\n", f);
+}
+
+static int write_uplink_resolv_conf_contents(FILE *f, OrderedSet *dns, OrderedSet *domains) {
+
+        fputs("# This is "PRIVATE_UPLINK_RESOLV_CONF" managed by man:systemd-resolved(8).\n"
+              "# Do not edit.\n"
+              "#\n"
+              "# This file might be symlinked as /etc/resolv.conf. If you're looking at\n"
+              "# /etc/resolv.conf and seeing this text, you have followed the symlink.\n"
+              "#\n"
+              "# This is a dynamic resolv.conf file for connecting local clients directly to\n"
+              "# all known uplink DNS servers. This file lists all configured search domains.\n"
+              "#\n"
+              "# Third party programs should typically not access this file directly, but only\n"
+              "# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a\n"
+              "# different way, replace this symlink by a static file or a different symlink.\n"
+              "#\n"
+              "# See man:systemd-resolved.service(8) for details about the supported modes of\n"
+              "# operation for /etc/resolv.conf.\n"
+              "\n", f);
+
+        if (ordered_set_isempty(dns))
+                fputs("# No DNS servers known.\n", f);
+        else {
+                unsigned count = 0;
+                DnsServer *s;
+
+                ORDERED_SET_FOREACH(s, dns)
+                        write_resolv_conf_server(s, f, &count);
+        }
+
+        if (ordered_set_isempty(domains))
+                fputs("search .\n", f); /* Make sure that if the local hostname is chosen as fqdn this does not
+                                         * imply a search domain */
+        else
+                write_resolv_conf_search(domains, f);
+
+        return fflush_and_check(f);
+}
+
+static int write_stub_resolv_conf_contents(FILE *f, OrderedSet *dns, OrderedSet *domains) {
+        fputs("# This is "PRIVATE_STUB_RESOLV_CONF" managed by man:systemd-resolved(8).\n"
+              "# Do not edit.\n"
+              "#\n"
+              "# This file might be symlinked as /etc/resolv.conf. If you're looking at\n"
+              "# /etc/resolv.conf and seeing this text, you have followed the symlink.\n"
+              "#\n"
+              "# This is a dynamic resolv.conf file for connecting local clients to the\n"
+              "# internal DNS stub resolver of systemd-resolved. This file lists all\n"
+              "# configured search domains.\n"
+              "#\n"
+              "# Run \"resolvectl status\" to see details about the uplink DNS servers\n"
+              "# currently in use.\n"
+              "#\n"
+              "# Third party programs should typically not access this file directly, but only\n"
+              "# through the symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a\n"
+              "# different way, replace this symlink by a static file or a different symlink.\n"
+              "#\n"
+              "# See man:systemd-resolved.service(8) for details about the supported modes of\n"
+              "# operation for /etc/resolv.conf.\n"
+              "\n"
+              "nameserver 127.0.0.53\n"
+              "options edns0 trust-ad\n", f);
+
+        if (ordered_set_isempty(domains))
+                fputs("search .\n", f); /* Make sure that if the local hostname is chosen as fqdn this does not
+                                         * imply a search domain */
+        else
+                write_resolv_conf_search(domains, f);
+
+        return fflush_and_check(f);
+}
+
+int manager_write_resolv_conf(Manager *m) {
+        _cleanup_ordered_set_free_ OrderedSet *dns = NULL, *domains = NULL;
+        _cleanup_(unlink_and_freep) char *temp_path_uplink = NULL, *temp_path_stub = NULL;
+        _cleanup_fclose_ FILE *f_uplink = NULL, *f_stub = NULL;
+        int r;
+
+        assert(m);
+
+        /* Read the system /etc/resolv.conf first */
+        (void) manager_read_resolv_conf(m);
+
+        /* Add the full list to a set, to filter out duplicates */
+        r = manager_compile_dns_servers(m, &dns);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to compile list of DNS servers, ignoring: %m");
+
+        r = manager_compile_search_domains(m, &domains, false);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to compile list of search domains, ignoring: %m");
+
+        r = fopen_temporary_label(PRIVATE_UPLINK_RESOLV_CONF, PRIVATE_UPLINK_RESOLV_CONF, &f_uplink, &temp_path_uplink);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to open new %s for writing, ignoring: %m", PRIVATE_UPLINK_RESOLV_CONF);
+
+        (void) fchmod(fileno(f_uplink), 0644);
+
+        r = write_uplink_resolv_conf_contents(f_uplink, dns, domains);
+        if (r < 0)
+                return log_warning_errno(r, "Failed to write new %s, ignoring: %m", PRIVATE_UPLINK_RESOLV_CONF);
+
+        if (m->dns_stub_listener_mode != DNS_STUB_LISTENER_NO) {
+                r = fopen_temporary_label(PRIVATE_STUB_RESOLV_CONF, PRIVATE_STUB_RESOLV_CONF, &f_stub, &temp_path_stub);
+                if (r < 0)
+                        return log_warning_errno(r, "Failed to open new %s for writing, ignoring: %m", PRIVATE_STUB_RESOLV_CONF);
+
+                (void) fchmod(fileno(f_stub), 0644);
+
+                r = write_stub_resolv_conf_contents(f_stub, dns, domains);
+                if (r < 0)
+                        return log_warning_errno(r, "Failed to write new %s, ignoring: %m", PRIVATE_STUB_RESOLV_CONF);
+
+                r = conservative_rename(temp_path_stub, PRIVATE_STUB_RESOLV_CONF);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to move new %s into place, ignoring: %m", PRIVATE_STUB_RESOLV_CONF);
+
+                temp_path_stub = mfree(temp_path_stub); /* free the string explicitly, so that we don't unlink anymore */
+        } else {
+                r = symlink_atomic_label(basename(PRIVATE_UPLINK_RESOLV_CONF), PRIVATE_STUB_RESOLV_CONF);
+                if (r < 0)
+                        log_warning_errno(r, "Failed to symlink %s, ignoring: %m", PRIVATE_STUB_RESOLV_CONF);
+        }
+
+        r = conservative_rename(temp_path_uplink, PRIVATE_UPLINK_RESOLV_CONF);
+        if (r < 0)
+                log_warning_errno(r, "Failed to move new %s into place: %m", PRIVATE_UPLINK_RESOLV_CONF);
+
+        temp_path_uplink = mfree(temp_path_uplink); /* free the string explicitly, so that we don't unlink anymore */
+        return r;
+}
+
+int resolv_conf_mode(void) {
+        static const char * const table[_RESOLV_CONF_MODE_MAX] = {
+                [RESOLV_CONF_UPLINK] = PRIVATE_UPLINK_RESOLV_CONF,
+                [RESOLV_CONF_STUB] = PRIVATE_STUB_RESOLV_CONF,
+                [RESOLV_CONF_STATIC] = PRIVATE_STATIC_RESOLV_CONF,
+        };
+
+        struct stat system_st;
+
+        if (stat("/etc/resolv.conf", &system_st) < 0) {
+                if (errno == ENOENT)
+                        return RESOLV_CONF_MISSING;
+
+                return -errno;
+        }
+
+        for (ResolvConfMode m = 0; m < _RESOLV_CONF_MODE_MAX; m++) {
+                struct stat our_st;
+
+                if (!table[m])
+                        continue;
+
+                if (stat(table[m], &our_st) < 0) {
+                        if (errno != ENOENT)
+                                log_debug_errno(errno, "Failed to stat() %s, ignoring: %m", table[m]);
+
+                        continue;
+                }
+
+                if (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..8c0dee8
--- /dev/null
+++ b/src/resolve/resolved-resolv-conf.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "resolved-manager.h"
+
+int manager_check_resolv_conf(const Manager *m);
+int manager_read_resolv_conf(Manager *m);
+int manager_write_resolv_conf(Manager *m);
+
+typedef enum ResolvConfMode {
+        RESOLV_CONF_UPLINK,
+        RESOLV_CONF_STUB,
+        RESOLV_CONF_STATIC,
+        RESOLV_CONF_FOREIGN,
+        RESOLV_CONF_MISSING,
+        _RESOLV_CONF_MODE_MAX,
+        _RESOLV_CONF_MODE_INVALID = -EINVAL,
+} ResolvConfMode;
+
+int resolv_conf_mode(void);
+
+const char* resolv_conf_mode_to_string(ResolvConfMode m) _const_;
+ResolvConfMode resolv_conf_mode_from_string(const char *s) _pure_;
diff --git a/src/resolve/resolved-socket-graveyard.c b/src/resolve/resolved-socket-graveyard.c
new file mode 100644
index 0000000..471fe1d
--- /dev/null
+++ b/src/resolve/resolved-socket-graveyard.c
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+
+#include "resolved-socket-graveyard.h"
+
+#define SOCKET_GRAVEYARD_USEC (5 * USEC_PER_SEC)
+#define SOCKET_GRAVEYARD_MAX 100
+
+/* This implements a socket "graveyard" for UDP sockets. If a socket fd is added to the graveyard it is kept
+ * open for a couple of more seconds, expecting one reply. Once the reply is received the fd is closed
+ * immediately, or if none is received it is closed after the timeout. Why all this? So that if we contact a
+ * DNS server, and it doesn't reply instantly, and we lose interest in the response and thus close the fd, we
+ * don't end up sending back an ICMP error once the server responds but we aren't listening anymore. (See
+ * https://github.com/systemd/systemd/issues/17421 for further information.)
+ *
+ * Note that we don't allocate any timer event source to clear up the graveyard once the socket's timeout is
+ * reached. Instead we operate lazily: we close old entries when adding a new fd to the graveyard, or
+ * whenever any code runs manager_socket_graveyard_process() — which the DNS transaction code does right
+ * before allocating a new UDP socket. */
+
+static SocketGraveyard* socket_graveyard_free(SocketGraveyard *g) {
+        if (!g)
+                return NULL;
+
+        if (g->manager) {
+                assert(g->manager->n_socket_graveyard > 0);
+                g->manager->n_socket_graveyard--;
+
+                if (g->manager->socket_graveyard_oldest == g)
+                        g->manager->socket_graveyard_oldest = g->graveyard_prev;
+
+                LIST_REMOVE(graveyard, g->manager->socket_graveyard, g);
+
+                assert((g->manager->n_socket_graveyard > 0) == !!g->manager->socket_graveyard);
+                assert((g->manager->n_socket_graveyard > 0) == !!g->manager->socket_graveyard_oldest);
+        }
+
+        if (g->io_event_source) {
+                log_debug("Closing graveyard socket fd %i", sd_event_source_get_io_fd(g->io_event_source));
+                sd_event_source_disable_unref(g->io_event_source);
+        }
+
+        return mfree(g);
+}
+
+DEFINE_TRIVIAL_CLEANUP_FUNC(SocketGraveyard*, socket_graveyard_free);
+
+void manager_socket_graveyard_process(Manager *m) {
+        usec_t n = USEC_INFINITY;
+
+        assert(m);
+
+        while (m->socket_graveyard_oldest) {
+                SocketGraveyard *g = m->socket_graveyard_oldest;
+
+                if (n == USEC_INFINITY)
+                        assert_se(sd_event_now(m->event, clock_boottime_or_monotonic(), &n) >= 0);
+
+                if (g->deadline > n)
+                        break;
+
+                socket_graveyard_free(g);
+        }
+}
+
+void manager_socket_graveyard_clear(Manager *m) {
+        assert(m);
+
+        while (m->socket_graveyard)
+                socket_graveyard_free(m->socket_graveyard);
+}
+
+static int on_io_event(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
+        SocketGraveyard *g = userdata;
+
+        assert(g);
+
+        /* An IO event happened on the graveyard fd. We don't actually care which event that is, and we don't
+         * read any incoming packet off the socket. We just close the fd, that's enough to not trigger the
+         * ICMP unreachable port event */
+
+        socket_graveyard_free(g);
+        return 0;
+}
+
+static void manager_socket_graveyard_make_room(Manager *m) {
+        assert(m);
+
+        while (m->n_socket_graveyard >= SOCKET_GRAVEYARD_MAX)
+                socket_graveyard_free(m->socket_graveyard_oldest);
+}
+
+int manager_add_socket_to_graveyard(Manager *m, int fd) {
+        _cleanup_(socket_graveyard_freep) SocketGraveyard *g = NULL;
+        int r;
+
+        assert(m);
+        assert(fd >= 0);
+
+        manager_socket_graveyard_process(m);
+        manager_socket_graveyard_make_room(m);
+
+        g = new(SocketGraveyard, 1);
+        if (!g)
+                return log_oom();
+
+        *g = (SocketGraveyard) {
+                .manager = m,
+        };
+
+        LIST_PREPEND(graveyard, m->socket_graveyard, g);
+        if (!m->socket_graveyard_oldest)
+                m->socket_graveyard_oldest = g;
+
+        m->n_socket_graveyard++;
+
+        assert_se(sd_event_now(m->event, clock_boottime_or_monotonic(), &g->deadline) >= 0);
+        g->deadline += SOCKET_GRAVEYARD_USEC;
+
+        r = sd_event_add_io(m->event, &g->io_event_source, fd, EPOLLIN, on_io_event, g);
+        if (r < 0)
+                return log_error_errno(r, "Failed to create graveyard IO source: %m");
+
+        r = sd_event_source_set_io_fd_own(g->io_event_source, true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to enable graveyard IO source fd ownership: %m");
+
+        (void) sd_event_source_set_description(g->io_event_source, "graveyard");
+
+        log_debug("Added socket %i to graveyard", fd);
+
+        TAKE_PTR(g);
+        return 0;
+}
diff --git a/src/resolve/resolved-socket-graveyard.h b/src/resolve/resolved-socket-graveyard.h
new file mode 100644
index 0000000..9b13bb0
--- /dev/null
+++ b/src/resolve/resolved-socket-graveyard.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: LGPL-2.1+ */
+#pragma once
+
+typedef struct SocketGraveyard SocketGraveyard;
+
+#include "resolved-manager.h"
+
+struct SocketGraveyard {
+        Manager *manager;
+        usec_t deadline;
+        sd_event_source *io_event_source;
+        LIST_FIELDS(SocketGraveyard, graveyard);
+};
+
+void manager_socket_graveyard_process(Manager *m);
+void manager_socket_graveyard_clear(Manager *m);
+
+int manager_add_socket_to_graveyard(Manager *m, int fd);
diff --git a/src/resolve/resolved-util.c b/src/resolve/resolved-util.c
new file mode 100644
index 0000000..00abada
--- /dev/null
+++ b/src/resolve/resolved-util.c
@@ -0,0 +1,84 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "dns-def.h"
+#include "dns-domain.h"
+#include "hostname-util.h"
+#include "idn-util.h"
+#include "resolved-util.h"
+#include "utf8.h"
+
+int resolve_system_hostname(char **full_hostname, char **first_label) {
+        _cleanup_free_ char *h = NULL, *n = NULL;
+#if HAVE_LIBIDN2
+        _cleanup_free_ char *utf8 = NULL;
+#elif HAVE_LIBIDN
+        int k;
+#endif
+        char label[DNS_LABEL_MAX];
+        const char *p, *decoded;
+        int r;
+
+        /* Return the full hostname in *full_hostname, if nonnull.
+         *
+         * Extract and normalize the first label of the locally configured hostname, check it's not
+         * "localhost", and return it in *first_label, if nonnull. */
+
+        r = gethostname_strict(&h);
+        if (r < 0)
+                return log_debug_errno(r, "Can't determine system hostname: %m");
+
+        p = h;
+        r = dns_label_unescape(&p, label, sizeof label, 0);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to unescape hostname: %m");
+        if (r == 0)
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "Couldn't find a single label in hostname.");
+
+#if HAVE_LIBIDN || HAVE_LIBIDN2
+        r = dlopen_idn();
+        if (r < 0) {
+                log_debug_errno(r, "Failed to initialize IDN support, ignoring: %m");
+                decoded = label; /* no decoding */
+        } else
+#endif
+        {
+#if HAVE_LIBIDN2
+                r = sym_idn2_to_unicode_8z8z(label, &utf8, 0);
+                if (r != IDN2_OK)
+                        return log_debug_errno(SYNTHETIC_ERRNO(EUCLEAN),
+                                               "Failed to undo IDNA: %s", sym_idn2_strerror(r));
+                assert(utf8_is_valid(utf8));
+
+                r = strlen(utf8);
+                decoded = utf8;
+#elif HAVE_LIBIDN
+                k = dns_label_undo_idna(label, r, label, sizeof label);
+                if (k < 0)
+                        return log_debug_errno(k, "Failed to undo IDNA: %m");
+                if (k > 0)
+                        r = k;
+
+                if (!utf8_is_valid(label))
+                        return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+                                               "System hostname is not UTF-8 clean.");
+                decoded = label;
+#else
+                decoded = label; /* no decoding */
+#endif
+        }
+
+        r = dns_label_escape_new(decoded, r, &n);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to escape hostname: %m");
+
+        if (is_localhost(n))
+                return log_debug_errno(SYNTHETIC_ERRNO(EINVAL),
+                                       "System hostname is 'localhost', ignoring.");
+
+        if (full_hostname)
+                *full_hostname = TAKE_PTR(h);
+        if (first_label)
+                *first_label = TAKE_PTR(n);
+        return 0;
+}
diff --git a/src/resolve/resolved-util.h b/src/resolve/resolved-util.h
new file mode 100644
index 0000000..446b7c9
--- /dev/null
+++ b/src/resolve/resolved-util.h
@@ -0,0 +1,4 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+int resolve_system_hostname(char **full_hostname, char **first_label);
diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c
new file mode 100644
index 0000000..9a61b19
--- /dev/null
+++ b/src/resolve/resolved-varlink.c
@@ -0,0 +1,561 @@
+/* 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_NO_SOURCE:
+                return varlink_error(q->varlink_request, "io.systemd.Resolve.NoSource", NULL);
+
+        case DNS_TRANSACTION_STUB_LOOP:
+                return varlink_error(q->varlink_request, "io.systemd.Resolve.StubLoop", NULL);
+
+        case DNS_TRANSACTION_NOT_FOUND:
+                /* We return this as NXDOMAIN. This is only generated when a host doesn't implement LLMNR/TCP, and we
+                 * thus quickly know that we cannot resolve an in-addr.arpa or ip6.arpa address. */
+                return varlink_errorb(q->varlink_request, "io.systemd.Resolve.DNSError",
+                                      JSON_BUILD_OBJECT(JSON_BUILD_PAIR("rcode", JSON_BUILD_INTEGER(DNS_RCODE_NXDOMAIN))));
+
+        case DNS_TRANSACTION_RCODE_FAILURE:
+                return varlink_errorb(q->varlink_request, "io.systemd.Resolve.DNSError",
+                                      JSON_BUILD_OBJECT(JSON_BUILD_PAIR("rcode", JSON_BUILD_INTEGER(q->answer_rcode))));
+
+        case DNS_TRANSACTION_NULL:
+        case DNS_TRANSACTION_PENDING:
+        case DNS_TRANSACTION_VALIDATING:
+        case DNS_TRANSACTION_SUCCESS:
+        default:
+                assert_not_reached("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 a bunch of NO_XYZ flags are on at most, plus the
+         *    method-specific flags specified in 'ok'.
+         *
+         * 2. If no protocols are enabled we automatically convert that to "all protocols are enabled".
+         *
+         * The second rule means that clients can just pass 0 as flags for the common case, and all supported
+         * protocols are enabled. Moreover it's useful so that client's do not have to be aware of all
+         * protocols implemented in resolved, but can use 0 as protocols flags set as indicator for
+         * "everything".
+         */
+
+        if (*flags & ~(SD_RESOLVED_PROTOCOLS_ALL|
+                       SD_RESOLVED_NO_CNAME|
+                       SD_RESOLVED_NO_VALIDATE|
+                       SD_RESOLVED_NO_SYNTHESIZE|
+                       SD_RESOLVED_NO_CACHE|
+                       SD_RESOLVED_NO_ZONE|
+                       SD_RESOLVED_NO_TRUST_ANCHOR|
+                       SD_RESOLVED_NO_NETWORK|
+                       ok))
+                return false;
+
+        if ((*flags & SD_RESOLVED_PROTOCOLS_ALL) == 0) /* If no protocol is enabled, enable all */
+                *flags |= SD_RESOLVED_PROTOCOLS_ALL;
+
+        /* If the SD_RESOLVED_NO_SEARCH flag is acceptable, and the query name is dot-suffixed, turn off
+         * search domains. Note that DNS name normalization drops the dot suffix, hence we propagate this
+         * into the flags field as early as we can. */
+        if (name && FLAGS_SET(ok, SD_RESOLVED_NO_SEARCH) && dns_name_dot_suffixed(name) > 0)
+                *flags |= SD_RESOLVED_NO_SEARCH;
+
+        return true;
+}
+
+static void vl_method_resolve_hostname_complete(DnsQuery *query) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
+        _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        _cleanup_free_ char *normalized = NULL;
+        DnsResourceRecord *rr;
+        DnsQuestion *question;
+        int ifindex, r;
+
+        assert(q);
+
+        if (q->state != DNS_TRANSACTION_SUCCESS) {
+                r = reply_query_state(q);
+                goto finish;
+        }
+
+        r = dns_query_process_cname_many(q);
+        if (r == -ELOOP) {
+                r = varlink_error(q->varlink_request, "io.systemd.Resolve.CNAMELoop", NULL);
+                goto finish;
+        }
+        if (r < 0)
+                goto finish;
+        if (r == DNS_QUERY_CNAME) {
+                /* This was a cname, and the query was restarted. */
+                TAKE_PTR(q);
+                return;
+        }
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
+                _cleanup_(json_variant_unrefp) JsonVariant *entry = NULL;
+                int family;
+                const void *p;
+
+                r = dns_question_matches_rr(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue;
+
+                if (rr->key->type == DNS_TYPE_A) {
+                        family = AF_INET;
+                        p = &rr->a.in_addr;
+                } else if (rr->key->type == DNS_TYPE_AAAA) {
+                        family = AF_INET6;
+                        p = &rr->aaaa.in6_addr;
+                } else {
+                        r = -EAFNOSUPPORT;
+                        goto finish;
+                }
+
+                r = json_build(&entry,
+                               JSON_BUILD_OBJECT(
+                                               JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)),
+                                               JSON_BUILD_PAIR("family", JSON_BUILD_INTEGER(family)),
+                                               JSON_BUILD_PAIR("address", JSON_BUILD_BYTE_ARRAY(p, FAMILY_ADDRESS_SIZE(family)))));
+                if (r < 0)
+                        goto finish;
+
+                if (!canonical)
+                        canonical = dns_resource_record_ref(rr);
+
+                r = json_variant_append_array(&array, entry);
+                if (r < 0)
+                        goto finish;
+        }
+
+        if (json_variant_is_blank_object(array)) {
+                r = varlink_error(q->varlink_request, "io.systemd.Resolve.NoSuchResourceRecord", NULL);
+                goto finish;
+        }
+
+        assert(canonical);
+        r = dns_name_normalize(dns_resource_key_name(canonical->key), 0, &normalized);
+        if (r < 0)
+                goto finish;
+
+        r = varlink_replyb(q->varlink_request,
+                           JSON_BUILD_OBJECT(
+                                           JSON_BUILD_PAIR("addresses", JSON_BUILD_VARIANT(array)),
+                                           JSON_BUILD_PAIR("name", JSON_BUILD_STRING(normalized)),
+                                           JSON_BUILD_PAIR("flags", JSON_BUILD_INTEGER(dns_query_reply_flags_make(q)))));
+finish:
+        if (r < 0) {
+                log_error_errno(r, "Failed to send hostname reply: %m");
+                r = varlink_error_errno(q->varlink_request, r);
+        }
+}
+
+static int parse_as_address(Varlink *link, LookupParameters *p) {
+        _cleanup_free_ char *canonical = NULL;
+        int r, ff, parsed_ifindex, ifindex;
+        union in_addr_union parsed;
+
+        assert(link);
+        assert(p);
+
+        /* Check if this parses as literal address. If so, just parse it and return that, do not involve networking */
+        r = in_addr_ifindex_from_string_auto(p->name, &ff, &parsed, &parsed_ifindex);
+        if (r < 0)
+                return 0; /* not a literal address */
+
+        /* Make sure the data we parsed matches what is requested */
+        if ((p->family != AF_UNSPEC && ff != p->family) ||
+            (p->ifindex > 0 && parsed_ifindex > 0 && parsed_ifindex != p->ifindex))
+                return varlink_error(link, "io.systemd.Resolve.NoSuchResourceRecord", NULL);
+
+        ifindex = parsed_ifindex > 0 ? parsed_ifindex : p->ifindex;
+
+        /* Reformat the address as string, to return as canonicalized name */
+        r = in_addr_ifindex_to_string(ff, &parsed, ifindex, &canonical);
+        if (r < 0)
+                return r;
+
+        return varlink_replyb(
+                        link,
+                        JSON_BUILD_OBJECT(
+                                JSON_BUILD_PAIR("addresses",
+                                        JSON_BUILD_ARRAY(
+                                                JSON_BUILD_OBJECT(
+                                                        JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)),
+                                                        JSON_BUILD_PAIR("family", JSON_BUILD_INTEGER(ff)),
+                                                        JSON_BUILD_PAIR("address", JSON_BUILD_BYTE_ARRAY(&parsed, FAMILY_ADDRESS_SIZE(ff)))))),
+                                JSON_BUILD_PAIR("name", JSON_BUILD_STRING(canonical)),
+                                JSON_BUILD_PAIR("flags", JSON_BUILD_INTEGER(SD_RESOLVED_FLAGS_MAKE(dns_synthesize_protocol(p->flags), ff, true, true)|
+                                                                            SD_RESOLVED_SYNTHETIC))));
+}
+
+static int vl_method_resolve_hostname(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+        static const JsonDispatch dispatch_table[] = {
+                { "ifindex", JSON_VARIANT_UNSIGNED, json_dispatch_int,    offsetof(LookupParameters, ifindex), 0              },
+                { "name",    JSON_VARIANT_STRING,   json_dispatch_string, offsetof(LookupParameters, name),    JSON_MANDATORY },
+                { "family",  JSON_VARIANT_UNSIGNED, json_dispatch_int,    offsetof(LookupParameters, family),  0              },
+                { "flags",   JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(LookupParameters, flags),   0              },
+                {}
+        };
+
+        _cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL;
+        _cleanup_(lookup_parameters_destroy) LookupParameters p = {
+                .family = AF_UNSPEC,
+        };
+        _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+        Manager *m;
+        int r;
+
+        assert(link);
+
+        m = varlink_server_get_userdata(varlink_get_server(link));
+        assert(m);
+
+        if (FLAGS_SET(flags, VARLINK_METHOD_ONEWAY))
+                return -EINVAL;
+
+        r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+        if (r < 0)
+                return r;
+
+        if (p.ifindex < 0)
+                return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("ifindex"));
+
+        r = dns_name_is_valid(p.name);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("name"));
+
+        if (!IN_SET(p.family, AF_UNSPEC, AF_INET, AF_INET6))
+                return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("family"));
+
+        if (!validate_and_mangle_flags(p.name, &p.flags, SD_RESOLVED_NO_SEARCH))
+                return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("flags"));
+
+        r = parse_as_address(link, &p);
+        if (r != 0)
+                return r;
+
+        r = dns_question_new_address(&question_utf8, p.family, p.name, false);
+        if (r < 0)
+                return r;
+
+        r = dns_question_new_address(&question_idna, p.family, p.name, true);
+        if (r < 0 && r != -EALREADY)
+                return r;
+
+        r = dns_query_new(m, &q, question_utf8, question_idna ?: question_utf8, NULL, p.ifindex, p.flags);
+        if (r < 0)
+                return r;
+
+        q->varlink_request = varlink_ref(link);
+        varlink_set_userdata(link, q);
+        q->request_family = p.family;
+        q->complete = vl_method_resolve_hostname_complete;
+
+        r = dns_query_go(q);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(q);
+        return 1;
+}
+
+static int json_dispatch_address(const char *name, JsonVariant *variant, JsonDispatchFlags flags, void *userdata) {
+        LookupParameters *p = 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 *query) {
+        _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
+        _cleanup_(dns_query_freep) DnsQuery *q = query;
+        DnsQuestion *question;
+        DnsResourceRecord *rr;
+        int ifindex, r;
+
+        assert(q);
+
+        if (q->state != DNS_TRANSACTION_SUCCESS) {
+                r = reply_query_state(q);
+                goto finish;
+        }
+
+        r = dns_query_process_cname_many(q);
+        if (r == -ELOOP) {
+                r = varlink_error(q->varlink_request, "io.systemd.Resolve.CNAMELoop", NULL);
+                goto finish;
+        }
+        if (r < 0)
+                goto finish;
+        if (r == DNS_QUERY_CNAME) {
+                /* This was a cname, and the query was restarted. */
+                TAKE_PTR(q);
+                return;
+        }
+
+        question = dns_query_question_for_protocol(q, q->answer_protocol);
+
+        DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
+                _cleanup_(json_variant_unrefp) JsonVariant *entry = NULL;
+                _cleanup_free_ char *normalized = NULL;
+
+                r = dns_question_matches_rr(question, rr, NULL);
+                if (r < 0)
+                        goto finish;
+                if (r == 0)
+                        continue;
+
+                r = dns_name_normalize(rr->ptr.name, 0, &normalized);
+                if (r < 0)
+                        goto finish;
+
+                r = json_build(&entry,
+                               JSON_BUILD_OBJECT(
+                                               JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)),
+                                               JSON_BUILD_PAIR("name", JSON_BUILD_STRING(normalized))));
+                if (r < 0)
+                        goto finish;
+
+                r = json_variant_append_array(&array, entry);
+                if (r < 0)
+                        goto finish;
+        }
+
+        if (json_variant_is_blank_object(array)) {
+                r = varlink_error(q->varlink_request, "io.systemd.Resolve.NoSuchResourceRecord", NULL);
+                goto finish;
+        }
+
+        r = varlink_replyb(q->varlink_request,
+                           JSON_BUILD_OBJECT(
+                                           JSON_BUILD_PAIR("names", JSON_BUILD_VARIANT(array)),
+                                           JSON_BUILD_PAIR("flags", JSON_BUILD_INTEGER(dns_query_reply_flags_make(q)))));
+finish:
+        if (r < 0) {
+                log_error_errno(r, "Failed to send address reply: %m");
+                r = varlink_error_errno(q->varlink_request, r);
+        }
+}
+
+static int vl_method_resolve_address(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+        static const JsonDispatch dispatch_table[] = {
+                { "ifindex", JSON_VARIANT_UNSIGNED, json_dispatch_int,     offsetof(LookupParameters, ifindex), 0              },
+                { "family",  JSON_VARIANT_UNSIGNED, json_dispatch_int,     offsetof(LookupParameters, family),  JSON_MANDATORY },
+                { "address", JSON_VARIANT_ARRAY,    json_dispatch_address, 0,                                   JSON_MANDATORY },
+                { "flags",   JSON_VARIANT_UNSIGNED, json_dispatch_uint64,  offsetof(LookupParameters, flags),   0              },
+                {}
+        };
+
+        _cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
+        _cleanup_(lookup_parameters_destroy) LookupParameters p = {
+                .family = AF_UNSPEC,
+        };
+        _cleanup_(dns_query_freep) DnsQuery *q = NULL;
+        Manager *m;
+        int r;
+
+        assert(link);
+
+        m = varlink_server_get_userdata(varlink_get_server(link));
+        assert(m);
+
+        if (FLAGS_SET(flags, VARLINK_METHOD_ONEWAY))
+                return -EINVAL;
+
+        r = json_dispatch(parameters, dispatch_table, NULL, 0, &p);
+        if (r < 0)
+                return r;
+
+        if (p.ifindex < 0)
+                return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("ifindex"));
+
+        if (!IN_SET(p.family, AF_INET, AF_INET6))
+                return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("family"));
+
+        if (FAMILY_ADDRESS_SIZE(p.family) != p.address_size)
+                return varlink_error(link, "io.systemd.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, NULL, p.ifindex, p.flags|SD_RESOLVED_NO_SEARCH);
+        if (r < 0)
+                return r;
+
+        q->varlink_request = varlink_ref(link);
+        varlink_set_userdata(link, q);
+
+        q->request_family = p.family;
+        q->request_address = p.address;
+        q->complete = vl_method_resolve_address_complete;
+
+        r = dns_query_go(q);
+        if (r < 0)
+                return r;
+
+        TAKE_PTR(q);
+        return 1;
+}
+
+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..85ab917
--- /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;
+        _unused_ _cleanup_(notify_on_cleanup) const char *notify_stop = NULL;
+        int r;
+
+        log_setup();
+
+        r = service_parse_argv("systemd-resolved.service",
+                               "Provide name resolution with caching using DNS, mDNS, LLMNR.",
+                               BUS_IMPLEMENTATIONS(&manager_object,
+                                                   &log_control_object),
+                               argc, argv);
+        if (r <= 0)
+                return r;
+
+        umask(0022);
+
+        r = mac_selinux_init();
+        if (r < 0)
+                return r;
+
+        /* Drop privileges, but only if we have been started as root. If we are not running as root we assume most
+         * privileges are already dropped and we can't create our directory. */
+        if (getuid() == 0) {
+                const char *user = "systemd-resolve";
+                uid_t uid;
+                gid_t gid;
+
+                r = get_user_creds(&user, &uid, &gid, NULL, NULL, 0);
+                if (r < 0)
+                        return log_error_errno(r, "Cannot resolve user name %s: %m", user);
+
+                /* As we're root, we can create the directory where resolv.conf will live */
+                r = mkdir_safe_label("/run/systemd/resolve", 0755, uid, gid, MKDIR_WARN_MODE);
+                if (r < 0)
+                        return log_error_errno(r, "Could not create runtime directory: %m");
+
+                /* Drop privileges, but keep three caps. Note that we drop two of those too, later on (see below) */
+                r = drop_privileges(uid, gid,
+                                    (UINT64_C(1) << CAP_NET_RAW)|          /* needed for SO_BINDTODEVICE */
+                                    (UINT64_C(1) << CAP_NET_BIND_SERVICE)| /* needed to bind on port 53 */
+                                    (UINT64_C(1) << CAP_SETPCAP)           /* needed in order to drop the caps later */);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to drop privileges: %m");
+        }
+
+        assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGTERM, SIGINT, SIGUSR1, SIGUSR2, SIGRTMIN+1, -1) >= 0);
+
+        r = manager_new(&m);
+        if (r < 0)
+                return log_error_errno(r, "Could not create manager: %m");
+
+        r = manager_start(m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to start manager: %m");
+
+        /* Write finish default resolv.conf to avoid a dangling symlink */
+        (void) manager_write_resolv_conf(m);
+
+        (void) manager_check_resolv_conf(m);
+
+        /* Let's drop the remaining caps now */
+        r = capability_bounding_set_drop((UINT64_C(1) << CAP_NET_RAW), true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to drop remaining caps: %m");
+
+        notify_stop = notify_start(NOTIFY_READY, NOTIFY_STOPPING);
+
+        r = sd_event_loop(m->event);
+        if (r < 0)
+                return log_error_errno(r, "Event loop failed: %m");
+
+        return 0;
+}
+
+DEFINE_MAIN_FUNCTION(run);
diff --git a/src/resolve/resolved.conf.in b/src/resolve/resolved.conf.in
new file mode 100644
index 0000000..6d4176d
--- /dev/null
+++ b/src/resolve/resolved.conf.in
@@ -0,0 +1,34 @@
+#  This file is part of systemd.
+#
+#  systemd is free software; you can redistribute it and/or modify it under the
+#  terms of the GNU Lesser General Public License as published by the Free
+#  Software Foundation; either version 2.1 of the License, or (at your option)
+#  any later version.
+#
+# Entries in this file show the compile time defaults. Local configuration
+# should be created by either modifying this file, or by creating "drop-ins" in
+# the resolved.conf.d/ subdirectory. The latter is generally recommended.
+# Defaults can be restored by simply deleting this file and all drop-ins.
+#
+# Use 'systemd-analyze cat-config systemd/resolved.conf' to display the full config.
+#
+# See resolved.conf(5) for details.
+
+[Resolve]
+# Some examples of DNS servers which may be used for DNS= and FallbackDNS=:
+# Cloudflare: 1.1.1.1#cloudflare-dns.com 1.0.0.1#cloudflare-dns.com 2606:4700:4700::1111#cloudflare-dns.com 2606:4700:4700::1001#cloudflare-dns.com
+# Google:     8.8.8.8#dns.google 8.8.4.4#dns.google 2001:4860:4860::8888#dns.google 2001:4860:4860::8844#dns.google
+# Quad9:      9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net 2620:fe::fe#dns.quad9.net 2620:fe::9#dns.quad9.net
+#DNS=
+#FallbackDNS={{DNS_SERVERS}}
+#Domains=
+#DNSSEC={{DEFAULT_DNSSEC_MODE_STR}}
+#DNSOverTLS={{DEFAULT_DNS_OVER_TLS_MODE_STR}}
+#MulticastDNS={{DEFAULT_MDNS_MODE_STR}}
+#LLMNR={{DEFAULT_LLMNR_MODE_STR}}
+#Cache=yes
+#CacheFromLocalhost=no
+#DNSStubListener=yes
+#DNSStubListenerExtra=
+#ReadEtcHosts=yes
+#ResolveUnicastSingleLabel=no
diff --git a/src/resolve/test-dns-packet.c b/src/resolve/test-dns-packet.c
new file mode 100644
index 0000000..01f15ca
--- /dev/null
+++ b/src/resolve/test-dns-packet.c
@@ -0,0 +1,155 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <net/if.h>
+
+#include "sd-id128.h"
+
+#include "alloc-util.h"
+#include "fileio.h"
+#include "glob-util.h"
+#include "log.h"
+#include "macro.h"
+#include "resolved-dns-packet.h"
+#include "resolved-dns-rr.h"
+#include "path-util.h"
+#include "string-util.h"
+#include "strv.h"
+#include "tests.h"
+#include "unaligned.h"
+
+#define HASH_KEY SD_ID128_MAKE(d3,1e,48,90,4b,fa,4c,fe,af,9d,d5,a1,d7,2e,8a,b1)
+
+static void verify_rr_copy(DnsResourceRecord *rr) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *copy = NULL;
+        const char *a, *b;
+
+        assert_se(copy = dns_resource_record_copy(rr));
+        assert_se(dns_resource_record_equal(copy, rr) > 0);
+
+        assert_se(a = dns_resource_record_to_string(rr));
+        assert_se(b = dns_resource_record_to_string(copy));
+
+        assert_se(streq(a, b));
+}
+
+static uint64_t hash(DnsResourceRecord *rr) {
+        struct siphash state;
+
+        siphash24_init(&state, HASH_KEY.bytes);
+        dns_resource_record_hash_func(rr, &state);
+        return siphash24_finalize(&state);
+}
+
+static void test_packet_from_file(const char* filename, bool canonical) {
+        _cleanup_free_ char *data = NULL;
+        size_t data_size, packet_size, offset;
+
+        assert_se(read_full_file(filename, &data, &data_size) >= 0);
+        assert_se(data);
+        assert_se(data_size > 8);
+
+        log_info("============== %s %s==============", filename, canonical ? "canonical " : "");
+
+        for (offset = 0; offset < data_size; offset += 8 + packet_size) {
+                _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL, *p2 = NULL;
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL, *rr2 = NULL;
+                const char *s, *s2;
+                uint64_t hash1, hash2;
+
+                packet_size = unaligned_read_le64(data + offset);
+                assert_se(packet_size > 0);
+                assert_se(offset + 8 + packet_size <= data_size);
+
+                assert_se(dns_packet_new(&p, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0);
+
+                assert_se(dns_packet_append_blob(p, data + offset + 8, packet_size, NULL) >= 0);
+                assert_se(dns_packet_read_rr(p, &rr, NULL, NULL) >= 0);
+
+                verify_rr_copy(rr);
+
+                s = dns_resource_record_to_string(rr);
+                assert_se(s);
+                puts(s);
+
+                hash1 = hash(rr);
+
+                assert_se(dns_resource_record_to_wire_format(rr, canonical) >= 0);
+
+                assert_se(dns_packet_new(&p2, DNS_PROTOCOL_DNS, 0, DNS_PACKET_SIZE_MAX) >= 0);
+                assert_se(dns_packet_append_blob(p2, rr->wire_format, rr->wire_format_size, NULL) >= 0);
+                assert_se(dns_packet_read_rr(p2, &rr2, NULL, NULL) >= 0);
+
+                verify_rr_copy(rr);
+
+                s2 = dns_resource_record_to_string(rr);
+                assert_se(s2);
+                assert_se(streq(s, s2));
+
+                hash2 = hash(rr);
+                assert_se(hash1 == hash2);
+        }
+}
+
+static void test_dns_resource_record_get_cname_target(void) {
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *cname = NULL, *dname = NULL;
+        _cleanup_free_ char *target = NULL;
+
+        assert_se(cname = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_CNAME, "quux.foobar"));
+        assert_se(cname->cname.name = strdup("wuff.wuff"));
+
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "waldo"), cname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "foobar"), cname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux"), cname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, ""), cname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "."), cname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "nope.quux.foobar"), cname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux.foobar"), cname, &target) == 0);
+        assert_se(streq(target, "wuff.wuff"));
+        target = mfree(target);
+
+        assert_se(dname = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNAME, "quux.foobar"));
+        assert_se(dname->dname.name = strdup("wuff.wuff"));
+
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "waldo"), dname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "foobar"), dname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux"), dname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, ""), dname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "."), dname, &target) == -EUNATCH);
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "yupp.quux.foobar"), dname, &target) == 0);
+        assert_se(streq(target, "yupp.wuff.wuff"));
+        target = mfree(target);
+
+        assert_se(dns_resource_record_get_cname_target(&DNS_RESOURCE_KEY_CONST(DNS_CLASS_IN, DNS_TYPE_A, "quux.foobar"), cname, &target) == 0);
+        assert_se(streq(target, "wuff.wuff"));
+}
+
+int main(int argc, char **argv) {
+        int N;
+        _cleanup_globfree_ glob_t g = {};
+        char **fnames;
+
+        log_parse_environment();
+
+        if (argc >= 2) {
+                N = argc - 1;
+                fnames = argv + 1;
+        } else {
+                _cleanup_free_ char *pkts_glob = NULL;
+                assert_se(get_testdata_dir("test-resolve/*.pkts", &pkts_glob) >= 0);
+                assert_se(glob(pkts_glob, GLOB_NOSORT, NULL, &g) == 0);
+                N = g.gl_pathc;
+                fnames = g.gl_pathv;
+        }
+
+        for (int i = 0; i < N; i++) {
+                test_packet_from_file(fnames[i], false);
+                puts("");
+                test_packet_from_file(fnames[i], true);
+                if (i + 1 < N)
+                        puts("");
+        }
+
+        test_dns_resource_record_get_cname_target();
+
+        return EXIT_SUCCESS;
+}
diff --git a/src/resolve/test-dnssec-complex.c b/src/resolve/test-dnssec-complex.c
new file mode 100644
index 0000000..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..63dcf57
--- /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, NULL) >= 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, NULL) >= 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, NULL) >= 0);
+
+        /* Validate the RR as it if was 2015-12-2 today */
+        assert_se(dnssec_verify_rrset(answer, a->key, rrsig, dnskey, 1449092754*USEC_PER_SEC, &result) >= 0);
+        assert_se(result == DNSSEC_VALIDATED);
+}
+
+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 = systemd_bitmap_new();
+        assert_se(nsec->nsec.types);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_A) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_NS) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_SOA) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_MX) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_TXT) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_RRSIG) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_NSEC) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, DNS_TYPE_DNSKEY) >= 0);
+        assert_se(systemd_bitmap_set(nsec->nsec.types, 65534) >= 0);
+
+        log_info("NSEC: %s", strna(dns_resource_record_to_string(nsec)));
+
+        rrsig = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_RRSIG, "NaSa.GOV.");
+        assert_se(rrsig);
+
+        rrsig->rrsig.type_covered = DNS_TYPE_NSEC;
+        rrsig->rrsig.algorithm = DNSSEC_ALGORITHM_RSASHA256;
+        rrsig->rrsig.labels = 2;
+        rrsig->rrsig.original_ttl = 300;
+        rrsig->rrsig.expiration = 0x5689002f;
+        rrsig->rrsig.inception = 0x56617230;
+        rrsig->rrsig.key_tag = 30390;
+        rrsig->rrsig.signer = strdup("Nasa.Gov.");
+        assert_se(rrsig->rrsig.signer);
+        rrsig->rrsig.signature_size = sizeof(signature_blob);
+        rrsig->rrsig.signature = memdup(signature_blob, rrsig->rrsig.signature_size);
+        assert_se(rrsig->rrsig.signature);
+
+        log_info("RRSIG: %s", strna(dns_resource_record_to_string(rrsig)));
+
+        dnskey = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_DNSKEY, "nASA.gOV");
+        assert_se(dnskey);
+
+        dnskey->dnskey.flags = 256;
+        dnskey->dnskey.protocol = 3;
+        dnskey->dnskey.algorithm = DNSSEC_ALGORITHM_RSASHA256;
+        dnskey->dnskey.key_size = sizeof(dnskey_blob);
+        dnskey->dnskey.key = memdup(dnskey_blob, sizeof(dnskey_blob));
+        assert_se(dnskey->dnskey.key);
+
+        log_info("DNSKEY: %s", strna(dns_resource_record_to_string(dnskey)));
+        log_info("DNSKEY keytag: %u", dnssec_keytag(dnskey, false));
+
+        assert_se(dnssec_key_match_rrsig(nsec->key, rrsig) > 0);
+        assert_se(dnssec_rrsig_match_dnskey(rrsig, dnskey, false) > 0);
+
+        answer = dns_answer_new(1);
+        assert_se(answer);
+        assert_se(dns_answer_add(answer, nsec, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0);
+
+        /* Validate the RR as it if was 2015-12-11 today */
+        assert_se(dnssec_verify_rrset(answer, nsec->key, rrsig, dnskey, 1449849318*USEC_PER_SEC, &result) >= 0);
+        assert_se(result == DNSSEC_VALIDATED);
+}
+
+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, NULL) >= 0);
+        assert_se(dns_answer_add(answer, mx2, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0);
+        assert_se(dns_answer_add(answer, mx3, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0);
+        assert_se(dns_answer_add(answer, mx4, 0, DNS_ANSWER_AUTHENTICATED, NULL) >= 0);
+
+        /* Validate the RR as it if was 2020-02-24 today */
+        assert_se(dnssec_verify_rrset(answer, mx1->key, rrsig, dnskey, 1582534685*USEC_PER_SEC, &result) >= 0);
+        assert_se(result == DNSSEC_VALIDATED);
+}
+
+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..6706885
--- /dev/null
+++ b/src/resolve/test-resolved-etc-hosts.c
@@ -0,0 +1,151 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <arpa/inet.h>
+#include <malloc.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include "fd-util.h"
+#include "fileio.h"
+#include "fs-util.h"
+#include "log.h"
+#include "resolved-etc-hosts.h"
+#include "strv.h"
+#include "tests.h"
+#include "tmpfile-util.h"
+
+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(MALLOC_ELEMENTSOF(bn->addresses) >= 3);
+        assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.4")));
+        assert_se(address_equal_4(bn->addresses[1], inet_addr("1.2.3.5")));
+        assert_se(address_equal_6(bn->addresses[2], {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5}));
+
+        assert_se(bn = hashmap_get(hosts.by_name, "dash"));
+        assert_se(bn->n_addresses == 1);
+        assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1);
+        assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.6")));
+
+        assert_se(bn = hashmap_get(hosts.by_name, "dash-dash.where-dash"));
+        assert_se(bn->n_addresses == 1);
+        assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 1);
+        assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.6")));
+
+        /* See https://tools.ietf.org/html/rfc1035#section-2.3.1 */
+        FOREACH_STRING(s, "bad-dash-", "-bad-dash", "-bad-dash.bad-")
+                assert_se(!hashmap_get(hosts.by_name, s));
+
+        assert_se(bn = hashmap_get(hosts.by_name, "before.comment"));
+        assert_se(bn->n_addresses == 4);
+        assert_se(MALLOC_ELEMENTSOF(bn->addresses) >= 4);
+        assert_se(address_equal_4(bn->addresses[0], inet_addr("1.2.3.9")));
+        assert_se(address_equal_4(bn->addresses[1], inet_addr("1.2.3.10")));
+        assert_se(address_equal_4(bn->addresses[2], inet_addr("1.2.3.11")));
+        assert_se(address_equal_4(bn->addresses[3], inet_addr("1.2.3.12")));
+
+        assert(!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(MALLOC_ELEMENTSOF(bn->addresses) >= 1);
+        assert_se(address_equal_6(bn->addresses[0], {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5}));
+
+        assert_se( set_contains(hosts.no_address, "some.where"));
+        assert_se( set_contains(hosts.no_address, "some.other"));
+        assert_se( set_contains(hosts.no_address, "deny.listed"));
+        assert_se(!set_contains(hosts.no_address, "foobar.foo.foo"));
+}
+
+static void test_parse_file(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;
+}