]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
resolved: add ability to define additional local RRs via drop-ins 41213/head
authorLennart Poettering <lennart@amutable.com>
Thu, 26 Feb 2026 14:54:14 +0000 (15:54 +0100)
committerLennart Poettering <lennart@amutable.com>
Tue, 24 Mar 2026 20:24:48 +0000 (21:24 +0100)
This is an extension of the /etc/hosts concept, but can provide any kind
of RRs (well, actually, we only parse A/AAAA/PTR for now, but the
concept is open for more).

Fixes: #17791
13 files changed:
man/resolved.conf.xml
man/rules/meson.build
man/systemd-resolved.service.xml
man/systemd.rr.xml [new file with mode: 0644]
src/resolve/meson.build
src/resolve/resolved-dns-query.c
src/resolve/resolved-gperf.gperf
src/resolve/resolved-manager.c
src/resolve/resolved-manager.h
src/resolve/resolved-static-records.c [new file with mode: 0644]
src/resolve/resolved-static-records.h [new file with mode: 0644]
src/resolve/resolved.conf.in
test/units/TEST-75-RESOLVED.sh

index 9adc0143c7c05540936a54400503f841689afc48..f8899fe662c9531985375b16c390429239106802 100644 (file)
@@ -363,12 +363,33 @@ DNSStubListenerExtra=udp:[2001:db8:0:f102::13]:9953</programlisting>
         <term><varname>ReadEtcHosts=</varname></term>
         <listitem><para>Takes a boolean argument. If <literal>yes</literal> (the default),
         <command>systemd-resolved</command> will read <filename>/etc/hosts</filename>, and try to resolve
-        hosts or address by using the entries in the file before sending query to DNS servers.
+        hosts or addresses by using the entries in the file before sending query to DNS servers.
         </para>
 
         <xi:include href="version-info.xml" xpointer="v240"/></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><varname>ReadStaticRecords=</varname></term>
