diff --git a/man/resolved.conf.xml b/man/resolved.conf.xml
index f8899fe662..2c5358209f 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 117bf7ccc3..2a5c4eb650 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 71899d36f6..51be831086 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 6a7967842d..3d5dc2772a 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 be98a8a567..54ae110c09 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 89b13b0f1d..d48896494f 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 8b8a66d036..b5f31f9139 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 d7d7077265..add4f64910 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 d72e9104d7..6fbd7d39fd 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 147d30845b..c1f7b26c72 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 705878422e..6dc28f39fc 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()
* ================================================================ */