resolved: add configurable DNS cache size

Add CacheSize= option to [Resolve] section of resolved.conf to allow
configuring the maximum number of entries in the per-scope DNS cache.
The default remains 4096 entries. Setting this to 0 disables caching
(similar to Cache=no).

CacheSize= is only read when Cache=yes or Cache=no-negative. When
Cache=no, caching is fully disabled regardless of CacheSize=.

Changes:
- Add cache_size field to Manager struct
- Parse CacheSize= from resolved.conf via gperf
- Thread cache_size through dns_cache_put() and helper functions
- Replace hard-coded CACHE_MAX with the configurable cache_size
- When cache_size is 0 or Cache=no, flush cache and skip caching
- Add man page documentation for the new option
- Add unit tests for cache size enforcement

Co-developed-by: Claude <claude@anthropic.com>
This commit is contained in:
ishwarbb
2026-03-23 13:02:40 +00:00
committed by Luca Boccassi
parent dd2d04179b
commit 133909c8f6
11 changed files with 153 additions and 8 deletions

View File

@@ -301,6 +301,32 @@
</listitem>
</varlistentry>
<varlistentry>
<term><varname>DNSCacheSize=</varname></term>
<term><varname>MulticastDNSCacheSize=</varname></term>
<term><varname>LLMNRCacheSize=</varname></term>
<listitem><para>Takes a non-negative integer. Configures the maximum number of DNS resource record
entries that may be stored in the per-scope cache for unicast DNS, Multicast DNS (mDNS), and
Link-Local Multicast Name Resolution (LLMNR) respectively. Each defaults to 4096. The maximum
allowed value is 16777216 (2^24). Setting any of these to 0 effectively disables caching for the
respective protocol. These settings are only effective when <varname>Cache=</varname> is set to
<literal>yes</literal> or <literal>no-negative</literal>. If <varname>Cache=no</varname>, caching
is fully disabled regardless of these values.</para>
<para>Note that Multicast DNS relies heavily on caching for request suppression and efficient
operation. It is recommended to keep <varname>MulticastDNSCacheSize=</varname> at a reasonably high
value even when reducing <varname>DNSCacheSize=</varname>.</para>
<para>Note that <command>systemd-resolved</command> automatically flushes all caches on system
memory pressure, thus in most cases manual cache size configuration should not be necessary.</para>
<para>Note that caching is turned off by default for host-local DNS servers.
See <varname>CacheFromLocalhost=</varname> for details.</para>
<xi:include href="version-info.xml" xpointer="v261"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>DNSStubListener=</varname></term>
<listitem><para>Takes a boolean argument or one of <literal>udp</literal> and

View File