+        <listitem><para>Takes a boolean argument. If <literal>yes</literal> (the default),
+        <command>systemd-resolved</command> will read
+        <filename>/etc/systemd/resolve/static.d/*.rr</filename>,
+        <filename>/run/systemd/resolve/static.d/*.rr</filename>,
+        <filename>/usr/local/lib/systemd/resolve/static.d/*.rr</filename>,
+        <filename>/usr/lib/systemd/resolve/static.d/*.rr</filename>, and try to resolve lookups by using the
+        entries in these files before sending query to DNS servers. This functionality is very similar to the
+        one controlled by <varname>ReadEtcHosts=</varname>, but allows more flexible control of DNS resource
+        records fields beyond just A/AAAA/PTR. See
+        <citerefentry><refentrytitle>systemd.rr</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
+        details.</para>
+
+        <para>If both this option and <varname>ReadEtcHosts=</varname> are enabled then this mechanism takes
+        precedence: any records discovered via static resource records will take precedence over records
+        under the same name from <filename>/etc/hosts</filename>.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><varname>ResolveUnicastSingleLabel=</varname></term>
         <listitem><para>Takes a boolean argument. When false (the default),
@@ -418,6 +439,7 @@ DNSStubListenerExtra=udp:[2001:db8:0:f102::13]:9953</programlisting>
       <member><citerefentry><refentrytitle>systemd-resolved.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>systemd-networkd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>dnssec-trust-anchors.d</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd.rr</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
       <member><citerefentry project='man-pages'><refentrytitle>resolv.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
       </simplelist></para>
   </refsect1>
index d2d26abe5da31859fae99845fc21490306e2cf99..682f55c774dd65a2fee98338c6b60c58017dd8d6 100644 (file)
@@ -1280,6 +1280,7 @@ manpages = [
  ['systemd.pcrlock', '5', ['systemd.pcrlock.d'], ''],
  ['systemd.preset', '5', [], ''],
  ['systemd.resource-control', '5', [], ''],
+ ['systemd.rr', '5', [], 'ENABLE_RESOLVE'],
  ['systemd.scope', '5', [], ''],
  ['systemd.service', '5', [], ''],
  ['systemd.slice', '5', [], ''],
index a5ab48d2fa05c97ea97924ab13763f446824c9e3..1d27d2c3c59e74ac0821da27a22191ea6074badd 100644 (file)
       <listitem><para>The hostname <literal>_localdnsproxy</literal> is resolved to the IP address 127.0.0.54,
       i.e. the address the local DNS proxy (see above) is listening on.</para></listitem>
 
+      <listitem><para>The files matching <filename>/etc/systemd/resolve/static.d/*.rr</filename>,
+      <filename>/run/systemd/resolve/static.d/*.rr</filename>,
+      <filename>/usr/local/lib/systemd/resolve/static.d/*.rr</filename>,
+      <filename>/usr/lib/systemd/resolve/static.d/*.rr</filename> may be used to define arbitrary
+      records. See
+      <citerefentry><refentrytitle>systemd.rr</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
+      details. Support for this may be disabled with <varname>ReadStaticRecords=no</varname>, see
+      <citerefentry><refentrytitle>resolved.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+      </para></listitem>
+
       <listitem><para>The mappings defined in <filename>/etc/hosts</filename> are resolved to their
       configured addresses and back, but they will not affect lookups for non-address types (like MX).
       Support for <filename>/etc/hosts</filename> may be disabled with <varname>ReadEtcHosts=no</varname>,
@@ -510,6 +520,7 @@ search foobar.com barbar.com
       <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>resolved.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>systemd.dns-delegate</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd.rr</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>systemd.dnssd</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>dnssec-trust-anchors.d</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
       <member><citerefentry><refentrytitle>nss-resolve</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
diff --git a/man/systemd.rr.xml b/man/systemd.rr.xml
new file mode 100644 (file)
index 0000000..d6718cc
--- /dev/null
@@ -0,0 +1,95 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="systemd.rr"
+          xmlns:xi="http://www.w3.org/2001/XInclude"
+          conditional='ENABLE_RESOLVE'>
+
+  <refentryinfo>
+    <title>systemd.rr</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd.rr</refentrytitle>
+    <manvolnum>5</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd.rr</refname>
+    <refpurpose>Local static DNS resource record definitions</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para><simplelist>
+      <member><filename>/etc/systemd/resolve/static.d/*.rr</filename></member>
+      <member><filename>/run/systemd/resolve/static.d/*.rr</filename></member>
+      <member><filename>/usr/local/lib/systemd/resolve/static.d/*.rr</filename></member>
+      <member><filename>/usr/lib/systemd/resolve/static.d/*.rr</filename></member>
+    </simplelist></para>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><filename>*.rr</filename> files may be used to define resource record sets ("RRsets") that shall be
+    resolvable locally, similar in style to address records defined by <filename>/etc/hosts</filename> (see
+    <citerefentry><refentrytitle>hosts</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
+    details). These files are read by
+    <citerefentry><refentrytitle>systemd-resolved.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    and are used to synthesize local responses to local queries matching the defined resource record set.</para>
+
+    <para>These drop-in files are in JSON format. Each file may either contain a single top-level DNS RR
+    object, or an array of one or more DNS RR objects. Each RR object has at least a <literal>key</literal>
+    subobject consisting of a <literal>name</literal> string field and a <literal>type</literal> integer
+    field (which contains the RR type in numeric form). Depending on the chosen type the RR object also has
+    the following fields:</para>
+
+    <itemizedlist>
+      <listitem><para>For A/AAAA RRs, the RR object should have an <literal>address</literal> field set to
+      either an IP address formatted as string, or an array consisting of 4 or 16 8-bit unsigned integers for
+      the IP address.</para></listitem>
+
+      <listitem><para>For PTR/NS/CNAME/DNAME RRs, the RR object should have a <literal>name</literal> field
+      set to the name the record shall point to.</para></listitem>
+    </itemizedlist>
+
+    <para>This JSON serialization of DNS RRs matches the one returned by <command>resolvectl</command>.</para>
+
+    <para>Currently no other RR types are supported.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>Examples</title>
+    <example>
+      <title>Simple A Record</title>
+      <para>To make local address lookups for <literal>foobar.example.com</literal> resolve to the
+      192.168.100.1 IPv4 address, create
+      <filename>/run/systemd/resolve/static.d/foobar_example_com.rr</filename>:
+
+      <programlisting>
+{
+        "key" : {
+                "type" : 1,
+                "name" : "foobar.example.com"
+        },
+        "address" : [ 192, 168, 100, 1 ]
+}</programlisting></para>
+
+    </example>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para><simplelist type="inline">
+      <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-resolved.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>resolved.conf</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>hosts</refentrytitle><manvolnum>5</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>resolvectl</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+    </simplelist></para>
+  </refsect1>
+
+</refentry>
index be2979343f3f0d6a66dc2c149576d40eb82c2940..b9b2e24b18123255cce60bedfcb94cc1cbd1b850 100644 (file)
@@ -36,6 +36,7 @@ systemd_resolved_extract_sources = files(
         'resolved-mdns.c',
         'resolved-resolv-conf.c',
         'resolved-socket-graveyard.c',
+        'resolved-static-records.c',
         'resolved-util.c',
         'resolved-varlink.c',
 )
index a0ef75044717925d5cb8c5dd6bff2774a1d419c1..6ec6569ae7639741a840616815c71f6acbf5d06f 100644 (file)
@@ -21,6 +21,7 @@
 #include "resolved-etc-hosts.h"
 #include "resolved-hook.h"
 #include "resolved-manager.h"
+#include "resolved-static-records.h"
 #include "resolved-timeouts.h"
 #include "set.h"
 #include "string-util.h"
@@ -910,6 +911,33 @@ static int dns_query_try_etc_hosts(DnsQuery *q) {
         return 1;
 }
 
+static int dns_query_try_static_records(DnsQuery *q) {
+        int r;
+
+        assert(q);
+
+        if (FLAGS_SET(q->flags, SD_RESOLVED_NO_SYNTHESIZE))
+                return 0;
+
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+        r = manager_static_records_lookup(
+                        q->manager,
+                        q->question_bypass ? q->question_bypass->question : q->question_utf8,
+                        &answer);
+        if (r <= 0)
+                return r;
+
+        dns_query_reset_answer(q);
+
+        q->answer = TAKE_PTR(answer);
+        q->answer_rcode = DNS_RCODE_SUCCESS;
+        q->answer_protocol = dns_synthesize_protocol(q->flags);
+        q->answer_family = dns_synthesize_family(q->flags);
+        q->answer_query_flags = SD_RESOLVED_AUTHENTICATED|SD_RESOLVED_CONFIDENTIAL|SD_RESOLVED_SYNTHETIC;
+
+        return 1;
+}
+
 static int dns_query_go_scopes(DnsQuery *q) {
         int r;
 
@@ -1038,6 +1066,14 @@ int dns_query_go(DnsQuery *q) {
             q->state != DNS_TRANSACTION_NULL)
                 return 0;
 
+        r = dns_query_try_static_records(q);
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                dns_query_complete(q, DNS_TRANSACTION_SUCCESS);
+                return 1;
+        }
+
         r = dns_query_try_etc_hosts(q);
         if (r < 0)
                 return r;
index c548320449b6faec3856bed66f9594e903dca317..8b8a66d0369bfb6f00ac3756d4c4db8e942010fb 100644 (file)
@@ -31,6 +31,7 @@ Resolve.DNSOverTLS,                config_parse_dns_over_tls_mode,       0,
 Resolve.Cache,                     config_parse_dns_cache_mode,          DNS_CACHE_MODE_YES,  offsetof(Manager, enable_cache)
 Resolve.DNSStubListener,           config_parse_dns_stub_listener_mode,  0,                   offsetof(Manager, dns_stub_listener_mode)
 Resolve.ReadEtcHosts,              config_parse_bool,                    0,                   offsetof(Manager, read_etc_hosts)
+Resolve.ReadStaticRecords,         config_parse_bool,                    0,                   offsetof(Manager, read_static_records)
 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)
index 19ff92bfca5a9fe91a45568d755dc030e6eaffe8..25a51ed02b0423efd1b59b1b132e4beed7bf637e 100644 (file)
@@ -49,6 +49,7 @@
 #include "resolved-mdns.h"
 #include "resolved-resolv-conf.h"
 #include "resolved-socket-graveyard.h"
+#include "resolved-static-records.h"
 #include "resolved-util.h"
 #include "resolved-varlink.h"
 #include "set.h"
@@ -637,6 +638,7 @@ static void manager_set_defaults(Manager *m) {
         m->enable_cache = DNS_CACHE_MODE_YES;
         m->dns_stub_listener_mode = DNS_STUB_LISTENER_YES;
         m->read_etc_hosts = true;
+        m->read_static_records = true;
         m->resolve_unicast_single_label = false;
         m->cache_from_localhost = false;
         m->stale_retention_usec = 0;
@@ -660,6 +662,7 @@ static int manager_dispatch_reload_signal(sd_event_source *s, const struct signa
         m->delegates = hashmap_free(m->delegates);
         dns_trust_anchor_flush(&m->trust_anchor);
         manager_etc_hosts_flush(m);
+        manager_static_records_flush(m);
 
         manager_set_defaults(m);
 
@@ -730,6 +733,7 @@ int manager_new(Manager **ret) {
                 .read_resolv_conf = true,
                 .need_builtin_fallbacks = true,
                 .etc_hosts_last = USEC_INFINITY,
+                .static_records_last = USEC_INFINITY,
 
                 .sigrtmin18_info.memory_pressure_handler = manager_memory_pressure,
                 .sigrtmin18_info.memory_pressure_userdata = m,
@@ -918,6 +922,7 @@ Manager* manager_free(Manager *m) {
 
         dns_trust_anchor_flush(&m->trust_anchor);
         manager_etc_hosts_flush(m);
+        manager_static_records_flush(m);
 
         while ((sb = hashmap_first(m->dns_service_browsers)))
                 dns_service_browser_free(sb);
index 4f595e6d04c249616abc50caaf821a17ade20fcb..d72e9104d79d04796a2cfef41b4f47775d0e2b87 100644 (file)
@@ -123,6 +123,12 @@ typedef struct Manager {
         struct stat etc_hosts_stat;
         bool read_etc_hosts;
 
+        /* Data from {/etc,/run,/usr/local/lib,/usr/lib}/systemd/resolve/static.d/ */
+        Hashmap *static_records;
+        usec_t static_records_last;
+        Set *static_records_stat;
+        bool read_static_records;
+
         /* List of refused DNS Record Types */
         Set *refuse_record_types;
 
