378 lines
12 KiB
C
378 lines
12 KiB
C
/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
#include "lib/selection_iter.h"
|
|
#include "lib/selection.h"
|
|
|
|
#include "lib/generic/trie.h"
|
|
#include "lib/generic/pack.h"
|
|
#include "lib/zonecut.h"
|
|
#include "lib/resolve.h"
|
|
|
|
#define VERBOSE_MSG(qry, ...) kr_log_q((qry), SELECTION, __VA_ARGS__)
|
|
|
|
/// To be held per query and locally. Allocations are in the kr_request's mempool.
|
|
struct iter_local_state {
|
|
trie_t *names; /// knot_dname_t -> struct iter_name_state *
|
|
trie_t *addresses; /// IP address -> struct address_state *
|
|
knot_dname_t *zonecut;
|
|
/** Used to distinguish old and valid records in tries. */
|
|
unsigned int generation;
|
|
enum kr_selection_error last_error;
|
|
unsigned int no_ns_addr_count;
|
|
};
|
|
|
|
enum record_state { RECORD_UNKNOWN, RECORD_RESOLVED, RECORD_TRIED };
|
|
|
|
// To be held per NS name and locally
|
|
struct iter_name_state {
|
|
unsigned int generation;
|
|
enum record_state a_state;
|
|
enum record_state aaaa_state;
|
|
};
|
|
|
|
void iter_local_state_alloc(struct knot_mm *mm, void **local_state)
|
|
{
|
|
*local_state = mm_calloc(mm, 1, sizeof(struct iter_local_state));
|
|
}
|
|
|
|
static struct address_state *get_address_state(struct iter_local_state *local_state,
|
|
const struct kr_transport *transport)
|
|
{
|
|
if (!transport) {
|
|
return NULL;
|
|
}
|
|
|
|
uint8_t *address = ip_to_bytes(&transport->address, transport->address_len);
|
|
trie_val_t *address_state = trie_get_try(local_state->addresses, (char *)address,
|
|
transport->address_len);
|
|
if (!address_state) {
|
|
kr_assert(transport->deduplicated);
|
|
/* Transport was chosen by a different query. */
|
|
return NULL;
|
|
}
|
|
return *address_state;
|
|
}
|
|
|
|
static void unpack_state_from_zonecut(struct iter_local_state *local_state,
|
|
struct kr_query *qry)
|
|
{
|
|
struct kr_zonecut *zonecut = &qry->zone_cut;
|
|
struct knot_mm *mm = &qry->request->pool;
|
|
|
|
bool zcut_changed = false;
|
|
if (local_state->names == NULL || local_state->addresses == NULL) {
|
|
/* Local state initialization. */
|
|
memset(local_state, 0, sizeof(struct iter_local_state));
|
|
local_state->names = trie_create(mm);
|
|
local_state->addresses = trie_create(mm);
|
|
} else {
|
|
zcut_changed = !knot_dname_is_equal(zonecut->name, local_state->zonecut);
|
|
}
|
|
local_state->zonecut = zonecut->name;
|
|
local_state->generation++;
|
|
|
|
if (zcut_changed) {
|
|
local_state->no_ns_addr_count = 0;
|
|
}
|
|
|
|
trie_it_t *it;
|
|
const unsigned int current_generation = local_state->generation;
|
|
|
|
for (it = trie_it_begin(zonecut->nsset); !trie_it_finished(it); trie_it_next(it)) {
|
|
knot_dname_t *dname = (knot_dname_t *)trie_it_key(it, NULL);
|
|
pack_t *addresses = *trie_it_val(it);
|
|
|
|
trie_val_t *val = trie_get_ins(local_state->names, (char *)dname,
|
|
knot_dname_size(dname));
|
|
if (!*val) {
|
|
/* We encountered this name for the first time. */
|
|
*val = mm_calloc(mm, 1, sizeof(struct iter_name_state));
|
|
}
|
|
struct iter_name_state *name_state = *val;
|
|
name_state->generation = current_generation;
|
|
|
|
if (zcut_changed) {
|
|
/* Set name as unresolved as they might have fallen out
|
|
* of cache (TTL expired). */
|
|
name_state->a_state = RECORD_UNKNOWN;
|
|
name_state->aaaa_state = RECORD_UNKNOWN;
|
|
}
|
|
|
|
/* Iterate over all addresses of this NS (if any). */
|
|
for (uint8_t *obj = pack_head(*addresses); obj != pack_tail(*addresses);
|
|
obj = pack_obj_next(obj)) {
|
|
uint8_t *address = pack_obj_val(obj);
|
|
size_t address_len = pack_obj_len(obj);
|
|
trie_val_t *tval = trie_get_ins(local_state->addresses,
|
|
(char *)address,
|
|
address_len);
|
|
if (!*tval) {
|
|
/* We have have not seen this address before. */
|
|
*tval = mm_calloc(mm, 1, sizeof(struct address_state));
|
|
}
|
|
struct address_state *address_state = *tval;
|
|
address_state->generation = current_generation;
|
|
address_state->ns_name = dname;
|
|
|
|
if (address_len == sizeof(struct in_addr)) {
|
|
name_state->a_state = RECORD_RESOLVED;
|
|
} else if (address_len == sizeof(struct in6_addr)) {
|
|
name_state->aaaa_state = RECORD_RESOLVED;
|
|
}
|
|
union kr_sockaddr tmp_address;
|
|
bytes_to_ip(address, address_len, 0, &tmp_address);
|
|
update_address_state(address_state, &tmp_address, address_len, qry);
|
|
}
|
|
}
|
|
trie_it_free(it);
|
|
kr_cache_commit(&qry->request->ctx->cache);
|
|
}
|
|
|
|
static int get_valid_addresses(struct iter_local_state *local_state,
|
|
struct choice choices[])
|
|
{
|
|
unsigned count = 0;
|
|
trie_it_t *it;
|
|
for (it = trie_it_begin(local_state->addresses); !trie_it_finished(it);
|
|
trie_it_next(it)) {
|
|
size_t address_len;
|
|
uint8_t *address = (uint8_t *)trie_it_key(it, &address_len);
|
|
struct address_state *address_state = *trie_it_val(it);
|
|
if (address_state->generation == local_state->generation &&
|
|
!address_state->broken) {
|
|
choices[count] = (struct choice){
|
|
.address_len = address_len,
|
|
.address_state = address_state,
|
|
};
|
|
bytes_to_ip(address, address_len, 0, &choices[count].address);
|
|
count++;
|
|
}
|
|
}
|
|
trie_it_free(it);
|
|
return count;
|
|
}
|
|
|
|
static int get_resolvable_names(struct iter_local_state *local_state,
|
|
struct to_resolve resolvable[], struct kr_query *qry)
|
|
{
|
|
/* Further resolution is not possible until we get `. DNSKEY` record;
|
|
* we have to choose one of the known addresses here. */
|
|
if (qry->sname[0] == '\0' && qry->stype == KNOT_RRTYPE_DNSKEY) {
|
|
return 0;
|
|
}
|
|
|
|
unsigned count = 0;
|
|
trie_it_t *it;
|
|
for (it = trie_it_begin(local_state->names); !trie_it_finished(it);
|
|
trie_it_next(it)) {
|
|
struct iter_name_state *name_state = *trie_it_val(it);
|
|
if (name_state->generation != local_state->generation)
|
|
continue;
|
|
|
|
knot_dname_t *name = (knot_dname_t *)trie_it_key(it, NULL);
|
|
if (qry->stype == KNOT_RRTYPE_DNSKEY &&
|
|
knot_dname_in_bailiwick(name, qry->sname) > 0) {
|
|
/* Resolving `domain. DNSKEY` can't trigger the
|
|
* resolution of `sub.domain. A/AAAA` since it
|
|
* will cause a cycle. */
|
|
continue;
|
|
}
|
|
|
|
/* FIXME: kr_rplan_satisfies(qry,…) should have been here, but this leads to failures on
|
|
* iter_ns_badip.rpl, this is because the test requires the resolver to switch to parent
|
|
* side after a record in cache expires. Only way to do this in the current zonecut setup is
|
|
* to requery the same query twice in the row. So we have to allow that and only check the
|
|
* rplan from parent upwards.
|
|
*/
|
|
bool a_in_rplan = kr_rplan_satisfies(qry->parent, name,
|
|
KNOT_CLASS_IN, KNOT_RRTYPE_A);
|
|
bool aaaa_in_rplan = kr_rplan_satisfies(qry->parent, name,
|
|
KNOT_CLASS_IN, KNOT_RRTYPE_AAAA);
|
|
|
|
if (name_state->a_state == RECORD_UNKNOWN &&
|
|
!qry->flags.NO_IPV4 && !a_in_rplan) {
|
|
resolvable[count++] = (struct to_resolve){
|
|
name, KR_TRANSPORT_RESOLVE_A
|
|
};
|
|
}
|
|
|
|
if (name_state->aaaa_state == RECORD_UNKNOWN &&
|
|
!qry->flags.NO_IPV6 && !aaaa_in_rplan) {
|
|
resolvable[count++] = (struct to_resolve){
|
|
name, KR_TRANSPORT_RESOLVE_AAAA
|
|
};
|
|
}
|
|
}
|
|
trie_it_free(it);
|
|
return count;
|
|
}
|
|
|
|
static void update_name_state(knot_dname_t *name, enum kr_transport_protocol type,
|
|
trie_t *names)
|
|
{
|
|
size_t name_len = knot_dname_size(name);
|
|
trie_val_t *val = trie_get_try(names, (char *)name, name_len);
|
|
|
|
if (!val) {
|
|
return;
|
|
}
|
|
|
|
struct iter_name_state *name_state = (struct iter_name_state *)*val;
|
|
switch (type) {
|
|
case KR_TRANSPORT_RESOLVE_A:
|
|
name_state->a_state = RECORD_TRIED;
|
|
break;
|
|
case KR_TRANSPORT_RESOLVE_AAAA:
|
|
name_state->aaaa_state = RECORD_TRIED;
|
|
break;
|
|
default:
|
|
kr_assert(false);
|
|
}
|
|
}
|
|
|
|
void iter_choose_transport(struct kr_query *qry, struct kr_transport **transport)
|
|
{
|
|
struct knot_mm *mempool = &qry->request->pool;
|
|
struct iter_local_state *local_state =
|
|
(struct iter_local_state *)
|
|
qry->server_selection.local_state->private;
|
|
|
|
unpack_state_from_zonecut(local_state, qry);
|
|
|
|
struct choice choices[trie_weight(local_state->addresses) + 1/*avoid 0*/];
|
|
/* We may try to resolve A and AAAA record for each name, so therefore
|
|
* 2*trie_weight(…) is here. */
|
|
struct to_resolve resolvable[2 * trie_weight(local_state->names)];
|
|
|
|
// Filter valid addresses and names from the tries
|
|
int choices_len = get_valid_addresses(local_state, choices);
|
|
int resolvable_len = get_resolvable_names(local_state, resolvable, qry);
|
|
bool * const force_resolve_p = &qry->server_selection.local_state->force_resolve;
|
|
|
|
// Print some stats into debug logs.
|
|
if (kr_log_is_debug_qry(SELECTION, qry)) {
|
|
int v4_choices = 0;
|
|
for (int i = 0; i < choices_len; ++i)
|
|
if (choices[i].address.ip.sa_family == AF_INET)
|
|
++v4_choices;
|
|
int v4_resolvable = 0;
|
|
for (int i = 0; i < resolvable_len; ++i)
|
|
if (resolvable[i].type == KR_TRANSPORT_RESOLVE_A)
|
|
++v4_resolvable;
|
|
VERBOSE_MSG(qry, "=> id: '%05u' choosing from addresses: %d v4 + %d v6; "
|
|
"names to resolve: %d v4 + %d v6; "
|
|
"force_resolve: %d; NO6: IPv6 is %s\n",
|
|
qry->id, v4_choices, choices_len - v4_choices,
|
|
v4_resolvable, resolvable_len - v4_resolvable,
|
|
(int)*force_resolve_p, no6_is_bad() ? "KO" : "OK");
|
|
}
|
|
|
|
if (*force_resolve_p && resolvable_len) {
|
|
choices_len = 0;
|
|
*force_resolve_p = false;
|
|
}
|
|
|
|
bool tcp = qry->flags.TCP || qry->server_selection.local_state->truncated;
|
|
*transport = select_transport(choices, choices_len, resolvable, resolvable_len,
|
|
qry->server_selection.local_state->timeouts,
|
|
mempool, tcp, NULL);
|
|
bool nxnsattack_mitigation = false;
|
|
|
|
if (*transport) {
|
|
switch ((*transport)->protocol) {
|
|
case KR_TRANSPORT_RESOLVE_A:
|
|
case KR_TRANSPORT_RESOLVE_AAAA:
|
|
if (++local_state->no_ns_addr_count > KR_COUNT_NO_NSADDR_LIMIT) {
|
|
*transport = NULL;
|
|
nxnsattack_mitigation = true;
|
|
break;
|
|
}
|
|
/* Note that we tried resolving this name to not try it again. */
|
|
update_name_state((*transport)->ns_name, (*transport)->protocol, local_state->names);
|
|
break;
|
|
case KR_TRANSPORT_TLS:
|
|
case KR_TRANSPORT_TCP:
|
|
/* We need to propagate this to flags since it's used in
|
|
* other parts of the resolver. */
|
|
qry->flags.TCP = true;
|
|
case KR_TRANSPORT_UDP: /* fall through */
|
|
local_state->no_ns_addr_count = 0;
|
|
break;
|
|
default:
|
|
kr_assert(false);
|
|
break;
|
|
}
|
|
|
|
if (*transport &&
|
|
(*transport)->protocol == KR_TRANSPORT_TCP &&
|
|
!qry->server_selection.local_state->truncated &&
|
|
qry->server_selection.local_state->force_udp) {
|
|
// Last chance on broken TCP.
|
|
(*transport)->protocol = KR_TRANSPORT_UDP;
|
|
qry->flags.TCP = false;
|
|
}
|
|
}
|
|
|
|
if (*transport == NULL && local_state->last_error == KR_SELECTION_DNSSEC_ERROR) {
|
|
/* Last selected server had broken DNSSEC and now we have no more
|
|
* servers to ask. We signal this to the rest of resolver by
|
|
* setting DNSSEC_BOGUS flag. */
|
|
qry->flags.DNSSEC_BOGUS = true;
|
|
}
|
|
|
|
if (kr_log_is_debug_qry(SELECTION, qry))
|
|
{
|
|
KR_DNAME_GET_STR(zonecut_str, qry->zone_cut.name);
|
|
if (*transport) {
|
|
KR_DNAME_GET_STR(ns_name, (*transport)->ns_name);
|
|
const enum kr_transport_protocol proto = *transport ? (*transport)->protocol : -1;
|
|
const char *ns_str = kr_straddr(&(*transport)->address.ip);
|
|
const char *ip_version;
|
|
switch (proto)
|
|
{
|
|
case KR_TRANSPORT_RESOLVE_A:
|
|
case KR_TRANSPORT_RESOLVE_AAAA:
|
|
ip_version = (proto == KR_TRANSPORT_RESOLVE_A) ? "A" : "AAAA";
|
|
VERBOSE_MSG(qry, "=> id: '%05u' choosing to resolve %s: '%s' zone cut: '%s'\n",
|
|
qry->id, ip_version, ns_name, zonecut_str);
|
|
break;
|
|
default:
|
|
VERBOSE_MSG(qry, "=> id: '%05u' choosing: '%s'@'%s'"
|
|
" with timeout %u ms zone cut: '%s'\n",
|
|
qry->id, ns_name, ns_str ? ns_str : "",
|
|
(*transport)->timeout, zonecut_str);
|
|
break;
|
|
}
|
|
} else {
|
|
const char *nxns_msg = nxnsattack_mitigation
|
|
? " (stopped due to mitigation for NXNSAttack CVE-2020-12667)" : "";
|
|
VERBOSE_MSG(qry, "=> id: '%05u' no suitable transport, zone cut: '%s'%s\n",
|
|
qry->id, zonecut_str, nxns_msg );
|
|
}
|
|
}
|
|
}
|
|
|
|
void iter_error(struct kr_query *qry, const struct kr_transport *transport,
|
|
enum kr_selection_error sel_error)
|
|
{
|
|
if (!qry->server_selection.initialized) {
|
|
return;
|
|
}
|
|
struct iter_local_state *local_state = qry->server_selection.local_state->private;
|
|
struct address_state *addr_state = get_address_state(local_state, transport);
|
|
local_state->last_error = sel_error;
|
|
error(qry, addr_state, transport, sel_error);
|
|
}
|
|
|
|
void iter_update_rtt(struct kr_query *qry, const struct kr_transport *transport,
|
|
unsigned rtt)
|
|
{
|
|
if (!qry->server_selection.initialized) {
|
|
return;
|
|
}
|
|
struct iter_local_state *local_state = qry->server_selection.local_state->private;
|
|
struct address_state *addr_state = get_address_state(local_state, transport);
|
|
update_rtt(qry, addr_state, transport, rtt);
|
|
}
|