@@ -8,6 +8,7 @@
#include "ordered-set.h"
#include "proc-cmdline.h"
#include "resolved-conf.h"
#include "resolved-dns-cache.h"
#include "resolved-dns-search-domain.h"
#include "resolved-dns-server.h"
#include "resolved-dns-stub.h"
@@ -304,6 +305,28 @@ int manager_parse_config_file(Manager *m) {
return 0;
}
int config_parse_dns_cache_max(
const char *unit,
const char *filename,
unsigned line,
const char *section,
unsigned section_line,
const char *lvalue,
int ltype,
const char *rvalue,
void *data,
void *userdata) {
Manager *m = ASSERT_PTR(userdata);
assert(ltype >= 0 && ltype < _DNS_PROTOCOL_MAX);
return config_parse_unsigned_bounded(
unit, filename, line, section, section_line, lvalue, rvalue,
0, CACHE_MAX_UPPER_LIMIT, true,
&m->cache_max[ltype]);
}
int config_parse_record_types(
const char *unit,
const char *filename,

View File

@@ -19,4 +19,5 @@ CONFIG_PARSER_PROTOTYPE(config_parse_dns_servers);
CONFIG_PARSER_PROTOTYPE(config_parse_search_domains);
CONFIG_PARSER_PROTOTYPE(config_parse_dns_stub_listener_mode);
CONFIG_PARSER_PROTOTYPE(config_parse_dns_stub_listener_extra);
CONFIG_PARSER_PROTOTYPE(config_parse_dns_cache_max);
CONFIG_PARSER_PROTOTYPE(config_parse_record_types);

View File

@@ -18,10 +18,6 @@
#include "string-util.h"
#include "time-util.h"
/* Never cache more than 4K entries. RFC 1536, Section 5 suggests to
* leave DNS caches unbounded, but that's crazy. */
#define CACHE_MAX 4096
/* We never keep any item longer than 2h in our cache unless StaleRetentionSec is greater than zero. */
#define CACHE_TTL_MAX_USEC (2 * USEC_PER_HOUR)
@@ -184,9 +180,12 @@ static void dns_cache_make_space(DnsCache *c, unsigned add) {
if (add <= 0)
return;
if (c->cache_max == 0)
return;
/* Makes space for n new entries. Note that we actually allow
* the cache to grow beyond CACHE_MAX, but only when we shall
* add more RRs to the cache than CACHE_MAX at once. In that
* the cache to grow beyond cache_max, but only when we shall
* add more RRs to the cache than cache_max at once. In that
* case the cache will be emptied completely otherwise. */
for (;;) {
@@ -196,7 +195,7 @@ static void dns_cache_make_space(DnsCache *c, unsigned add) {
if (prioq_isempty(c->by_expiry))
break;
if (prioq_size(c->by_expiry) + add < CACHE_MAX)
if (prioq_size(c->by_expiry) + add < c->cache_max)
break;
i = prioq_peek(c->by_expiry);
@@ -753,6 +752,10 @@ int dns_cache_put(
assert(c);
assert(owner_address);
/* Check cache mode here too, since the mDNS caller doesn't guard against Cache=no. */
if (cache_mode == DNS_CACHE_MODE_NO || c->cache_max == 0)
return 0;
dns_cache_remove_previous(c, key, answer);
/* We only care for positive replies and NXDOMAINs, on all other replies we will simply flush the respective

View File

@@ -3,11 +3,17 @@
#include "resolved-forward.h"
/* Never cache more than 4K entries by default. RFC 1536, Section 5 suggests to
* leave DNS caches unbounded, but that's crazy. */
#define DEFAULT_CACHE_MAX 4096U
#define CACHE_MAX_UPPER_LIMIT (1U << 24)
typedef struct DnsCache {
Hashmap *by_key;
Prioq *by_expiry;
unsigned n_hit;
unsigned n_miss;
unsigned cache_max;
} DnsCache;
void dns_cache_flush(DnsCache *c);

View File

@@ -76,6 +76,7 @@ int dns_scope_new(
.protocol = protocol,
.family = family,
.resend_timeout = MULTICAST_RESEND_TIMEOUT_MIN_USEC,
.cache.cache_max = m->cache_max[protocol],
/* Enforce ratelimiting for the multicast protocols */
.ratelimit = { MULTICAST_RATELIMIT_INTERVAL_USEC, MULTICAST_RATELIMIT_BURST },

View File

@@ -6,6 +6,7 @@ _Pragma("GCC diagnostic ignored \"-Wzero-as-null-pointer-constant\"")
#endif
#include <stddef.h>
#include "conf-parser.h"
#include "dns-packet.h"
#include "resolved-conf.h"
#include "resolved-dns-server.h"
#include "resolved-manager.h"
@@ -35,5 +36,8 @@ Resolve.ReadStaticRecords, config_parse_bool, 0,
Resolve.ResolveUnicastSingleLabel, config_parse_bool, 0, offsetof(Manager, resolve_unicast_single_label)
Resolve.DNSStubListenerExtra, config_parse_dns_stub_listener_extra, 0, offsetof(Manager, dns_extra_stub_listeners)
Resolve.CacheFromLocalhost, config_parse_bool, 0, offsetof(Manager, cache_from_localhost)
Resolve.DNSCacheSize, config_parse_dns_cache_max, DNS_PROTOCOL_DNS, 0
Resolve.MulticastDNSCacheSize, config_parse_dns_cache_max, DNS_PROTOCOL_MDNS, 0
Resolve.LLMNRCacheSize, config_parse_dns_cache_max, DNS_PROTOCOL_LLMNR, 0
Resolve.StaleRetentionSec, config_parse_sec, 0, offsetof(Manager, stale_retention_usec)
Resolve.RefuseRecordTypes, config_parse_record_types, 0, offsetof(Manager, refuse_record_types)

View File

@@ -641,6 +641,8 @@ static void manager_set_defaults(Manager *m) {
m->read_static_records = true;
m->resolve_unicast_single_label = false;
m->cache_from_localhost = false;
for (DnsProtocol p = 0; p < _DNS_PROTOCOL_MAX; p++)
m->cache_max[p] = DEFAULT_CACHE_MAX;
m->stale_retention_usec = 0;
m->refuse_record_types = set_free(m->refuse_record_types);
m->resolv_conf_stat = (struct stat) {};

View File

@@ -25,6 +25,7 @@ typedef struct Manager {
DnssecMode dnssec_mode;
DnsOverTlsMode dns_over_tls_mode;
DnsCacheMode enable_cache;
unsigned cache_max[_DNS_PROTOCOL_MAX];
bool cache_from_localhost;
DnsStubListenerMode dns_stub_listener_mode;
usec_t stale_retention_usec;

View File

@@ -36,6 +36,9 @@
#LLMNR={{DEFAULT_LLMNR_MODE_STR}}
#Cache=yes
#CacheFromLocalhost=no
#DNSCacheSize=4096
#MulticastDNSCacheSize=4096
#LLMNRCacheSize=4096
#DNSStubListener=yes
#DNSStubListenerExtra=
#ReadEtcHosts=yes

View File

@@ -21,7 +21,9 @@
#include "tmpfile-util.h"
static DnsCache new_cache(void) {
return (DnsCache) {};
return (DnsCache) {
.cache_max = DEFAULT_CACHE_MAX,
};
}
typedef struct PutArgs {
@@ -511,6 +513,79 @@ TEST(dns_a_to_cname_success_escaped_name_returns_error) {
ASSERT_TRUE(dns_cache_is_empty(&cache));
}
TEST(dns_cache_size_honored) {
_cleanup_(dns_cache_unrefp) DnsCache cache = new_cache();
_cleanup_(put_args_unrefp) PutArgs put_args = mk_put_args();
cache.cache_max = 4;
put_args.key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "one.example.com");
ASSERT_NOT_NULL(put_args.key);
put_args.rcode = DNS_RCODE_SUCCESS;
answer_add_a(&put_args, put_args.key, 0xc0a80101, 3600, DNS_ANSWER_CACHEABLE);
ASSERT_OK(cache_put(&cache, &put_args));
dns_resource_key_unref(put_args.key);
dns_answer_unref(put_args.answer);
put_args.answer = dns_answer_new(1);
ASSERT_NOT_NULL(put_args.answer);
put_args.key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "two.example.com");
ASSERT_NOT_NULL(put_args.key);
answer_add_a(&put_args, put_args.key, 0xc0a80102, 3600, DNS_ANSWER_CACHEABLE);
ASSERT_OK(cache_put(&cache, &put_args));
dns_resource_key_unref(put_args.key);
dns_answer_unref(put_args.answer);
put_args.answer = dns_answer_new(1);
ASSERT_NOT_NULL(put_args.answer);
put_args.key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "three.example.com");
ASSERT_NOT_NULL(put_args.key);
answer_add_a(&put_args, put_args.key, 0xc0a80103, 3600, DNS_ANSWER_CACHEABLE);
ASSERT_OK(cache_put(&cache, &put_args));
dns_resource_key_unref(put_args.key);
dns_answer_unref(put_args.answer);
put_args.answer = dns_answer_new(1);
ASSERT_NOT_NULL(put_args.answer);
put_args.key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "four.example.com");
ASSERT_NOT_NULL(put_args.key);
answer_add_a(&put_args, put_args.key, 0xc0a80104, 3600, DNS_ANSWER_CACHEABLE);
ASSERT_OK(cache_put(&cache, &put_args));
dns_resource_key_unref(put_args.key);
dns_answer_unref(put_args.answer);
put_args.answer = dns_answer_new(1);
ASSERT_NOT_NULL(put_args.answer);
put_args.key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "five.example.com");
ASSERT_NOT_NULL(put_args.key);
answer_add_a(&put_args, put_args.key, 0xc0a80105, 3600, DNS_ANSWER_CACHEABLE);
ASSERT_OK(cache_put(&cache, &put_args));
/* Each dns_cache_put() call reserves space for both the answer RR and the key (cache_keys=2),
* so eviction triggers when prioq_size + 2 >= cache_max (i.e. at the 3rd entry with cache_max=4).
* After 5 inserts, only the last 2 entries remain. */
ASSERT_EQ(dns_cache_size(&cache), 2u);
}
TEST(dns_cache_size_zero_evicts_all) {
_cleanup_(dns_cache_unrefp) DnsCache cache = new_cache();
_cleanup_(put_args_unrefp) PutArgs put_args = mk_put_args();
cache.cache_max = 0;
put_args.key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_A, "www.example.com");
ASSERT_NOT_NULL(put_args.key);
put_args.rcode = DNS_RCODE_SUCCESS;
answer_add_a(&put_args, put_args.key, 0xc0a8017f, 3600, DNS_ANSWER_CACHEABLE);
ASSERT_OK(cache_put(&cache, &put_args));
ASSERT_TRUE(dns_cache_is_empty(&cache));
}
/* ================================================================
* dns_cache_lookup()
* ================================================================ */