***/
#include "resolved-dns-cache.h"
+#include "resolved-dns-packet.h"
+/* Never cache more than 1K entries */
#define CACHE_MAX 1024
+
+/* We never keep any item longer than 10min in our cache */
#define CACHE_TTL_MAX_USEC (10 * USEC_PER_MINUTE)
+typedef enum DnsCacheItemType DnsCacheItemType;
+typedef struct DnsCacheItem DnsCacheItem;
+
+enum DnsCacheItemType {
+ DNS_CACHE_POSITIVE,
+ DNS_CACHE_NODATA,
+ DNS_CACHE_NXDOMAIN,
+};
+
+struct DnsCacheItem {
+ DnsResourceKey *key;
+ DnsResourceRecord *rr;
+ usec_t until;
+ DnsCacheItemType type;
+ unsigned prioq_idx;
+ int owner_family;
+ union in_addr_union owner_address;
+ LIST_FIELDS(DnsCacheItem, by_key);
+};
+
static void dns_cache_item_free(DnsCacheItem *i) {
if (!i)
return;
dns_resource_record_unref(i->rr);
+ dns_resource_key_unref(i->key);
free(i);
}
if (!i)
return;
- first = hashmap_get(c->rrsets, &i->rr->key);
- LIST_REMOVE(rrsets, first, i);
+ first = hashmap_get(c->by_key, i->key);
+ LIST_REMOVE(by_key, first, i);
if (first)
- assert_se(hashmap_replace(c->rrsets, &first->rr->key, first) >= 0);
+ assert_se(hashmap_replace(c->by_key, first->key, first) >= 0);
else
- hashmap_remove(c->rrsets, &i->rr->key);
+ hashmap_remove(c->by_key, i->key);
- prioq_remove(c->expire, i, &i->expire_prioq_idx);
+ prioq_remove(c->by_expiry, i, &i->prioq_idx);
dns_cache_item_free(i);
}
assert(c);
- while ((i = hashmap_first(c->rrsets)))
+ while ((i = hashmap_first(c->by_key)))
dns_cache_item_remove_and_free(c, i);
- assert(hashmap_size(c->rrsets) == 0);
- assert(prioq_size(c->expire) == 0);
+ assert(hashmap_size(c->by_key) == 0);
+ assert(prioq_size(c->by_expiry) == 0);
- hashmap_free(c->rrsets);
- c->rrsets = NULL;
+ hashmap_free(c->by_key);
+ c->by_key = NULL;
- prioq_free(c->expire);
- c->expire = NULL;
+ prioq_free(c->by_expiry);
+ c->by_expiry = NULL;
}
-void dns_cache_remove(DnsCache *c, DnsResourceKey *key) {
+static void dns_cache_remove(DnsCache *c, DnsResourceKey *key) {
DnsCacheItem *i;
assert(c);
assert(key);
- while ((i = hashmap_get(c->rrsets, &key)))
+ while ((i = hashmap_get(c->by_key, key)))
dns_cache_item_remove_and_free(c, i);
}
* case the cache will be emptied completely otherwise. */
for (;;) {
- _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
DnsCacheItem *i;
- if (prioq_size(c->expire) <= 0)
+ if (prioq_size(c->by_expiry) <= 0)
break;
- if (prioq_size(c->expire) + add < CACHE_MAX)
+ if (prioq_size(c->by_expiry) + add < CACHE_MAX)
break;
- i = prioq_peek(c->expire);
- rr = dns_resource_record_ref(i->rr);
- dns_cache_remove(c, &rr->key);
+ 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(c, key);
}
}
/* Remove all entries that are past their TTL */
for (;;) {
+ _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
DnsCacheItem *i;
- usec_t ttl;
- i = prioq_peek(c->expire);
+ i = prioq_peek(c->by_expiry);
if (!i)
break;
- ttl = i->rr->ttl * USEC_PER_SEC;
- if (ttl > CACHE_TTL_MAX_USEC)
- ttl = CACHE_TTL_MAX_USEC;
-
if (t <= 0)
- t = now(CLOCK_MONOTONIC);
+ t = now(CLOCK_BOOTTIME);
- if (i->timestamp + ttl > t)
+ if (i->until > t)
break;
- dns_cache_remove(c, &i->rr->key);
+ /* 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(c, key);
}
}
static int dns_cache_item_prioq_compare_func(const void *a, const void *b) {
- usec_t t, z;
const DnsCacheItem *x = a, *y = b;
- t = x->timestamp + x->rr->ttl * USEC_PER_SEC;
- z = y->timestamp + y->rr->ttl * USEC_PER_SEC;
-
- if (t < z)
+ if (x->until < y->until)
return -1;
- if (t > z)
+ if (x->until > y->until)
return 1;
return 0;
}
-static void dns_cache_item_update(DnsCache *c, DnsCacheItem *i, DnsResourceRecord *rr, usec_t timestamp) {
+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) {
+ 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);
- if (!i->rrsets_prev) {
+ 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 void dns_cache_item_update_positive(DnsCache *c, DnsCacheItem *i, DnsResourceRecord *rr, usec_t timestamp) {
+ assert(c);
+ assert(i);
+ assert(rr);
+
+ 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->rrsets, &rr->key, i) >= 0);
+ assert_se(hashmap_replace(c->by_key, rr->key, i) >= 0);
}
+ dns_resource_record_ref(rr);
dns_resource_record_unref(i->rr);
- i->rr = dns_resource_record_ref(rr);
+ i->rr = rr;
+
+ dns_resource_key_unref(i->key);
+ i->key = dns_resource_key_ref(rr->key);
- i->timestamp = timestamp;
+ i->until = timestamp + MIN(rr->ttl * USEC_PER_SEC, CACHE_TTL_MAX_USEC);
- prioq_reshuffle(c->expire, i, &i->expire_prioq_idx);
+ prioq_reshuffle(c->by_expiry, i, &i->prioq_idx);
}
-int dns_cache_put(DnsCache *c, DnsResourceRecord *rr, usec_t timestamp) {
+static int dns_cache_put_positive(
+ DnsCache *c,
+ DnsResourceRecord *rr,
+ usec_t timestamp,
+ int owner_family,
+ const union in_addr_union *owner_address) {
+
_cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
- DnsCacheItem *first = NULL, *existing;
+ DnsCacheItem *existing;
int r;
assert(c);
assert(rr);
+ assert(owner_address);
/* New TTL is 0? Delete the entry... */
if (rr->ttl <= 0) {
- dns_cache_remove(c, &rr->key);
+ dns_cache_remove(c, rr->key);
return 0;
}
+ if (rr->key->class == DNS_CLASS_ANY)
+ return 0;
+ if (rr->key->type == DNS_TYPE_ANY)
+ return 0;
+
/* Entry exists already? Update TTL and timestamp */
existing = dns_cache_get(c, rr);
if (existing) {
- dns_cache_item_update(c, existing, rr, timestamp);
+ dns_cache_item_update_positive(c, existing, rr, timestamp);
return 0;
}
/* Otherwise, add the new RR */
- r = prioq_ensure_allocated(&c->expire, dns_cache_item_prioq_compare_func);
+ r = dns_cache_init(c);
if (r < 0)
return r;
- r = hashmap_ensure_allocated(&c->rrsets, dns_resource_key_hash_func, dns_resource_key_compare_func);
+ dns_cache_make_space(c, 1);
+
+ i = new0(DnsCacheItem, 1);
+ if (!i)
+ return -ENOMEM;
+
+ i->type = DNS_CACHE_POSITIVE;
+ i->key = dns_resource_key_ref(rr->key);
+ i->rr = dns_resource_record_ref(rr);
+ i->until = timestamp + MIN(i->rr->ttl * USEC_PER_SEC, CACHE_TTL_MAX_USEC);
+ i->prioq_idx = PRIOQ_IDX_NULL;
+ i->owner_family = owner_family;
+ i->owner_address = *owner_address;
+
+ r = dns_cache_link_item(c, i);
+ if (r < 0)
+ return r;
+
+ i = NULL;
+ return 0;
+}
+
+static int dns_cache_put_negative(
+ DnsCache *c,
+ DnsResourceKey *key,
+ int rcode,
+ usec_t timestamp,
+ uint32_t soa_ttl,
+ int owner_family,
+ const union in_addr_union *owner_address) {
+
+ _cleanup_(dns_cache_item_freep) DnsCacheItem *i = NULL;
+ int r;
+
+ assert(c);
+ assert(key);
+ assert(owner_address);
+
+ dns_cache_remove(c, key);
+
+ if (key->class == DNS_CLASS_ANY)
+ return 0;
+ if (key->type == DNS_TYPE_ANY)
+ return 0;
+ if (soa_ttl <= 0)
+ return 0;
+
+ if (!IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN))
+ return 0;
+
+ r = dns_cache_init(c);
if (r < 0)
return r;
if (!i)
return -ENOMEM;
- i->rr = dns_resource_record_ref(rr);
- i->timestamp = timestamp;
- i->expire_prioq_idx = PRIOQ_IDX_NULL;
+ i->type = rcode == DNS_RCODE_SUCCESS ? DNS_CACHE_NODATA : DNS_CACHE_NXDOMAIN;
+ i->key = dns_resource_key_ref(key);
+ i->until = timestamp + MIN(soa_ttl * USEC_PER_SEC, CACHE_TTL_MAX_USEC);
+ i->prioq_idx = PRIOQ_IDX_NULL;
+ i->owner_family = owner_family;
+ i->owner_address = *owner_address;
- r = prioq_put(c->expire, i, &i->expire_prioq_idx);
+ r = dns_cache_link_item(c, i);
if (r < 0)
return r;
- first = hashmap_get(c->rrsets, &i->rr->key);
- if (first) {
- LIST_PREPEND(rrsets, first, i);
- assert_se(hashmap_replace(c->rrsets, &first->rr->key, first) >= 0);
- } else {
- r = hashmap_put(c->rrsets, &i->rr->key, i);
- if (r < 0) {
- prioq_remove(c->expire, i, &i->expire_prioq_idx);
- return r;
- }
- }
-
i = NULL;
-
return 0;
}
-int dns_cache_put_rrs(DnsCache *c, DnsResourceRecord **rrs, unsigned n_rrs, usec_t timestamp) {
- unsigned i, added = 0;
+int dns_cache_put(
+ DnsCache *c,
+ DnsQuestion *q,
+ int rcode,
+ DnsAnswer *answer,
+ unsigned max_rrs,
+ usec_t timestamp,
+ int owner_family,
+ const union in_addr_union *owner_address) {
+
+ unsigned i;
int r;
assert(c);
+ assert(q);
- if (n_rrs <= 0)
+ /* First, delete all matching old RRs, so that we only keep
+ * complete by_key in place. */
+ for (i = 0; i < q->n_keys; i++)
+ dns_cache_remove(c, q->keys[i]);
+
+ if (!answer)
+ return 0;
+
+ for (i = 0; i < answer->n_rrs; i++)
+ dns_cache_remove(c, answer->rrs[i]->key);
+
+ /* We only care for positive replies and NXDOMAINs, on all
+ * other replies we will simply flush the respective entries,
+ * and that's it */
+
+ if (!IN_SET(rcode, DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN))
return 0;
- assert(rrs);
+ /* Make some space for our new entries */
+ dns_cache_make_space(c, answer->n_rrs + q->n_keys);
- /* First iteration, delete all matching old RRs, so that we
- * only keep complete rrsets in place. */
- for (i = 0; i < n_rrs; i++)
- dns_cache_remove(c, &rrs[i]->key);
+ if (timestamp <= 0)
+ timestamp = now(CLOCK_BOOTTIME);
- dns_cache_make_space(c, n_rrs);
+ /* Second, add in positive entries for all contained RRs */
+ for (i = 0; i < MIN(max_rrs, answer->n_rrs); i++) {
+ r = dns_cache_put_positive(c, answer->rrs[i], timestamp, owner_family, owner_address);
+ if (r < 0)
+ goto fail;
+ }
- /* Second iteration, add in new RRs */
- for (added = 0; added < n_rrs; added++) {
- if (timestamp <= 0)
- timestamp = now(CLOCK_MONOTONIC);
+ /* Third, add in negative entries for all keys with no RR */
+ for (i = 0; i < q->n_keys; i++) {
+ DnsResourceRecord *soa = NULL;
- r = dns_cache_put(c, rrs[added], timestamp);
+ r = dns_answer_contains(answer, q->keys[i]);
if (r < 0)
goto fail;
+ if (r > 0)
+ continue;
+ r = dns_answer_find_soa(answer, q->keys[i], &soa);
+ if (r < 0)
+ goto fail;
+ if (r == 0)
+ continue;
+
+ r = dns_cache_put_negative(c, q->keys[i], rcode, timestamp, MIN(soa->soa.minimum, soa->ttl), owner_family, owner_address);
+ if (r < 0)
+ goto fail;
}
return 0;
/* Adding all RRs failed. Let's clean up what we already
* added, just in case */
- for (i = 0; i < added; i++)
- dns_cache_remove(c, &rrs[i]->key);
+ for (i = 0; i < q->n_keys; i++)
+ dns_cache_remove(c, q->keys[i]);
+ for (i = 0; i < answer->n_rrs; i++)
+ dns_cache_remove(c, answer->rrs[i]->key);
return r;
}
-DnsCacheItem* dns_cache_lookup(DnsCache *c, DnsResourceKey *key) {
- assert(c);
- assert(key);
-
- return hashmap_get(c->rrsets, key);
-}
-
-DnsCacheItem* dns_cache_get(DnsCache *c, DnsResourceRecord *rr) {
- DnsCacheItem *i;
+int dns_cache_lookup(DnsCache *c, DnsQuestion *q, int *rcode, DnsAnswer **ret) {
+ _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+ unsigned i, n = 0;
+ int r;
+ bool nxdomain = false;
assert(c);
- assert(rr);
+ assert(q);
+ assert(rcode);
+ assert(ret);
- LIST_FOREACH(rrsets, i, hashmap_get(c->rrsets, &rr->key))
- if (dns_resource_record_equal(i->rr, rr))
- return i;
+ if (q->n_keys <= 0) {
+ *ret = NULL;
+ *rcode = 0;
+ return 0;
+ }
- return NULL;
-}
+ for (i = 0; i < q->n_keys; i++) {
+ DnsCacheItem *j;
-int dns_cache_lookup_many(DnsCache *c, DnsResourceKey *keys, unsigned n_keys, DnsResourceRecord ***rrs) {
- DnsResourceRecord **p = NULL;
- size_t allocated = 0, used = 0;
- unsigned i;
- int r;
+ if (q->keys[i]->type == DNS_TYPE_ANY ||
+ q->keys[i]->class == DNS_CLASS_ANY) {
+ /* If we have ANY lookups we simply refresh */
+ *ret = NULL;
+ *rcode = 0;
+ return 0;
+ }
- assert(c);
- assert(rrs);
+ j = hashmap_get(c->by_key, q->keys[i]);
+ if (!j) {
+ /* If one question cannot be answered we need to refresh */
+ *ret = NULL;
+ *rcode = 0;
+ return 0;
+ }
- if (n_keys <= 0) {
- *rrs = NULL;
- return 0;
+ LIST_FOREACH(by_key, j, j) {
+ if (j->rr)
+ n++;
+ else if (j->type == DNS_CACHE_NXDOMAIN)
+ nxdomain = true;
+ }
}
- assert(keys);
+ if (n <= 0) {
+ *ret = NULL;
+ *rcode = nxdomain ? DNS_RCODE_NXDOMAIN : DNS_RCODE_SUCCESS;
+ return 1;
+ }
- for (i = 0; i < n_keys; i++) {
+ answer = dns_answer_new(n);
+ if (!answer)
+ return -ENOMEM;
+
+ for (i = 0; i < q->n_keys; i++) {
DnsCacheItem *j;
- j = dns_cache_lookup(c, &keys[i]);
- if (!j) {
- *rrs = NULL;
- r = 0;
- goto fail;
+ j = hashmap_get(c->by_key, q->keys[i]);
+ LIST_FOREACH(by_key, j, j) {
+ if (j->rr) {
+ r = dns_answer_add(answer, j->rr);
+ if (r < 0)
+ return r;
+ }
}
+ }
- LIST_FOREACH(rrsets, j, j) {
+ *ret = answer;
+ *rcode = DNS_RCODE_SUCCESS;
+ answer = NULL;
- if (!GREEDY_REALLOC(p, allocated, used+1)) {
- r = -ENOMEM;
- goto fail;
- }
+ return n;
+}
+
+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);
- p[used++] = dns_resource_record_ref(j->rr);
+ /* 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;
- *rrs = p;
- return (int) used;
+ /* 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;
-fail:
- dns_resource_record_freev(p, used);
- return r;
+ /* There's a conflict */
+ return 1;
}