From: ishwarbb Date: Mon, 23 Mar 2026 13:02:40 +0000 (+0000) Subject: resolved: add configurable DNS cache size X-Git-Tag: v261-rc1~118 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=133909c8f65e49052e9e2bd413589095c1ab41fe;p=thirdparty%2Fsystemd.git 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 --- diff --git a/man/resolved.conf.xml b/man/resolved.conf.xml index f8899fe662c..2c5358209f0 100644 --- a/man/resolved.conf.xml +++ b/man/resolved.conf.xml @@ -301,6 +301,32 @@ + + DNSCacheSize= + MulticastDNSCacheSize= + LLMNRCacheSize= + 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 Cache= is set to + yes or no-negative. If Cache=no, caching + is fully disabled regardless of these values. + + Note that Multicast DNS relies heavily on caching for request suppression and efficient + operation. It is recommended to keep MulticastDNSCacheSize= at a reasonably high + value even when reducing DNSCacheSize=. + + Note that systemd-resolved automatically flushes all caches on system + memory pressure, thus in most cases manual cache size configuration should not be necessary. + + Note that caching is turned off by default for host-local DNS servers. + See CacheFromLocalhost= for details. + + + + + DNSStubListener= Takes a boolean argument or one of udp and diff --git a/src/resolve/resolved-conf.c b/src/resolve/resolved-conf.c index 117bf7ccc32..2a5c4eb6509 100644 --- a/src/resolve/resolved-conf.c +++ b/src/resolve/resolved-conf.c @@ -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, diff --git a/src/resolve/resolved-conf.h b/src/resolve/resolved-conf.h index 71899d36f68..51be8310860 100644 --- a/src/resolve/resolved-conf.h +++ b/src/resolve/resolved-conf.h @@ -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); diff --git a/src/resolve/resolved-dns-cache.c b/src/resolve/resolved-dns-cache.c index 6a7967842db..3d5dc2772a8 100644 --- a/src/resolve/resolved-dns-cache.c +++ b/src/resolve/resolved-dns-cache.c @@ -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 diff --git a/src/resolve/resolved-dns-cache.h b/src/resolve/resolved-dns-cache.h index be98a8a5676..54ae110c09c 100644 --- a/src/resolve/resolved-dns-cache.h +++ b/src/resolve/resolved-dns-cache.h @@ -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); diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c index 89b13b0f1d6..d48896494f9 100644 --- a/src/resolve/resolved-dns-scope.c +++ b/src/resolve/resolved-dns-scope.c @@ -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 }, diff --git a/src/resolve/resolved-gperf.gperf b/src/resolve/resolved-gperf.gperf index 8b8a66d0369..b5f31f91397 100644 --- a/src/resolve/resolved-gperf.gperf +++ b/src/resolve/resolved-gperf.gperf @@ -6,6 +6,7 @@ _Pragma("GCC diagnostic ignored \"-Wzero-as-null-pointer-constant\"") #endif #include #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) diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index d7d70772658..add4f649105 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -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) {}; diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h index d72e9104d79..6fbd7d39fd4 100644 --- a/src/resolve/resolved-manager.h +++ b/src/resolve/resolved-manager.h @@ -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; diff --git a/src/resolve/resolved.conf.in b/src/resolve/resolved.conf.in index 147d30845b1..c1f7b26c721 100644 --- a/src/resolve/resolved.conf.in +++ b/src/resolve/resolved.conf.in @@ -36,6 +36,9 @@ #LLMNR={{DEFAULT_LLMNR_MODE_STR}} #Cache=yes #CacheFromLocalhost=no +#DNSCacheSize=4096 +#MulticastDNSCacheSize=4096 +#LLMNRCacheSize=4096 #DNSStubListener=yes #DNSStubListenerExtra= #ReadEtcHosts=yes diff --git a/src/resolve/test-dns-cache.c b/src/resolve/test-dns-cache.c index 705878422e0..6dc28f39fcc 100644 --- a/src/resolve/test-dns-cache.c +++ b/src/resolve/test-dns-cache.c @@ -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() * ================================================================ */