diff --git a/src/resolve/resolved-static-records.c b/src/resolve/resolved-static-records.c
new file mode 100644 (file)
index 0000000..4aa6f2e
--- /dev/null
@@ -0,0 +1,226 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-json.h"
+
+#include "alloc-util.h"
+#include "conf-files.h"
+#include "constants.h"
+#include "dns-answer.h"
+#include "dns-domain.h"
+#include "dns-question.h"
+#include "dns-rr.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "hashmap.h"
+#include "json-util.h"
+#include "log.h"
+#include "resolved-manager.h"
+#include "resolved-static-records.h"
+#include "set.h"
+#include "stat-util.h"
+
+/* This implements a mechanism to extend what systemd-resolved resolves locally, via .rr drop-ins in
+ * {/etc,/run,/usr/local/lib,/usr/lib}/systemd/resolve/static.d/. These files are in JSON format, and are RR
+ * serializations, that match the usual way we serialize RRs to JSON.
+ *
+ * Note that this deliberately doesn't use the (probably more user-friendly) classic DNS zone file format,
+ * to keep things a bit simpler, and symmetric to the places we currently already generate JSON
+ * serializations of DNS RRs. Also note the semantics are different from DNS zone file format, for example
+ * regarding delegation (i.e. the RRs defined here have no effect on subdomains), which is probably nicer for
+ * one-off mappings of domains to specific resources. Or in other words, this is supposed to be a drop-in
+ * based alternative to /etc/hosts, not a one to DNS zone files. (The JSON format is also a lot more
+ * extensible to us, for example we could teach it to map certain lookups to specific DNS errors, or extend
+ * it so that subdomains always get NXDOMAIN or similar).
+ *
+ * (That said, if there's a good reason, we can also support *.zone files too one day).
+ */
+
+/* Recheck static records at most once every 2s */
+#define STATIC_RECORDS_RECHECK_USEC (2*USEC_PER_SEC)
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
+                answer_by_name_hash_ops,
+                char,
+                dns_name_hash_func,
+                dns_name_compare_func,
+                DnsAnswer,
+                dns_answer_unref);
+
+static int load_static_record_file_item(sd_json_variant *rj, Hashmap **records) {
+        int r;
+
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+        r = dns_resource_record_from_json(rj, &rr);
+        if (r < 0)
+                return log_error_errno(r, "Failed to parse DNS record from JSON: %m");
+
+        _cleanup_(dns_answer_unrefp) DnsAnswer *a =
+                hashmap_remove(*records, dns_resource_key_name(rr->key));
+
+        r = dns_answer_add_extend_full(&a, rr, /* ifindex= */ 0, DNS_ANSWER_AUTHENTICATED, /* rrsig= */ NULL, /* until= */ USEC_INFINITY);
+        if (r < 0)
+                return log_error_errno(r, "Failed to append RR to DNS answer: %m");
+
+        DnsAnswerItem *item = ASSERT_PTR(ordered_set_first(a->items));
+
+        r = hashmap_ensure_put(records, &answer_by_name_hash_ops, dns_resource_key_name(item->rr->key), a);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add RR to static record set: %m");
+
+        TAKE_PTR(a);
+
+        log_debug("Added static resource record: %s", dns_resource_record_to_string(rr));
+        return 1;
+}
+
+static int load_static_record_file(const ConfFile *cf, Hashmap **records, Set **stats) {
+        int r;
+
+        assert(cf);
+        assert(records);
+        assert(stats);
+
+        /* Have we seen this file before? Then we might as well skip loading it again, it wouldn't have any
+         * additional effect anyway. (Note: masking/overriding has already been applied before we reach this
+         * point, here everything is purely additive.) */
+        if (set_contains(*stats, &cf->st))
+                return 0;
+
+        _cleanup_free_ struct stat *st_copy = memdup(&cf->st, sizeof(cf->st));
+        if (!st_copy)
+                return log_oom();
+
+        if (set_ensure_consume(stats, &inode_unmodified_hash_ops, TAKE_PTR(st_copy)) < 0)
+                return log_oom();
+
+        _cleanup_fclose_ FILE *f = NULL;
+        r = xfopenat(cf->fd, /* path= */ NULL, "re", /* open_flags= */ 0, &f);
+        if (r < 0) {
+                log_warning_errno(r, "Failed to open '%s', skipping: %m", cf->result);
+                return 0;
+        }
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
+        unsigned line = 0, column = 0;
+        r = sd_json_parse_file(f, cf->result, /* flags= */ 0, &j, &line, &column);
+        if (r < 0) {
+                if (line > 0)
+                        log_syntax(/* unit= */ NULL, LOG_WARNING, cf->result, line, r, "Failed to parse JSON, skipping: %m");
+                else
+                        log_warning_errno(r, "Failed to parse JSON file '%s', skipping: %m", cf->result);
+                return 0;
+        }
+
+        if (sd_json_variant_is_array(j)) {
+                sd_json_variant *i;
+                int ret = 0;
+                JSON_VARIANT_ARRAY_FOREACH(i, j)
+                        RET_GATHER(ret, load_static_record_file_item(i, records));
+                if (ret < 0)
+                        return ret;
+        } else if (sd_json_variant_is_object(j)) {
+                r = load_static_record_file_item(j, records);
+                if (r < 0)
+                        return r;
+        } else {
+                log_warning("JSON file '%s' contains neither array nor object, skipping.", cf->result);
+                return 0;
+        }
+
+        return 1;
+}
+
+static int manager_static_records_read(Manager *m) {
+        int r;
+
+        usec_t ts;
+        assert_se(sd_event_now(m->event, CLOCK_BOOTTIME, &ts) >= 0);
+
+        /* See if we checked the static records db recently already */
+        if (m->static_records_last != USEC_INFINITY && usec_add(m->static_records_last, STATIC_RECORDS_RECHECK_USEC) > ts)
+                return 0;
+
+        m->static_records_last = ts;
+
+        ConfFile **files = NULL;
+        size_t n_files = 0;
+        CLEANUP_ARRAY(files, n_files, conf_file_free_many);
+
+        r = conf_files_list_nulstr_full(
+                        ".rr",
+                        /* root= */ NULL,
+                        CONF_FILES_REGULAR|CONF_FILES_FILTER_MASKED|CONF_FILES_WARN,
+                        CONF_PATHS_NULSTR("systemd/resolve/static.d/"),
+                        &files,
+                        &n_files);
+        if (r < 0)
+                return log_error_errno(r, "Failed to enumerate static record drop-ins: %m");
+
+        /* Let's suppress reloads if nothing changed. For that keep the set of inodes from the previous
+         * reload around, and see if there are any changes on them. */
+        bool reload;
+        if (set_size(m->static_records_stat) != n_files)
+                reload = true;
+        else {
+                reload = false;
+                FOREACH_ARRAY(f, files, n_files)
+                        if (!set_contains(m->static_records_stat, &(*f)->st)) {
+                                reload = true;
+                                break;
+                        }
+        }
+
+        if (!reload) {
+                log_debug("No static record files changed, not re-reading.");
+                return 0;
+        }
+
+        _cleanup_(hashmap_freep) Hashmap *records = NULL;
+        _cleanup_(set_freep) Set *stats = NULL;
+        FOREACH_ARRAY(f, files, n_files)
+                (void) load_static_record_file(*f, &records, &stats);
+
+        hashmap_free(m->static_records);
+        m->static_records = TAKE_PTR(records);
+
+        set_free(m->static_records_stat);
+        m->static_records_stat = TAKE_PTR(stats);
+
+        return 0;
+}
+
+int manager_static_records_lookup(Manager *m, DnsQuestion *q, DnsAnswer **answer) {
+        int r;
+
+        assert(m);
+        assert(q);
+        assert(answer);
+
+        if (!m->read_static_records)
+                return 0;
+
+        (void) manager_static_records_read(m);
+
+        const char *n = dns_question_first_name(q);
+        if (!n)
+                return 0;
+
+        DnsAnswer *f = hashmap_get(m->static_records, n);
+        if (!f)
+                return 0;
+
+        r = dns_answer_extend(answer, f);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+void manager_static_records_flush(Manager *m) {
+        assert(m);
+
+        m->static_records = hashmap_free(m->static_records);
+        m->static_records_stat = set_free(m->static_records_stat);
+        m->static_records_last = USEC_INFINITY;
+}
diff --git a/src/resolve/resolved-static-records.h b/src/resolve/resolved-static-records.h
new file mode 100644 (file)
index 0000000..f50c70e
--- /dev/null
@@ -0,0 +1,7 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "resolved-forward.h"
+
+void manager_static_records_flush(Manager *m);
+int manager_static_records_lookup(Manager *m, DnsQuestion* q, DnsAnswer **answer);
index 656bc7c0eb7eaa403dd47e4a46695d661f9a68b8..147d30845b1299bd5eaf37c7c2249d5031f4eec3 100644 (file)
@@ -39,6 +39,7 @@
 #DNSStubListener=yes
 #DNSStubListenerExtra=
 #ReadEtcHosts=yes
+#ReadStaticRecords=yes
 #ResolveUnicastSingleLabel=no
 #StaleRetentionSec=0
 #RefuseRecordTypes=
index b3656da94043adc8fc457ae098a9a1eaf091adaa..bb1cf9576c2924d33f074ced917f95c7ab4d22d2 100755 (executable)
@@ -1487,6 +1487,55 @@ EOF
     grep -qF "1.2.3.4" "$RUN_OUT"
 }
 
