]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
resolved: add configurable DNS cache size
authorishwarbb <ishwarbb23@gmail.com>
Mon, 23 Mar 2026 13:02:40 +0000 (13:02 +0000)
committerLuca Boccassi <luca.boccassi@gmail.com>
Mon, 18 May 2026 10:45:03 +0000 (11:45 +0100)
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>
man/resolved.conf.xml
src/resolve/resolved-conf.c
src/resolve/resolved-conf.h
src/resolve/resolved-dns-cache.c
src/resolve/resolved-dns-cache.h
src/resolve/resolved-dns-scope.c
src/resolve/resolved-gperf.gperf
src/resolve/resolved-manager.c
src/resolve/resolved-manager.h
src/resolve/resolved.conf.in
src/resolve/test-dns-cache.c

index f8899fe662c9531985375b16c390429239106802..2c5358209f07f539223139ac28a1b604e4fdd20e 100644 (file)
         </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
index 117bf7ccc32419f78231e7a918d3b978d9077cc5..2a5c4eb6509fe2796d0cf5adea8a90d4ab589f77 100644 (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,
index 71899d36f680dd0e559e3c59f90f7b9278bbf099..51be83108607402fce1ce6638bc21b7af09406a3 100644 (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);
index 6a7967842dbe49456bd3ea8350376770806a9ba4..3d5dc2772a83ad94c47890b859984dca367a753c 100644 (file)
 #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
index be98a8a56766531f8036e9a91736f3b637f21bb5..54ae110c09ca9d6d0ea19e565c2db659e71d88e9 100644 (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);
index 89b13b0f1d619d04f6902f451fb0a7006439a8d8..d48896494f90c4f086b682ef5a775d2f447a0e1f 100644 (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 },
index 8b8a66d0369bfb6f00ac3756d4c4db8e942010fb..b5f31f91397a6b6721703303695d07f4c31d9f81 100644 (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)
index d7d707726587d520f5bdfb416a5dfef623d4c4ad..add4f64910507c49b18fcaef70596d736efde66b 100644 (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) {};
index d72e9104d79d04796a2cfef41b4f47775d0e2b87..6fbd7d39fd4c93ce981f99194868f6df38b18354 100644 (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;
index 147d30845b1299bd5eaf37c7c2249d5031f4eec3..c1f7b26c72169e35cb0be54ed28851d850022a6c 100644 (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
index 705878422e081d0950b183d2e1c05d32719ec973..6dc28f39fccb0dc412cea32258e99d177ce857f9 100644 (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()
  * ================================================================ */