systemd/src/resolve/resolved-dns-query.c
Lennart Poettering ae55c9c0ae resolved: make sure "resolvectl monitor" can properly deal with stub queries
If we receive a query via the two stubs we store the original packet
instead of just the question object. Hence when we send monitor info to
subscribed clients we need to extract its question and also include it
in the returned data.

Fixes: #29580
2023-11-01 23:00:45 +01:00

1300 lines
46 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "alloc-util.h"
#include "dns-domain.h"
#include "dns-type.h"
#include "event-util.h"
#include "glyph-util.h"
#include "hostname-util.h"
#include "local-addresses.h"
#include "resolved-dns-query.h"
#include "resolved-dns-synthesize.h"
#include "resolved-etc-hosts.h"
#include "string-util.h"
#define QUERIES_MAX 2048
#define AUXILIARY_QUERIES_MAX 64
#define CNAME_REDIRECTS_MAX 16
assert_cc(AUXILIARY_QUERIES_MAX < UINT8_MAX);
assert_cc(CNAME_REDIRECTS_MAX < UINT8_MAX);
static int dns_query_candidate_new(DnsQueryCandidate **ret, DnsQuery *q, DnsScope *s) {
DnsQueryCandidate *c;
assert(ret);
assert(q);
assert(s);
c = new(DnsQueryCandidate, 1);
if (!c)
return -ENOMEM;
*c = (DnsQueryCandidate) {
.n_ref = 1,
.query = q,
.scope = s,
};
LIST_PREPEND(candidates_by_query, q->candidates, c);
LIST_PREPEND(candidates_by_scope, s->query_candidates, c);
*ret = c;
return 0;
}
static void dns_query_candidate_stop(DnsQueryCandidate *c) {
DnsTransaction *t;
assert(c);
/* Detach all the DnsTransactions attached to this query */
while ((t = set_steal_first(c->transactions))) {
set_remove(t->notify_query_candidates, c);
set_remove(t->notify_query_candidates_done, c);
dns_transaction_gc(t);
}
}
static DnsQueryCandidate* dns_query_candidate_unlink(DnsQueryCandidate *c) {
assert(c);
/* Detach this DnsQueryCandidate from the Query and Scope objects */
if (c->query) {
LIST_REMOVE(candidates_by_query, c->query->candidates, c);
c->query = NULL;
}
if (c->scope) {
LIST_REMOVE(candidates_by_scope, c->scope->query_candidates, c);
c->scope = NULL;
}
return c;
}
static DnsQueryCandidate* dns_query_candidate_free(DnsQueryCandidate *c) {
if (!c)
return NULL;
dns_query_candidate_stop(c);
dns_query_candidate_unlink(c);
set_free(c->transactions);
dns_search_domain_unref(c->search_domain);
return mfree(c);
}
DEFINE_PUBLIC_TRIVIAL_REF_UNREF_FUNC(DnsQueryCandidate, dns_query_candidate, dns_query_candidate_free);
static int dns_query_candidate_next_search_domain(DnsQueryCandidate *c) {
DnsSearchDomain *next;
assert(c);
if (c->search_domain && c->search_domain->linked)
next = c->search_domain->domains_next;
else
next = dns_scope_get_search_domains(c->scope);
for (;;) {
if (!next) /* We hit the end of the list */
return 0;
if (!next->route_only)
break;
/* Skip over route-only domains */
next = next->domains_next;
}
dns_search_domain_unref(c->search_domain);
c->search_domain = dns_search_domain_ref(next);
return 1;
}
static int dns_query_candidate_add_transaction(
DnsQueryCandidate *c,
DnsResourceKey *key,
DnsPacket *bypass) {
_cleanup_(dns_transaction_gcp) DnsTransaction *t = NULL;
int r;
assert(c);
assert(c->query); /* We shan't add transactions to a candidate that has been detached already */
if (key) {
/* Regular lookup with a resource key */
assert(!bypass);
t = dns_scope_find_transaction(c->scope, key, c->query->flags);
if (!t) {
r = dns_transaction_new(&t, c->scope, key, NULL, c->query->flags);
if (r < 0)
return r;
} else if (set_contains(c->transactions, t))
return 0;
} else {
/* "Bypass" lookup with a query packet */
assert(bypass);
r = dns_transaction_new(&t, c->scope, NULL, bypass, c->query->flags);
if (r < 0)
return r;
}
r = set_ensure_allocated(&t->notify_query_candidates_done, NULL);
if (r < 0)
return r;
r = set_ensure_put(&t->notify_query_candidates, NULL, c);
if (r < 0)
return r;
r = set_ensure_put(&c->transactions, NULL, t);
if (r < 0) {
(void) set_remove(t->notify_query_candidates, c);
return r;
}
TAKE_PTR(t);
return 1;
}
static int dns_query_candidate_go(DnsQueryCandidate *c) {
_unused_ _cleanup_(dns_query_candidate_unrefp) DnsQueryCandidate *keep_c = NULL;
DnsTransaction *t;
int r;
unsigned n = 0;
assert(c);
/* Let's keep a reference to the query while we're operating */
keep_c = dns_query_candidate_ref(c);
/* Start the transactions that are not started yet */
SET_FOREACH(t, c->transactions) {
if (t->state != DNS_TRANSACTION_NULL)
continue;
r = dns_transaction_go(t);
if (r < 0)
return r;
n++;
}
/* If there was nothing to start, then let's proceed immediately */
if (n == 0)
dns_query_candidate_notify(c);
return 0;
}
static DnsTransactionState dns_query_candidate_state(DnsQueryCandidate *c) {
DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS;
DnsTransaction *t;
assert(c);
if (c->error_code != 0)
return DNS_TRANSACTION_ERRNO;
SET_FOREACH(t, c->transactions)
switch (t->state) {
case DNS_TRANSACTION_NULL:
/* If there's a NULL transaction pending, then
* this means not all transactions where
* started yet, and we were called from within
* the stackframe that is supposed to start
* remaining transactions. In this case,
* simply claim the candidate is pending. */
case DNS_TRANSACTION_PENDING:
case DNS_TRANSACTION_VALIDATING:
/* If there's one transaction currently in
* VALIDATING state, then this means there's
* also one in PENDING state, hence we can
* return PENDING immediately. */
return DNS_TRANSACTION_PENDING;
case DNS_TRANSACTION_SUCCESS:
state = t->state;
break;
default:
if (state != DNS_TRANSACTION_SUCCESS)
state = t->state;
break;
}
return state;
}
static int dns_query_candidate_setup_transactions(DnsQueryCandidate *c) {
DnsQuestion *question;
DnsResourceKey *key;
int n = 0, r;
assert(c);
assert(c->query); /* We shan't add transactions to a candidate that has been detached already */
dns_query_candidate_stop(c);
if (c->query->question_bypass) {
/* If this is a bypass query, then pass the original query packet along to the transaction */
assert(dns_question_size(c->query->question_bypass->question) == 1);
if (!dns_scope_good_key(c->scope, dns_question_first_key(c->query->question_bypass->question)))
return 0;
r = dns_query_candidate_add_transaction(c, NULL, c->query->question_bypass);
if (r < 0)
goto fail;
return 1;
}
question = dns_query_question_for_protocol(c->query, c->scope->protocol);
/* Create one transaction per question key */
DNS_QUESTION_FOREACH(key, question) {
_cleanup_(dns_resource_key_unrefp) DnsResourceKey *new_key = NULL;
DnsResourceKey *qkey;
if (c->search_domain) {
r = dns_resource_key_new_append_suffix(&new_key, key, c->search_domain->name);
if (r < 0)
goto fail;
qkey = new_key;
} else
qkey = key;
if (!dns_scope_good_key(c->scope, qkey))
continue;
r = dns_query_candidate_add_transaction(c, qkey, NULL);
if (r < 0)
goto fail;
n++;
}
return n;
fail:
dns_query_candidate_stop(c);
return r;
}
void dns_query_candidate_notify(DnsQueryCandidate *c) {
DnsTransactionState state;
int r;
assert(c);
if (!c->query) /* This candidate has been abandoned, do nothing. */
return;
state = dns_query_candidate_state(c);
if (DNS_TRANSACTION_IS_LIVE(state))
return;
if (state != DNS_TRANSACTION_SUCCESS && c->search_domain) {
r = dns_query_candidate_next_search_domain(c);
if (r < 0)
goto fail;
if (r > 0) {
/* OK, there's another search domain to try, let's do so. */
r = dns_query_candidate_setup_transactions(c);
if (r < 0)
goto fail;
if (r > 0) {
/* New transactions where queued. Start them and wait */
r = dns_query_candidate_go(c);
if (r < 0)
goto fail;
return;
}
}
}
dns_query_ready(c->query);
return;
fail:
c->error_code = log_warning_errno(r, "Failed to follow search domains: %m");
dns_query_ready(c->query);
}
static void dns_query_stop(DnsQuery *q) {
assert(q);
event_source_disable(q->timeout_event_source);
LIST_FOREACH(candidates_by_query, c, q->candidates)
dns_query_candidate_stop(c);
}
static void dns_query_unlink_candidates(DnsQuery *q) {
assert(q);
while (q->candidates)
/* Here we drop *our* references to each of the candidates. If we had the only reference, the
* DnsQueryCandidate object will be freed. */
dns_query_candidate_unref(dns_query_candidate_unlink(q->candidates));
}
static void dns_query_reset_answer(DnsQuery *q) {
assert(q);
q->answer = dns_answer_unref(q->answer);
q->answer_rcode = 0;
q->answer_dnssec_result = _DNSSEC_RESULT_INVALID;
q->answer_errno = 0;
q->answer_query_flags = 0;
q->answer_protocol = _DNS_PROTOCOL_INVALID;
q->answer_family = AF_UNSPEC;
q->answer_search_domain = dns_search_domain_unref(q->answer_search_domain);
q->answer_full_packet = dns_packet_unref(q->answer_full_packet);
}
DnsQuery *dns_query_free(DnsQuery *q) {
if (!q)
return NULL;
q->timeout_event_source = sd_event_source_disable_unref(q->timeout_event_source);
while (q->auxiliary_queries)
dns_query_free(q->auxiliary_queries);
if (q->auxiliary_for) {
assert(q->auxiliary_for->n_auxiliary_queries > 0);
q->auxiliary_for->n_auxiliary_queries--;
LIST_REMOVE(auxiliary_queries, q->auxiliary_for->auxiliary_queries, q);
}
dns_query_unlink_candidates(q);
dns_question_unref(q->question_idna);
dns_question_unref(q->question_utf8);
dns_packet_unref(q->question_bypass);
dns_question_unref(q->collected_questions);
dns_query_reset_answer(q);
sd_bus_message_unref(q->bus_request);
sd_bus_track_unref(q->bus_track);
if (q->varlink_request) {
varlink_set_userdata(q->varlink_request, NULL);
varlink_unref(q->varlink_request);
}
if (q->request_packet)
hashmap_remove_value(q->stub_listener_extra ?
q->stub_listener_extra->queries_by_packet :
q->manager->stub_queries_by_packet,
q->request_packet,
q);
dns_packet_unref(q->request_packet);
dns_answer_unref(q->reply_answer);
dns_answer_unref(q->reply_authoritative);
dns_answer_unref(q->reply_additional);
if (q->request_stream) {
/* Detach the stream from our query, in case something else keeps a reference to it. */
(void) set_remove(q->request_stream->queries, q);
q->request_stream = dns_stream_unref(q->request_stream);
}
free(q->request_address_string);
if (q->manager) {
LIST_REMOVE(queries, q->manager->dns_queries, q);
q->manager->n_dns_queries--;
}
return mfree(q);
}
int dns_query_new(
Manager *m,
DnsQuery **ret,
DnsQuestion *question_utf8,
DnsQuestion *question_idna,
DnsPacket *question_bypass,
int ifindex,
uint64_t flags) {
_cleanup_(dns_query_freep) DnsQuery *q = NULL;
char key_str[DNS_RESOURCE_KEY_STRING_MAX];
DnsResourceKey *key;
int r;
assert(m);
if (question_bypass) {
/* It's either a "bypass" query, or a regular one, but can't be both. */
if (question_utf8 || question_idna)
return -EINVAL;
} else {
bool good = false;
/* This (primarily) checks two things:
*
* 1. That the question is not empty
* 2. That all RR keys in the question objects are for the same domain
*
* Or in other words, a single DnsQuery object may be used to look up A+AAAA combination for
* the same domain name, or SRV+TXT (for DNS-SD services), but not for unrelated lookups. */
if (dns_question_size(question_utf8) > 0) {
r = dns_question_is_valid_for_query(question_utf8);
if (r < 0)
return r;
if (r == 0)
return -EINVAL;
good = true;
}
/* If the IDNA and UTF8 questions are the same, merge their references */
r = dns_question_is_equal(question_idna, question_utf8);
if (r < 0)
return r;
if (r > 0)
question_idna = question_utf8;
else {
if (dns_question_size(question_idna) > 0) {
r = dns_question_is_valid_for_query(question_idna);
if (r < 0)
return r;
if (r == 0)
return -EINVAL;
good = true;
}
}
if (!good) /* don't allow empty queries */
return -EINVAL;
}
if (m->n_dns_queries >= QUERIES_MAX)
return -EBUSY;
q = new(DnsQuery, 1);
if (!q)
return -ENOMEM;
*q = (DnsQuery) {
.question_utf8 = dns_question_ref(question_utf8),
.question_idna = dns_question_ref(question_idna),
.question_bypass = dns_packet_ref(question_bypass),
.ifindex = ifindex,
.flags = flags,
.answer_dnssec_result = _DNSSEC_RESULT_INVALID,
.answer_protocol = _DNS_PROTOCOL_INVALID,
.answer_family = AF_UNSPEC,
};
if (question_bypass) {
DNS_QUESTION_FOREACH(key, question_bypass->question)
log_debug("Looking up bypass packet for %s.",
dns_resource_key_to_string(key, key_str, sizeof key_str));
} else {
/* First dump UTF8 question */
DNS_QUESTION_FOREACH(key, question_utf8)
log_debug("Looking up RR for %s.",
dns_resource_key_to_string(key, key_str, sizeof key_str));
/* And then dump the IDNA question, but only what hasn't been dumped already through the UTF8 question. */
DNS_QUESTION_FOREACH(key, question_idna) {
r = dns_question_contains_key(question_utf8, key);
if (r < 0)
return r;
if (r > 0)
continue;
log_debug("Looking up IDNA RR for %s.",
dns_resource_key_to_string(key, key_str, sizeof key_str));
}
}
LIST_PREPEND(queries, m->dns_queries, q);
m->n_dns_queries++;
q->manager = m;
if (ret)
*ret = q;
TAKE_PTR(q);
return 0;
}
int dns_query_make_auxiliary(DnsQuery *q, DnsQuery *auxiliary_for) {
assert(q);
assert(auxiliary_for);
/* Ensure that the query is not auxiliary yet, and
* nothing else is auxiliary to it either */
assert(!q->auxiliary_for);
assert(!q->auxiliary_queries);
/* Ensure that the unit we shall be made auxiliary for isn't
* auxiliary itself */
assert(!auxiliary_for->auxiliary_for);
if (auxiliary_for->n_auxiliary_queries >= AUXILIARY_QUERIES_MAX)
return -EAGAIN;
LIST_PREPEND(auxiliary_queries, auxiliary_for->auxiliary_queries, q);
q->auxiliary_for = auxiliary_for;
auxiliary_for->n_auxiliary_queries++;
return 0;
}
void dns_query_complete(DnsQuery *q, DnsTransactionState state) {
assert(q);
assert(!DNS_TRANSACTION_IS_LIVE(state));
assert(DNS_TRANSACTION_IS_LIVE(q->state));
/* Note that this call might invalidate the query. Callers should hence not attempt to access the
* query or transaction after calling this function. */
q->state = state;
(void) manager_monitor_send(q->manager, q->state, q->answer_rcode, q->answer_errno, q->question_idna, q->question_utf8, q->question_bypass, q->collected_questions, q->answer);
dns_query_stop(q);
if (q->complete)
q->complete(q);
}
static int on_query_timeout(sd_event_source *s, usec_t usec, void *userdata) {
DnsQuery *q = ASSERT_PTR(userdata);
assert(s);
dns_query_complete(q, DNS_TRANSACTION_TIMEOUT);
return 0;
}
static int dns_query_add_candidate(DnsQuery *q, DnsScope *s) {
_cleanup_(dns_query_candidate_unrefp) DnsQueryCandidate *c = NULL;
int r;
assert(q);
assert(s);
r = dns_query_candidate_new(&c, q, s);
if (r < 0)
return r;
/* If this a single-label domain on DNS, we might append a suitable search domain first. */
if (!FLAGS_SET(q->flags, SD_RESOLVED_NO_SEARCH) &&
dns_scope_name_wants_search_domain(s, dns_question_first_name(q->question_idna))) {
/* OK, we want a search domain now. Let's find one for this scope */
r = dns_query_candidate_next_search_domain(c);
if (r < 0)
return r;
}
r = dns_query_candidate_setup_transactions(c);
if (r < 0)
return r;
TAKE_PTR(c);
return 0;
}
static int dns_query_synthesize_reply(DnsQuery *q, DnsTransactionState *state) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
int r;
assert(q);
assert(state);
/* Tries to synthesize localhost RR replies (and others) where appropriate. Note that this is done *after* the
* the normal lookup finished. The data from the network hence takes precedence over the data we
* synthesize. (But note that many scopes refuse to resolve certain domain names) */
if (!IN_SET(*state,
DNS_TRANSACTION_RCODE_FAILURE,
DNS_TRANSACTION_NO_SERVERS,
DNS_TRANSACTION_TIMEOUT,
DNS_TRANSACTION_ATTEMPTS_MAX_REACHED,
DNS_TRANSACTION_NETWORK_DOWN,
DNS_TRANSACTION_NOT_FOUND))
return 0;
if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE))
return 0;
r = dns_synthesize_answer(
q->manager,
q->question_bypass ? q->question_bypass->question : q->question_utf8,
q->ifindex,
&answer);
if (r == -ENXIO) {
/* If we get ENXIO this tells us to generate NXDOMAIN unconditionally. */
dns_query_reset_answer(q);
q->answer_rcode = DNS_RCODE_NXDOMAIN;
q->answer_protocol = dns_synthesize_protocol(q->flags);
q->answer_family = dns_synthesize_family(q->flags);
q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
*state = DNS_TRANSACTION_RCODE_FAILURE;
return 0;
}
if (r <= 0)
return r;
dns_query_reset_answer(q);
q->answer = TAKE_PTR(answer);
q->answer_rcode = DNS_RCODE_SUCCESS;
q->answer_protocol = dns_synthesize_protocol(q->flags);
q->answer_family = dns_synthesize_family(q->flags);
q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
*state = DNS_TRANSACTION_SUCCESS;
return 1;
}
static int dns_query_try_etc_hosts(DnsQuery *q) {
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
int r;
assert(q);
/* Looks in /etc/hosts for matching entries. Note that this is done *before* the normal lookup is
* done. The data from /etc/hosts hence takes precedence over the network. */
if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE))
return 0;
r = manager_etc_hosts_lookup(
q->manager,
q->question_bypass ? q->question_bypass->question : q->question_utf8,
&answer);
if (r <= 0)
return r;
dns_query_reset_answer(q);
q->answer = TAKE_PTR(answer);
q->answer_rcode = DNS_RCODE_SUCCESS;
q->answer_protocol = dns_synthesize_protocol(q->flags);
q->answer_family = dns_synthesize_family(q->flags);
q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
return 1;
}
int dns_query_go(DnsQuery *q) {
DnsScopeMatch found = DNS_SCOPE_NO;
DnsScope *first = NULL;
int r;
assert(q);
if (q->state != DNS_TRANSACTION_NULL)
return 0;
r = dns_query_try_etc_hosts(q);
if (r < 0)
return r;
if (r > 0) {
dns_query_complete(q, DNS_TRANSACTION_SUCCESS);
return 1;
}
LIST_FOREACH(scopes, s, q->manager->dns_scopes) {
DnsScopeMatch match;
match = dns_scope_good_domain(s, q);
assert(match >= 0);
if (match > found) { /* Does this match better? If so, remember how well it matched, and the first one
* that matches this well */
found = match;
first = s;
}
}
if (found == DNS_SCOPE_NO) {
DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS;
r = dns_query_synthesize_reply(q, &state);
if (r < 0)
return r;
dns_query_complete(q, state);
return 1;
}
r = dns_query_add_candidate(q, first);
if (r < 0)
goto fail;
LIST_FOREACH(scopes, s, first->scopes_next) {
DnsScopeMatch match;
match = dns_scope_good_domain(s, q);
assert(match >= 0);
if (match < found)
continue;
r = dns_query_add_candidate(q, s);
if (r < 0)
goto fail;
}
dns_query_reset_answer(q);
r = event_reset_time_relative(
q->manager->event,
&q->timeout_event_source,
CLOCK_BOOTTIME,
SD_RESOLVED_QUERY_TIMEOUT_USEC,
0, on_query_timeout, q,
0, "query-timeout", true);
if (r < 0)
goto fail;
q->state = DNS_TRANSACTION_PENDING;
q->block_ready++;
/* Start the transactions */
LIST_FOREACH(candidates_by_query, c, q->candidates) {
r = dns_query_candidate_go(c);
if (r < 0) {
q->block_ready--;
goto fail;
}
}
q->block_ready--;
dns_query_ready(q);
return 1;
fail:
dns_query_stop(q);
return r;
}
static void dns_query_accept(DnsQuery *q, DnsQueryCandidate *c) {
DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS;
bool has_authenticated = false, has_non_authenticated = false, has_confidential = false, has_non_confidential = false;
DnssecResult dnssec_result_authenticated = _DNSSEC_RESULT_INVALID, dnssec_result_non_authenticated = _DNSSEC_RESULT_INVALID;
DnsTransaction *t;
int r;
assert(q);
if (!c) {
r = dns_query_synthesize_reply(q, &state);
if (r < 0)
goto fail;
dns_query_complete(q, state);
return;
}
if (c->error_code != 0) {
/* If the candidate had an error condition of its own, start with that. */
state = DNS_TRANSACTION_ERRNO;
q->answer = dns_answer_unref(q->answer);
q->answer_rcode = 0;
q->answer_dnssec_result = _DNSSEC_RESULT_INVALID;
q->answer_query_flags = 0;
q->answer_errno = c->error_code;
q->answer_full_packet = dns_packet_unref(q->answer_full_packet);
}
SET_FOREACH(t, c->transactions) {
switch (t->state) {
case DNS_TRANSACTION_SUCCESS: {
/* We found a successful reply, merge it into the answer */
if (state == DNS_TRANSACTION_SUCCESS) {
r = dns_answer_extend(&q->answer, t->answer);
if (r < 0)
goto fail;
q->answer_query_flags |= dns_transaction_source_to_query_flags(t->answer_source);
} else {
/* Override non-successful previous answers */
DNS_ANSWER_REPLACE(q->answer, dns_answer_ref(t->answer));
q->answer_query_flags = dns_transaction_source_to_query_flags(t->answer_source);
}
q->answer_rcode = t->answer_rcode;
q->answer_errno = 0;
DNS_PACKET_REPLACE(q->answer_full_packet, dns_packet_ref(t->received));
if (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED)) {
has_authenticated = true;
dnssec_result_authenticated = t->answer_dnssec_result;
} else {
has_non_authenticated = true;
dnssec_result_non_authenticated = t->answer_dnssec_result;
}
if (FLAGS_SET(t->answer_query_flags, SD_RESOLVED_CONFIDENTIAL))
has_confidential = true;
else
has_non_confidential = true;
state = DNS_TRANSACTION_SUCCESS;
break;
}
case DNS_TRANSACTION_NULL:
case DNS_TRANSACTION_PENDING:
case DNS_TRANSACTION_VALIDATING:
case DNS_TRANSACTION_ABORTED:
/* Ignore transactions that didn't complete */
continue;
default:
/* Any kind of failure? Store the data away, if there's nothing stored yet. */
if (state == DNS_TRANSACTION_SUCCESS)
continue;
/* If there's already an authenticated negative reply stored, then prefer that over any unauthenticated one */
if (FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) &&
!FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
continue;
DNS_ANSWER_REPLACE(q->answer, dns_answer_ref(t->answer));
q->answer_rcode = t->answer_rcode;
q->answer_dnssec_result = t->answer_dnssec_result;
q->answer_query_flags = t->answer_query_flags | dns_transaction_source_to_query_flags(t->answer_source);
q->answer_errno = t->answer_errno;
DNS_PACKET_REPLACE(q->answer_full_packet, dns_packet_ref(t->received));
state = t->state;
break;
}
}
if (state == DNS_TRANSACTION_SUCCESS) {
SET_FLAG(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED, has_authenticated && !has_non_authenticated);
SET_FLAG(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL, has_confidential && !has_non_confidential);
q->answer_dnssec_result = FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) ? dnssec_result_authenticated : dnssec_result_non_authenticated;
}
q->answer_protocol = c->scope->protocol;
q->answer_family = c->scope->family;
dns_search_domain_unref(q->answer_search_domain);
q->answer_search_domain = dns_search_domain_ref(c->search_domain);
r = dns_query_synthesize_reply(q, &state);
if (r < 0)
goto fail;
dns_query_complete(q, state);
return;
fail:
q->answer_errno = -r;
dns_query_complete(q, DNS_TRANSACTION_ERRNO);
}
void dns_query_ready(DnsQuery *q) {
DnsQueryCandidate *bad = NULL;
bool pending = false;
assert(q);
assert(DNS_TRANSACTION_IS_LIVE(q->state));
/* Note that this call might invalidate the query. Callers
* should hence not attempt to access the query or transaction
* after calling this function, unless the block_ready
* counter was explicitly bumped before doing so. */
if (q->block_ready > 0)
return;
LIST_FOREACH(candidates_by_query, c, q->candidates) {
DnsTransactionState state;
state = dns_query_candidate_state(c);
switch (state) {
case DNS_TRANSACTION_SUCCESS:
/* One of the candidates is successful,
* let's use it, and copy its data out */
dns_query_accept(q, c);
return;
case DNS_TRANSACTION_NULL:
case DNS_TRANSACTION_PENDING:
case DNS_TRANSACTION_VALIDATING:
/* One of the candidates is still going on,
* let's maybe wait for it */
pending = true;
break;
default:
/* Any kind of failure */
bad = c;
break;
}
}
if (pending)
return;
dns_query_accept(q, bad);
}
static int dns_query_collect_question(DnsQuery *q, DnsQuestion *question) {
_cleanup_(dns_question_unrefp) DnsQuestion *merged = NULL;
int r;
assert(q);
if (dns_question_size(question) == 0)
return 0;
/* When redirecting, save the first element in the chain, for informational purposes when monitoring */
r = dns_question_merge(q->collected_questions, question, &merged);
if (r < 0)
return r;
dns_question_unref(q->collected_questions);
q->collected_questions = TAKE_PTR(merged);
return 0;
}
static int dns_query_cname_redirect(DnsQuery *q, const DnsResourceRecord *cname) {
_cleanup_(dns_question_unrefp) DnsQuestion *nq_idna = NULL, *nq_utf8 = NULL;
int r, k;
assert(q);
if (q->n_cname_redirects >= CNAME_REDIRECTS_MAX)
return -ELOOP;
q->n_cname_redirects++;
r = dns_question_cname_redirect(q->question_idna, cname, &nq_idna);
if (r < 0)
return r;
if (r > 0)
log_debug("Following CNAME/DNAME %s %s %s.",
dns_question_first_name(q->question_idna),
special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
dns_question_first_name(nq_idna));
k = dns_question_is_equal(q->question_idna, q->question_utf8);
if (k < 0)
return k;
if (k > 0) {
/* Same question? Shortcut new question generation */
nq_utf8 = dns_question_ref(nq_idna);
k = r;
} else {
k = dns_question_cname_redirect(q->question_utf8, cname, &nq_utf8);
if (k < 0)
return k;
if (k > 0)
log_debug("Following UTF8 CNAME/DNAME %s %s %s.",
dns_question_first_name(q->question_utf8),
special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
dns_question_first_name(nq_utf8));
}
if (r == 0 && k == 0) /* No actual cname happened? */
return -ELOOP;
if (q->answer_protocol == DNS_PROTOCOL_DNS)
/* Don't permit CNAME redirects from unicast DNS to LLMNR or MulticastDNS, so that global resources
* cannot invade the local namespace. The opposite way we permit: local names may redirect to global
* ones. */
q->flags &= ~(SD_RESOLVED_LLMNR|SD_RESOLVED_MDNS); /* mask away the local protocols */
/* Turn off searching for the new name */
q->flags |= SD_RESOLVED_NO_SEARCH;
r = dns_query_collect_question(q, q->question_idna);
if (r < 0)
return r;
r = dns_query_collect_question(q, q->question_utf8);
if (r < 0)
return r;
/* Install the redirected question */
dns_question_unref(q->question_idna);
q->question_idna = TAKE_PTR(nq_idna);
dns_question_unref(q->question_utf8);
q->question_utf8 = TAKE_PTR(nq_utf8);
dns_query_unlink_candidates(q);
/* Note that we do *not* reset the answer here, because the answer we previously got might already
* include everything we need, let's check that first */
q->state = DNS_TRANSACTION_NULL;
return 0;
}
int dns_query_process_cname_one(DnsQuery *q) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *cname = NULL;
DnsQuestion *question;
DnsResourceRecord *rr;
bool full_match = true;
DnsResourceKey *k;
int r;
assert(q);
/* Processes a CNAME redirect if there's one. Returns one of three values:
*
* CNAME_QUERY_MATCH → direct RR match, caller should just use the RRs in this answer (and not
* bother with any CNAME/DNAME stuff)
*
* CNAME_QUERY_NOMATCH → no match at all, neither direct nor CNAME/DNAME, caller might decide to
* restart query or take things as NODATA reply.
*
* CNAME_QUERY_CNAME → no direct RR match, but a CNAME/DNAME match that we now followed for one step.
*
* The function might also return a failure, in particular -ELOOP if we encountered too many
* CNAMEs/DNAMEs in a chain or if following CNAMEs/DNAMEs was turned off.
*
* Note that this function doesn't actually restart the query. The caller can decide to do that in
* case of CNAME_QUERY_CNAME, though. */
if (!IN_SET(q->state, DNS_TRANSACTION_SUCCESS, DNS_TRANSACTION_NULL))
return DNS_QUERY_NOMATCH;
question = dns_query_question_for_protocol(q, q->answer_protocol);
/* Small reminder: our question will consist of one or more RR keys that match in name, but not in
* record type. Specifically, when we do an address lookup the question will typically consist of one
* A and one AAAA key lookup for the same domain name. When we get a response from a server we need
* to check if the answer answers all our questions to use it. Note that a response of CNAME/DNAME
* can answer both an A and the AAAA question for us, but an A/AAAA response only the relevant
* type.
*
* Hence we first check of the answers we collected are sufficient to answer all our questions
* directly. If one question wasn't answered we go on, waiting for more replies. However, if there's
* a CNAME/DNAME response we use it, and redirect to it, regardless if it was a response to the A or
* the AAAA query. */
DNS_QUESTION_FOREACH(k, question) {
bool match = false;
DNS_ANSWER_FOREACH(rr, q->answer) {
r = dns_resource_key_match_rr(k, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
if (r < 0)
return r;
if (r > 0) {
match = true; /* Yay, we found an RR that matches the key we are looking for */
break;
}
}
if (!match) {
/* Hmm. :-( there's no response for this key. This doesn't match. */
full_match = false;
break;
}
}
if (full_match)
return DNS_QUERY_MATCH; /* The answer can answer our question in full, no need to follow CNAMEs/DNAMEs */
/* Let's see if there is a CNAME/DNAME to match. This case is simpler: we accept the CNAME/DNAME that
* matches any of our questions. */
DNS_ANSWER_FOREACH(rr, q->answer) {
r = dns_question_matches_cname_or_dname(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
if (r < 0)
return r;
if (r > 0 && !cname)
cname = dns_resource_record_ref(rr);
}
if (!cname)
return DNS_QUERY_NOMATCH; /* No match and no CNAME/DNAME to follow */
if (q->flags & SD_RESOLVED_NO_CNAME)
return -ELOOP;
if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED))
q->previous_redirect_unauthenticated = true;
if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL))
q->previous_redirect_non_confidential = true;
if (!FLAGS_SET(q->answer_query_flags, SD_RESOLVED_SYNTHETIC))
q->previous_redirect_non_synthetic = true;
/* OK, let's actually follow the CNAME */
r = dns_query_cname_redirect(q, cname);
if (r < 0)
return r;
return DNS_QUERY_CNAME; /* Tell caller that we did a single CNAME/DNAME redirection step */
}
int dns_query_process_cname_many(DnsQuery *q) {
int r;
assert(q);
/* Follows CNAMEs through the current packet: as long as the current packet can fulfill our
* redirected CNAME queries we keep going, and restart the query once the current packet isn't good
* enough anymore. It's a wrapper around dns_query_process_cname_one() and returns the same values,
* but with extended semantics. Specifically:
*
* DNS_QUERY_MATCH → as above
*
* DNS_QUERY_CNAME → we ran into a CNAME/DNAME redirect that we could not answer from the current
* message, and thus restarted the query to resolve it.
*
* DNS_QUERY_NOMATCH → we reached the end of CNAME/DNAME chain, and there are no direct matches nor a
* CNAME/DNAME match. i.e. this is a NODATA case.
*
* Note that this function will restart the query for the caller if needed, and that's the case
* DNS_QUERY_CNAME is returned.
*/
r = dns_query_process_cname_one(q);
if (r != DNS_QUERY_CNAME)
return r; /* The first redirect is special: if it doesn't answer the question that's no
* reason to restart the query, we just accept this as a NODATA answer. */
for (;;) {
r = dns_query_process_cname_one(q);
if (r < 0 || r == DNS_QUERY_MATCH)
return r;
if (r == DNS_QUERY_NOMATCH) {
/* OK, so we followed one or more CNAME/DNAME RR but the existing packet can't answer
* this. Let's restart the query hence, with the new question. Why the different
* handling than the first chain element? Because if the server answers a direct
* question with an empty answer then this is a NODATA response. But if it responds
* with a CNAME chain that ultimately is incomplete (i.e. a non-empty but truncated
* CNAME chain) then we better follow up ourselves and ask for the rest of the
* chain. This is particular relevant since our cache will store CNAME/DNAME
* redirects that we learnt about for lookups of certain DNS types, but later on we
* can reuse this data even for other DNS types, but in that case need to follow up
* with the final lookup of the chain ourselves with the RR type we ourselves are
* interested in. */
r = dns_query_go(q);
if (r < 0)
return r;
return DNS_QUERY_CNAME;
}
/* So we found a CNAME that the existing packet already answers, again via a CNAME, let's
* continue going then. */
assert(r == DNS_QUERY_CNAME);
}
}
DnsQuestion* dns_query_question_for_protocol(DnsQuery *q, DnsProtocol protocol) {
assert(q);
if (q->question_bypass)
return q->question_bypass->question;
switch (protocol) {
case DNS_PROTOCOL_DNS:
return q->question_idna;
case DNS_PROTOCOL_MDNS:
case DNS_PROTOCOL_LLMNR:
return q->question_utf8;
default:
return NULL;
}
}
const char *dns_query_string(DnsQuery *q) {
const char *name;
int r;
/* Returns a somewhat useful human-readable lookup key string for this query */
if (q->question_bypass)
return dns_question_first_name(q->question_bypass->question);
if (q->request_address_string)
return q->request_address_string;
if (q->request_address_valid) {
r = in_addr_to_string(q->request_family, &q->request_address, &q->request_address_string);
if (r >= 0)
return q->request_address_string;
}
name = dns_question_first_name(q->question_utf8);
if (name)
return name;
return dns_question_first_name(q->question_idna);
}
bool dns_query_fully_authenticated(DnsQuery *q) {
assert(q);
return FLAGS_SET(q->answer_query_flags, SD_RESOLVED_AUTHENTICATED) && !q->previous_redirect_unauthenticated;
}
bool dns_query_fully_confidential(DnsQuery *q) {
assert(q);
return FLAGS_SET(q->answer_query_flags, SD_RESOLVED_CONFIDENTIAL) && !q->previous_redirect_non_confidential;
}
bool dns_query_fully_authoritative(DnsQuery *q) {
assert(q);
/* We are authoritative for everything synthetic (except if a previous CNAME/DNAME) wasn't
* synthetic. (Note: SD_RESOLVED_SYNTHETIC is reset on each CNAME/DNAME, hence the explicit check for
* previous synthetic DNAME/CNAME redirections.) */
if ((q->answer_query_flags & SD_RESOLVED_SYNTHETIC) && !q->previous_redirect_non_synthetic)
return true;
/* We are also authoritative for everything coming only from the trust anchor and the local
* zones. (Note: the SD_RESOLVED_FROM_xyz flags we merge on each redirect, hence no need to
* explicitly check previous redirects here.) */
return (q->answer_query_flags & SD_RESOLVED_FROM_MASK & ~(SD_RESOLVED_FROM_TRUST_ANCHOR | SD_RESOLVED_FROM_ZONE)) == 0;
}