+testcase_static_record() {
+    mkdir -p /run/systemd/resolve/static.d/
+    cat >/run/systemd/resolve/static.d/statictest.rr <<EOF
+{
+        "key": { "name" : "statictest.waldo", "type" : 1 },
+        "address" : [ 5, 7, 9, 11 ]
+}
+EOF
+    cat >/run/systemd/resolve/static.d/statictest2.rr <<EOF
+[
+        {
+                "key": { "name" : "statictest2.waldo", "type" : 1 },
+                "address" : [ 5, 7, 9, 12 ]
+        },
+        {
+                "key": { "name" : "statictest2.waldo", "type" : 28 },
+                "address" : [ 10, 11, 10, 11, 10, 11, 10, 11, 10, 11, 10, 11, 10, 11, 10, 12 ]
+        }
+]
+EOF
+    cat >/run/systemd/resolve/static.d/garbage.rr <<EOF
+[
+        {
+                "key": { "name" : "invalid...domain", "type" : 1 },
+                "address" : [ 5, 7, 9, 12 ]
+        },
+]
+EOF
+    cat >/run/systemd/resolve/static.d/garbage2.rr <<EOF
+[
+        {
+                "key": { "name" : "piff", "type" : 9 },
+        },
+]
+EOF
+
+    systemctl reload systemd-resolved
+
+    run resolvectl query statictest.waldo
+    grep -qF 5.7.9.11 "$RUN_OUT"
+
+    run resolvectl query statictest2.waldo
+    grep -qF 5.7.9.12 "$RUN_OUT"
+    grep -qF a0b:a0b:a0b:a0b:a0b:a0b:a0b:a0c "$RUN_OUT"
+
+    rm /run/systemd/resolve/static.d/statictest*.rr /run/systemd/resolve/static.d/garbage*.rr
+    systemctl reload systemd-resolved
+}
+
 # PRE-SETUP
 systemctl unmask systemd-resolved.service
 systemctl enable --now systemd-resolved.service