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