summaryrefslogtreecommitdiffstats
path: root/src/resolve/resolved-dns-zone.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/resolve/resolved-dns-zone.c')
-rw-r--r--src/resolve/resolved-dns-zone.c686
1 files changed, 686 insertions, 0 deletions
diff --git a/src/resolve/resolved-dns-zone.c b/src/resolve/resolved-dns-zone.c
new file mode 100644
index 0000000..f533f97
--- /dev/null
+++ b/src/resolve/resolved-dns-zone.c
@@ -0,0 +1,686 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "dns-domain.h"
+#include "list.h"
+#include "resolved-dns-packet.h"
+#include "resolved-dns-zone.h"
+#include "resolved-dnssd.h"
+#include "resolved-manager.h"
+#include "string-util.h"
+
+/* Never allow more than 1K entries */
+#define ZONE_MAX 1024
+
+void dns_zone_item_probe_stop(DnsZoneItem *i) {
+ DnsTransaction *t;
+ assert(i);
+
+ if (!i->probe_transaction)
+ return;
+
+ t = TAKE_PTR(i->probe_transaction);
+
+ set_remove(t->notify_zone_items, i);
+ set_remove(t->notify_zone_items_done, i);
+ dns_transaction_gc(t);
+}
+
+static DnsZoneItem* dns_zone_item_free(DnsZoneItem *i) {
+ if (!i)
+ return NULL;
+
+ dns_zone_item_probe_stop(i);
+ dns_resource_record_unref(i->rr);
+
+ return mfree(i);
+}
+DEFINE_TRIVIAL_CLEANUP_FUNC(DnsZoneItem*, dns_zone_item_free);
+
+static void dns_zone_item_remove_and_free(DnsZone *z, DnsZoneItem *i) {
+ DnsZoneItem *first;
+
+ assert(z);
+
+ if (!i)
+ return;
+
+ first = hashmap_get(z->by_key, i->rr->key);
+ LIST_REMOVE(by_key, first, i);
+ if (first)
+ assert_se(hashmap_replace(z->by_key, first->rr->key, first) >= 0);
+ else
+ hashmap_remove(z->by_key, i->rr->key);
+
+ first = hashmap_get(z->by_name, dns_resource_key_name(i->rr->key));
+ LIST_REMOVE(by_name, first, i);
+ if (first)
+ assert_se(hashmap_replace(z->by_name, dns_resource_key_name(first->rr->key), first) >= 0);
+ else
+ hashmap_remove(z->by_name, dns_resource_key_name(i->rr->key));
+
+ dns_zone_item_free(i);
+}
+
+void dns_zone_flush(DnsZone *z) {
+ DnsZoneItem *i;
+
+ assert(z);
+
+ while ((i = hashmap_first(z->by_key)))
+ dns_zone_item_remove_and_free(z, i);
+
+ assert(hashmap_size(z->by_key) == 0);
+ assert(hashmap_size(z->by_name) == 0);
+
+ z->by_key = hashmap_free(z->by_key);
+ z->by_name = hashmap_free(z->by_name);
+}
+
+DnsZoneItem* dns_zone_get(DnsZone *z, DnsResourceRecord *rr) {
+ assert(z);
+ assert(rr);
+
+ LIST_FOREACH(by_key, i, (DnsZoneItem*) hashmap_get(z->by_key, rr->key))
+ if (dns_resource_record_equal(i->rr, rr) > 0)
+ return i;
+
+ return NULL;
+}
+
+void dns_zone_remove_rr(DnsZone *z, DnsResourceRecord *rr) {
+ DnsZoneItem *i;
+
+ assert(z);
+
+ if (!rr)
+ return;
+
+ i = dns_zone_get(z, rr);
+ if (i)
+ dns_zone_item_remove_and_free(z, i);
+}
+
+int dns_zone_remove_rrs_by_key(DnsZone *z, DnsResourceKey *key) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL;
+ DnsResourceRecord *rr;
+ bool tentative;
+ int r;
+
+ r = dns_zone_lookup(z, key, 0, &answer, &soa, &tentative);
+ if (r < 0)
+ return r;
+
+ DNS_ANSWER_FOREACH(rr, answer)
+ dns_zone_remove_rr(z, rr);
+
+ return 0;
+}
+
+static int dns_zone_init(DnsZone *z) {
+ int r;
+
+ assert(z);
+
+ r = hashmap_ensure_allocated(&z->by_key, &dns_resource_key_hash_ops);
+ if (r < 0)
+ return r;
+
+ r = hashmap_ensure_allocated(&z->by_name, &dns_name_hash_ops);
+ if (r < 0)
+ return r;
+
+ return 0;
+}
+
+static int dns_zone_link_item(DnsZone *z, DnsZoneItem *i) {
+ DnsZoneItem *first;
+ int r;
+
+ first = hashmap_get(z->by_key, i->rr->key);
+ if (first) {
+ LIST_PREPEND(by_key, first, i);
+ assert_se(hashmap_replace(z->by_key, first->rr->key, first) >= 0);
+ } else {
+ r = hashmap_put(z->by_key, i->rr->key, i);
+ if (r < 0)
+ return r;
+ }
+
+ first = hashmap_get(z->by_name, dns_resource_key_name(i->rr->key));
+ if (first) {
+ LIST_PREPEND(by_name, first, i);
+ assert_se(hashmap_replace(z->by_name, dns_resource_key_name(first->rr->key), first) >= 0);
+ } else {
+ r = hashmap_put(z->by_name, dns_resource_key_name(i->rr->key), i);
+ if (r < 0)
+ return r;
+ }
+
+ return 0;
+}
+
+static int dns_zone_item_probe_start(DnsZoneItem *i) {
+ _cleanup_(dns_transaction_gcp) DnsTransaction *t = NULL;
+ int r;
+
+ assert(i);
+
+ if (i->probe_transaction)
+ return 0;
+
+ t = dns_scope_find_transaction(
+ i->scope,
+ &DNS_RESOURCE_KEY_CONST(i->rr->key->class, DNS_TYPE_ANY, dns_resource_key_name(i->rr->key)),
+ SD_RESOLVED_NO_CACHE|SD_RESOLVED_NO_ZONE);
+ if (!t) {
+ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+
+ key = dns_resource_key_new(i->rr->key->class, DNS_TYPE_ANY, dns_resource_key_name(i->rr->key));
+ if (!key)
+ return -ENOMEM;
+
+ r = dns_transaction_new(&t, i->scope, key, NULL, SD_RESOLVED_NO_CACHE|SD_RESOLVED_NO_ZONE);
+ if (r < 0)
+ return r;
+ }
+
+ r = set_ensure_allocated(&t->notify_zone_items_done, NULL);
+ if (r < 0)
+ return r;
+
+ r = set_ensure_put(&t->notify_zone_items, NULL, i);
+ if (r < 0)
+ return r;
+
+ t->probing = true;
+ i->probe_transaction = TAKE_PTR(t);
+
+ if (i->probe_transaction->state == DNS_TRANSACTION_NULL) {
+ i->block_ready++;
+ r = dns_transaction_go(i->probe_transaction);
+ i->block_ready--;
+
+ if (r < 0) {
+ dns_zone_item_probe_stop(i);
+ return r;
+ }
+ }
+
+ dns_zone_item_notify(i);
+ return 0;
+}
+
+int dns_zone_put(DnsZone *z, DnsScope *s, DnsResourceRecord *rr, bool probe) {
+ _cleanup_(dns_zone_item_freep) DnsZoneItem *i = NULL;
+ DnsZoneItem *existing;
+ int r;
+
+ assert(z);
+ assert(s);
+ assert(rr);
+
+ if (dns_class_is_pseudo(rr->key->class))
+ return -EINVAL;
+ if (dns_type_is_pseudo(rr->key->type))
+ return -EINVAL;
+
+ existing = dns_zone_get(z, rr);
+ if (existing)
+ return 0;
+
+ r = dns_zone_init(z);
+ if (r < 0)
+ return r;
+
+ i = new(DnsZoneItem, 1);
+ if (!i)
+ return -ENOMEM;
+
+ *i = (DnsZoneItem) {
+ .scope = s,
+ .rr = dns_resource_record_ref(rr),
+ .probing_enabled = probe,
+ };
+
+ r = dns_zone_link_item(z, i);
+ if (r < 0)
+ return r;
+
+ if (probe) {
+ bool established = false;
+
+ /* Check if there's already an RR with the same name
+ * established. If so, it has been probed already, and
+ * we don't need to probe again. */
+
+ LIST_FOREACH_OTHERS(by_name, j, i)
+ if (j->state == DNS_ZONE_ITEM_ESTABLISHED)
+ established = true;
+
+ if (established)
+ i->state = DNS_ZONE_ITEM_ESTABLISHED;
+ else {
+ i->state = DNS_ZONE_ITEM_PROBING;
+
+ r = dns_zone_item_probe_start(i);
+ if (r < 0) {
+ dns_zone_item_remove_and_free(z, i);
+ i = NULL;
+ return r;
+ }
+ }
+ } else
+ i->state = DNS_ZONE_ITEM_ESTABLISHED;
+
+ i = NULL;
+ return 0;
+}
+
+static int dns_zone_add_authenticated_answer(DnsAnswer *a, DnsZoneItem *i, int ifindex) {
+ DnsAnswerFlags flags;
+
+ /* From RFC 6762, Section 10.2
+ * "They (the rules about when to set the cache-flush bit) apply to
+ * startup announcements as described in Section 8.3, "Announcing",
+ * and to responses generated as a result of receiving query messages."
+ * So, set the cache-flush bit for mDNS answers except for DNS-SD
+ * service enumeration PTRs described in RFC 6763, Section 4.1. */
+ if (i->scope->protocol == DNS_PROTOCOL_MDNS &&
+ !dns_resource_key_is_dnssd_ptr(i->rr->key))
+ flags = DNS_ANSWER_AUTHENTICATED|DNS_ANSWER_CACHE_FLUSH;
+ else
+ flags = DNS_ANSWER_AUTHENTICATED;
+
+ return dns_answer_add(a, i->rr, ifindex, flags, NULL);
+}
+
+int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **ret_answer, DnsAnswer **ret_soa, bool *ret_tentative) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL;
+ unsigned n_answer = 0;
+ DnsZoneItem *first;
+ bool tentative = true, need_soa = false;
+ int r;
+
+ /* Note that we don't actually need the ifindex for anything. However when it is passed we'll initialize the
+ * ifindex field in the answer with it */
+
+ assert(z);
+ assert(key);
+ assert(ret_answer);
+
+ /* First iteration, count what we have */
+
+ if (key->type == DNS_TYPE_ANY || key->class == DNS_CLASS_ANY) {
+ bool found = false, added = false;
+ int k;
+
+ /* If this is a generic match, then we have to
+ * go through the list by the name and look
+ * for everything manually */
+
+ first = hashmap_get(z->by_name, dns_resource_key_name(key));
+ LIST_FOREACH(by_name, j, first) {
+ if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING))
+ continue;
+
+ found = true;
+
+ k = dns_resource_key_match_rr(key, j->rr, NULL);
+ if (k < 0)
+ return k;
+ if (k > 0) {
+ n_answer++;
+ added = true;
+ }
+
+ }
+
+ if (found && !added)
+ need_soa = true;
+
+ } else {
+ bool found = false;
+
+ /* If this is a specific match, then look for
+ * the right key immediately */
+
+ first = hashmap_get(z->by_key, key);
+ LIST_FOREACH(by_key, j, first) {
+ if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING))
+ continue;
+
+ found = true;
+ n_answer++;
+ }
+
+ if (!found) {
+ first = hashmap_get(z->by_name, dns_resource_key_name(key));
+ LIST_FOREACH(by_name, j, first) {
+ if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING))
+ continue;
+
+ need_soa = true;
+ break;
+ }
+ }
+ }
+
+ if (n_answer <= 0 && !need_soa)
+ goto return_empty;
+
+ if (n_answer > 0) {
+ answer = dns_answer_new(n_answer);
+ if (!answer)
+ return -ENOMEM;
+ }
+
+ if (need_soa) {
+ soa = dns_answer_new(1);
+ if (!soa)
+ return -ENOMEM;
+ }
+
+ /* Second iteration, actually add the RRs to the answers */
+ if (key->type == DNS_TYPE_ANY || key->class == DNS_CLASS_ANY) {
+ bool found = false, added = false;
+ int k;
+
+ first = hashmap_get(z->by_name, dns_resource_key_name(key));
+ LIST_FOREACH(by_name, j, first) {
+ if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING))
+ continue;
+
+ found = true;
+
+ if (j->state != DNS_ZONE_ITEM_PROBING)
+ tentative = false;
+
+ k = dns_resource_key_match_rr(key, j->rr, NULL);
+ if (k < 0)
+ return k;
+ if (k > 0) {
+ r = dns_zone_add_authenticated_answer(answer, j, ifindex);
+ if (r < 0)
+ return r;
+
+ added = true;
+ }
+ }
+
+ if (found && !added) {
+ r = dns_answer_add_soa(soa, dns_resource_key_name(key), LLMNR_DEFAULT_TTL, ifindex);
+ if (r < 0)
+ return r;
+ }
+ } else {
+ bool found = false;
+
+ first = hashmap_get(z->by_key, key);
+ LIST_FOREACH(by_key, j, first) {
+ if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING))
+ continue;
+
+ found = true;
+
+ if (j->state != DNS_ZONE_ITEM_PROBING)
+ tentative = false;
+
+ r = dns_zone_add_authenticated_answer(answer, j, ifindex);
+ if (r < 0)
+ return r;
+ }
+
+ if (!found) {
+ bool add_soa = false;
+
+ first = hashmap_get(z->by_name, dns_resource_key_name(key));
+ LIST_FOREACH(by_name, j, first) {
+ if (!IN_SET(j->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING))
+ continue;
+
+ if (j->state != DNS_ZONE_ITEM_PROBING)
+ tentative = false;
+
+ add_soa = true;
+ }
+
+ if (add_soa) {
+ r = dns_answer_add_soa(soa, dns_resource_key_name(key), LLMNR_DEFAULT_TTL, ifindex);
+ if (r < 0)
+ return r;
+ }
+ }
+ }
+
+ /* If the caller sets ret_tentative to NULL, then use this as
+ * indication to not return tentative entries */
+
+ if (!ret_tentative && tentative)
+ goto return_empty;
+
+ *ret_answer = TAKE_PTR(answer);
+
+ if (ret_soa)
+ *ret_soa = TAKE_PTR(soa);
+
+ if (ret_tentative)
+ *ret_tentative = tentative;
+
+ return 1;
+
+return_empty:
+ *ret_answer = NULL;
+
+ if (ret_soa)
+ *ret_soa = NULL;
+
+ if (ret_tentative)
+ *ret_tentative = false;
+
+ return 0;
+}
+
+void dns_zone_item_conflict(DnsZoneItem *i) {
+ assert(i);
+
+ if (!IN_SET(i->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_VERIFYING, DNS_ZONE_ITEM_ESTABLISHED))
+ return;
+
+ log_info("Detected conflict on %s", strna(dns_resource_record_to_string(i->rr)));
+
+ dns_zone_item_probe_stop(i);
+
+ /* Withdraw the conflict item */
+ i->state = DNS_ZONE_ITEM_WITHDRAWN;
+
+ (void) dnssd_signal_conflict(i->scope->manager, dns_resource_key_name(i->rr->key));
+
+ /* Maybe change the hostname */
+ if (manager_is_own_hostname(i->scope->manager, dns_resource_key_name(i->rr->key)) > 0)
+ manager_next_hostname(i->scope->manager);
+}
+
+void dns_zone_item_notify(DnsZoneItem *i) {
+ assert(i);
+ assert(i->probe_transaction);
+
+ if (i->block_ready > 0)
+ return;
+
+ if (IN_SET(i->probe_transaction->state, DNS_TRANSACTION_NULL, DNS_TRANSACTION_PENDING, DNS_TRANSACTION_VALIDATING))
+ return;
+
+ if (i->probe_transaction->state == DNS_TRANSACTION_SUCCESS) {
+ bool we_lost = false;
+
+ /* The probe got a successful reply. If we so far
+ * weren't established we just give up.
+ *
+ * In LLMNR case if we already
+ * were established, and the peer has the
+ * lexicographically larger IP address we continue
+ * and defend it. */
+
+ if (!IN_SET(i->state, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) {
+ log_debug("Got a successful probe for not yet established RR, we lost.");
+ we_lost = true;
+ } else if (i->probe_transaction->scope->protocol == DNS_PROTOCOL_LLMNR) {
+ assert(i->probe_transaction->received);
+ we_lost = memcmp(&i->probe_transaction->received->sender, &i->probe_transaction->received->destination, FAMILY_ADDRESS_SIZE(i->probe_transaction->received->family)) < 0;
+ if (we_lost)
+ log_debug("Got a successful probe reply for an established RR, and we have a lexicographically larger IP address and thus lost.");
+ }
+
+ if (we_lost) {
+ dns_zone_item_conflict(i);
+ return;
+ }
+
+ log_debug("Got a successful probe reply, but peer has lexicographically lower IP address and thus lost.");
+ }
+
+ log_debug("Record %s successfully probed.", strna(dns_resource_record_to_string(i->rr)));
+
+ dns_zone_item_probe_stop(i);
+ i->state = DNS_ZONE_ITEM_ESTABLISHED;
+}
+
+static int dns_zone_item_verify(DnsZoneItem *i) {
+ int r;
+
+ assert(i);
+
+ if (i->state != DNS_ZONE_ITEM_ESTABLISHED)
+ return 0;
+
+ log_debug("Verifying RR %s", strna(dns_resource_record_to_string(i->rr)));
+
+ i->state = DNS_ZONE_ITEM_VERIFYING;
+ r = dns_zone_item_probe_start(i);
+ if (r < 0) {
+ log_error_errno(r, "Failed to start probing for verifying RR: %m");
+ i->state = DNS_ZONE_ITEM_ESTABLISHED;
+ return r;
+ }
+
+ return 0;
+}
+
+int dns_zone_check_conflicts(DnsZone *zone, DnsResourceRecord *rr) {
+ DnsZoneItem *first;
+ int c = 0;
+
+ assert(zone);
+ assert(rr);
+
+ /* This checks whether a response RR we received from somebody
+ * else is one that we actually thought was uniquely ours. If
+ * so, we'll verify our RRs. */
+
+ /* No conflict if we don't have the name at all. */
+ first = hashmap_get(zone->by_name, dns_resource_key_name(rr->key));
+ if (!first)
+ return 0;
+
+ /* No conflict if we have the exact same RR */
+ if (dns_zone_get(zone, rr))
+ return 0;
+
+ /* No conflict if it is DNS-SD RR used for service enumeration. */
+ if (dns_resource_key_is_dnssd_ptr(rr->key))
+ return 0;
+
+ /* OK, somebody else has RRs for the same name. Yuck! Let's
+ * start probing again */
+
+ LIST_FOREACH(by_name, i, first) {
+ if (dns_resource_record_equal(i->rr, rr))
+ continue;
+
+ dns_zone_item_verify(i);
+ c++;
+ }
+
+ return c;
+}
+
+int dns_zone_verify_conflicts(DnsZone *zone, DnsResourceKey *key) {
+ DnsZoneItem *first;
+ int c = 0;
+
+ assert(zone);
+
+ /* Somebody else notified us about a possible conflict. Let's
+ * verify if that's true. */
+
+ first = hashmap_get(zone->by_name, dns_resource_key_name(key));
+ if (!first)
+ return 0;
+
+ LIST_FOREACH(by_name, i, first) {
+ dns_zone_item_verify(i);
+ c++;
+ }
+
+ return c;
+}
+
+void dns_zone_verify_all(DnsZone *zone) {
+ DnsZoneItem *i;
+
+ assert(zone);
+
+ HASHMAP_FOREACH(i, zone->by_key)
+ LIST_FOREACH(by_key, j, i)
+ dns_zone_item_verify(j);
+}
+
+void dns_zone_dump(DnsZone *zone, FILE *f) {
+ DnsZoneItem *i;
+
+ if (!zone)
+ return;
+
+ if (!f)
+ f = stdout;
+
+ HASHMAP_FOREACH(i, zone->by_key)
+ LIST_FOREACH(by_key, j, i) {
+ const char *t;
+
+ t = dns_resource_record_to_string(j->rr);
+ if (!t) {
+ log_oom();
+ continue;
+ }
+
+ fputc('\t', f);
+ fputs(t, f);
+ fputc('\n', f);
+ }
+}
+
+bool dns_zone_is_empty(DnsZone *zone) {
+ if (!zone)
+ return true;
+
+ return hashmap_isempty(zone->by_key);
+}
+
+bool dns_zone_contains_name(DnsZone *z, const char *name) {
+ DnsZoneItem *first;
+
+ first = hashmap_get(z->by_name, name);
+ if (!first)
+ return false;
+
+ LIST_FOREACH(by_name, i, first) {
+ if (!IN_SET(i->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING))
+ continue;
+
+ return true;
+ }
+
+ return false;
+}