]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
Implement DNS notifications from resolved via varlink
authorSuraj Krishnan <72937403+surajkrishnan14@users.noreply.github.com>
Tue, 26 Apr 2022 22:09:02 +0000 (17:09 -0500)
committerLuca Boccassi <luca.boccassi@gmail.com>
Fri, 9 Sep 2022 08:22:57 +0000 (09:22 +0100)
* The new varlink interface exposes a method to subscribe to DNS
resolutions on the system. The socket permissions are open for owner and
group only.
* Notifications are sent to subscriber(s), if any, after successful
resolution of A and AAAA records.

This feature could be used by applications for auditing/logging services
downstream of the resolver. It could also be used to asynchronously
update the firewall. For example, a system that has a tightly configured
firewall could open up connections selectively to known good hosts based
on a known allow-list of hostnames. Of course, updating the firewall
asynchronously will require other design considerations (such as
queueing packets in the user space while a verdict is made).

See also:
https://lists.freedesktop.org/archives/systemd-devel/2022-August/048202.html
https://lists.freedesktop.org/archives/systemd-devel/2022-February/047441.html

12 files changed:
man/org.freedesktop.resolve1.xml
man/resolved.conf.xml
src/resolve/resolved-bus.c
src/resolve/resolved-dns-query.c
src/resolve/resolved-dns-query.h
src/resolve/resolved-gperf.gperf
src/resolve/resolved-manager.c
src/resolve/resolved-manager.h
src/resolve/resolved-varlink.c
src/resolve/resolved.conf.in
test/knot-data/zones/onlinesign.test.zone
test/units/testsuite-75.sh

index 54f0a18418ec5e2ab44a763fed62e8bcc038b1ae..d3aedbc13e3d132878db11e47bf376a10a4515cf 100644 (file)
@@ -149,6 +149,7 @@ node /org/freedesktop/resolve1 {
       readonly s DNSStubListener = '...';
       @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
       readonly s ResolvConfMode = '...';
+      readonly b Monitor = ...;
   };
   interface org.freedesktop.DBus.Peer { ... };
   interface org.freedesktop.DBus.Introspectable { ... };
@@ -250,6 +251,8 @@ node /org/freedesktop/resolve1 {
 
     <variablelist class="dbus-property" generated="True" extra-ref="ResolvConfMode"/>
 
+    <variablelist class="dbus-property" generated="True" extra-ref="Monitor"/>
+
     <!--End of Autogenerated section-->
 
     <refsect2>
@@ -634,6 +637,8 @@ node /org/freedesktop/resolve1 {
       enabled. Possible values are <literal>yes</literal> (enabled), <literal>no</literal> (disabled),
       <literal>udp</literal> (only the UDP listener is enabled), and <literal>tcp</literal> (only the TCP
       listener is enabled).</para>
+
+      <para>The <varname>Monitor</varname> boolean property reports whether DNS monitoring is enabled.</para>
     </refsect2>
   </refsect1>
 
index 3c56b76748667d876f3ea1d213a2ae710623fe70..a0ccaec3995349cb5477e27207edaad94935dc5a 100644 (file)
@@ -329,6 +329,15 @@ DNSStubListenerExtra=udp:[2001:db8:0:f102::13]:9953</programlisting>
         url="https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-statement-dotless-domains-considered-harmful/">IAB
         Statement</ulink>, and may create a privacy and security risk.</para></listitem>
       </varlistentry>
+
+      <varlistentry>
+        <term><varname>Monitor=</varname></term>
+        <listitem><para>Takes a boolean argument. If <literal>true</literal>,
+        <command>systemd-resolved</command> will enable a varlink interface at
+        <filename>/run/systemd/resolve/io.systemd.Resolve.Monitor</filename> that exposes methods for clients to subscribe to
+        DNS resolution notifications on the system. If <literal>false</literal> (the default), the interface is disabled.
+        </para></listitem>
+      </varlistentry>
     </variablelist>
   </refsect1>
 
index 1304965d4e28f3c5e606dbc8f51f8e36baee374b..044448ad106fc76668d8acf75ea8d3651d9688e0 100644 (file)
@@ -517,6 +517,9 @@ static int bus_method_resolve_hostname(sd_bus_message *message, void *userdata,
 
         q->bus_request = sd_bus_message_ref(message);
         q->request_family = family;
+        q->request_name = strdup(hostname);
+        if (!q->request_name)
+                return log_oom();
         q->complete = bus_method_resolve_hostname_complete;
 
         r = dns_query_bus_track(q, message);
@@ -839,6 +842,9 @@ static int bus_method_resolve_record(sd_bus_message *message, void *userdata, sd
 
         q->bus_request = sd_bus_message_ref(message);
         q->complete = bus_method_resolve_record_complete;
+        q->request_name = strdup(name);
+        if (!q->request_name)
+                return log_oom();
 
         r = dns_query_bus_track(q, message);
         if (r < 0)
@@ -1196,6 +1202,9 @@ static int resolve_service_hostname(DnsQuery *q, DnsResourceRecord *rr, int ifin
                 return r;
 
         aux->request_family = q->request_family;
+        aux->request_name = strdup(rr->srv.name);
+        if (!aux->request_name)
+                return log_oom();
         aux->complete = resolve_service_hostname_complete;
 
         r = dns_query_make_auxiliary(aux, q);
@@ -2115,6 +2124,7 @@ static const sd_bus_vtable resolve_vtable[] = {
         SD_BUS_PROPERTY("DNSSECNegativeTrustAnchors", "as", bus_property_get_ntas, 0, 0),
         SD_BUS_PROPERTY("DNSStubListener", "s", bus_property_get_dns_stub_listener_mode, offsetof(Manager, dns_stub_listener_mode), 0),
         SD_BUS_PROPERTY("ResolvConfMode", "s", bus_property_get_resolv_conf_mode, 0, 0),
+        SD_BUS_PROPERTY("Monitor", "b", bus_property_get_bool, offsetof(Manager, enable_varlink_notifications), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
 
         SD_BUS_METHOD_WITH_ARGS("ResolveHostname",
                                 SD_BUS_ARGS("i", ifindex, "s", name, "i", family, "t", flags),
index d9c61148364eac737fbe60e49bc32ef381153e52..584495c779d98c1234df81b3242a98ac0612aa70 100644 (file)
@@ -427,6 +427,7 @@ DnsQuery *dns_query_free(DnsQuery *q) {
         }
 
         free(q->request_address_string);
+        free(q->request_name);
 
         if (q->manager) {
                 LIST_REMOVE(queries, q->manager->dns_queries, q);
@@ -585,6 +586,13 @@ void dns_query_complete(DnsQuery *q, DnsTransactionState state) {
 
         q->state = state;
 
+        if (state == DNS_TRANSACTION_SUCCESS && set_size(q->manager->varlink_subscription) > 0) {
+                DnsQuestion *question = q->request_packet ? q->request_packet->question : NULL;
+                const char *query_name = question ? dns_question_first_name(question) : q->request_name;
+                if (query_name)
+                        (void) send_dns_notification(q->manager, q->answer, query_name);
+        }
+
         dns_query_stop(q);
         if (q->complete)
                 q->complete(q);
index 43a833a08a2bd6ef012b7e8f1cc0e2bac57ae6a8..0b00465008dd2826bca4b9a9ac84c3273b7037e6 100644 (file)
@@ -95,6 +95,7 @@ struct DnsQuery {
         union in_addr_union request_address;
         unsigned block_all_complete;
         char *request_address_string;
+        char *request_name;
 
         /* DNS stub information */
         DnsPacket *request_packet;
index eab4c7ee14a03ea75435d74bc6f6dd8ecbb8aa15..ee0c9b71e724e343795af3e50a2b83d017fee74a 100644 (file)
@@ -32,3 +32,4 @@ Resolve.ReadEtcHosts,              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.Monitor,                   config_parse_bool,                    0,                   offsetof(Manager, enable_varlink_notifications)
index 86acd7ef8cf91f1883e286f5d5274920fa7fb757..8385543fdf7be4fac68806268e63bf2712f7a8c4 100644 (file)
@@ -1054,6 +1054,65 @@ static int manager_ipv6_send(
         return sendmsg_loop(fd, &mh, 0);
 }
 
+int send_dns_notification(Manager *m, DnsAnswer *answer, const char *query_name) {
+        _cleanup_free_ char *normalized = NULL;
+        DnsResourceRecord *rr;
+        int ifindex, r;
+        _cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
+        Varlink *connection;
+
+        assert(m);
+
+        if (set_isempty(m->varlink_subscription))
+                return 0;
+
+        DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, answer) {
+                _cleanup_(json_variant_unrefp) JsonVariant *entry = NULL;
+
+                if (rr->key->type == DNS_TYPE_A) {
+                        struct in_addr *addr = &rr->a.in_addr;
+                        r = json_build(&entry,
+                                JSON_BUILD_OBJECT(JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)),
+                                                  JSON_BUILD_PAIR_INTEGER("family", AF_INET),
+                                                  JSON_BUILD_PAIR_IN4_ADDR("address", addr),
+                                                  JSON_BUILD_PAIR_STRING("type", "A")));
+                } else if (rr->key->type == DNS_TYPE_AAAA) {
+                        struct in6_addr *addr6 = &rr->aaaa.in6_addr;
+                        r = json_build(&entry,
+                                JSON_BUILD_OBJECT(JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)),
+                                                  JSON_BUILD_PAIR_INTEGER("family", AF_INET6),
+                                                  JSON_BUILD_PAIR_IN6_ADDR("address", addr6),
+                                                  JSON_BUILD_PAIR_STRING("type", "AAAA")));
+                } else
+                        continue;
+                if (r < 0) {
+                        log_debug_errno(r, "Failed to build json object: %m");
+                        continue;
+                }
+
+                r = json_variant_append_array(&array, entry);
+                if (r < 0)
+                        return log_debug_errno(r, "Failed to append notification entry to array: %m");
+        }
+
+        if (json_variant_is_blank_object(array))
+                return 0;
+
+        r = dns_name_normalize(query_name, 0, &normalized);
+        if (r < 0)
+                return log_debug_errno(r, "Failed to normalize query name: %m");
+
+        SET_FOREACH(connection, m->varlink_subscription) {
+                r = varlink_notifyb(connection,
+                                    JSON_BUILD_OBJECT(JSON_BUILD_PAIR("addresses",
+                                                      JSON_BUILD_VARIANT(array)),
+                                                      JSON_BUILD_PAIR("name", JSON_BUILD_STRING(normalized))));
+                if (r < 0)
+                        log_debug_errno(r, "Failed to send notification, ignoring: %m");
+        }
+        return 0;
+}
+
 int manager_send(
                 Manager *m,
                 int fd,
index 35e0068a83e8c132305b241f50ff029e7126576c..a55ac90b8eeb7c76970d2e728db3481517160b4d 100644 (file)
@@ -41,6 +41,7 @@ struct Manager {
         DnsOverTlsMode dns_over_tls_mode;
         DnsCacheMode enable_cache;
         bool cache_from_localhost;
+        bool enable_varlink_notifications;
         DnsStubListenerMode dns_stub_listener_mode;
 
 #if ENABLE_DNS_OVER_TLS
@@ -147,6 +148,9 @@ struct Manager {
         Hashmap *polkit_registry;
 
         VarlinkServer *varlink_server;
+        VarlinkServer *varlink_notification_server;
+
+        Set *varlink_subscription;
 
         sd_event_source *clock_change_event_source;
 
@@ -164,6 +168,8 @@ int manager_start(Manager *m);
 
 uint32_t manager_find_mtu(Manager *m);
 
+int send_dns_notification(Manager *m, DnsAnswer *answer, const char *query_name);
+
 int manager_write(Manager *m, int fd, DnsPacket *p);
 int manager_send(Manager *m, int fd, int ifindex, int family, const union in_addr_union *destination, uint16_t port, const union in_addr_union *source, DnsPacket *p);
 int manager_recv(Manager *m, int fd, DnsProtocol protocol, DnsPacket **ret);
index dc5a98acbd56c6a932d76e824c756926298087d5..96c526a2144036ca5c7de70fb7480a3b1e6413a7 100644 (file)
@@ -100,6 +100,19 @@ static void vl_on_disconnect(VarlinkServer *s, Varlink *link, void *userdata) {
         dns_query_complete(q, DNS_TRANSACTION_ABORTED);
 }
 
+static void vl_on_notification_disconnect(VarlinkServer *s, Varlink *link, void *userdata) {
+        Manager *m = ASSERT_PTR(userdata);
+
+        assert(s);
+        assert(link);
+
+        Varlink *removed_link = set_remove(m->varlink_subscription, link);
+        if (removed_link) {
+                varlink_unref(removed_link);
+                log_debug("%u monitor clients remain active", set_size(m->varlink_subscription));
+        }
+}
+
 static bool validate_and_mangle_flags(
                 const char *name,
                 uint64_t *flags,
@@ -337,6 +350,9 @@ static int vl_method_resolve_hostname(Varlink *link, JsonVariant *parameters, Va
         q->varlink_request = varlink_ref(link);
         varlink_set_userdata(link, q);
         q->request_family = p.family;
+        q->request_name = strdup(p.name);
+        if (!q->request_name)
+                return log_oom();
         q->complete = vl_method_resolve_hostname_complete;
 
         r = dns_query_go(q);
@@ -519,6 +535,32 @@ static int vl_method_resolve_address(Varlink *link, JsonVariant *parameters, Var
         return 1;
 }
 
+static int vl_method_subscribe_dns_resolves(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
+        Manager *m;
+        int r;
+
+        assert(link);
+
+        m = varlink_server_get_userdata(varlink_get_server(link));
+        assert(m);
+
+        if (json_variant_elements(parameters) > 0)
+                return varlink_error_invalid_parameter(link, parameters);
+
+        r = set_ensure_put(&m->varlink_subscription, NULL, link);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add subscription to set: %m");
+        varlink_ref(link);
+
+        log_debug("%u clients now attached for varlink notifications", set_size(m->varlink_subscription));
+
+        /* if the client didn't set the more flag, return an empty response and close the connection */
+        if (!FLAGS_SET(flags, VARLINK_METHOD_MORE))
+                return varlink_reply(link, NULL);
+
+        return 1;
+}
+
 int manager_varlink_init(Manager *m) {
         _cleanup_(varlink_server_unrefp) VarlinkServer *s = NULL;
         int r;
@@ -554,6 +596,39 @@ int manager_varlink_init(Manager *m) {
                 return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
 
         m->varlink_server = TAKE_PTR(s);
+
+        if (m->enable_varlink_notifications) {
+                if (m->varlink_notification_server)
+                        return 0;
+
+                r = varlink_server_new(&s, VARLINK_SERVER_ACCOUNT_UID);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to allocate varlink server object: %m");
+
+                varlink_server_set_userdata(s, m);
+
+                r = varlink_server_bind_method_many(
+                                s,
+                                "io.systemd.Resolve.Monitor.SubscribeDnsResolves",
+                                vl_method_subscribe_dns_resolves);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to register varlink methods: %m");
+
+                r = varlink_server_bind_disconnect(s, vl_on_notification_disconnect);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to register varlink disconnect handler: %m");
+
+                r = varlink_server_listen_address(s, "/run/systemd/resolve/io.systemd.Resolve.Monitor", 0660);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to bind to varlink socket: %m");
+
+                r = varlink_server_attach_event(s, m->event, SD_EVENT_PRIORITY_NORMAL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
+
+                m->varlink_notification_server = TAKE_PTR(s);
+        }
+
         return 0;
 }
 
@@ -561,4 +636,5 @@ void manager_varlink_done(Manager *m) {
         assert(m);
 
         m->varlink_server = varlink_server_unref(m->varlink_server);
+        m->varlink_notification_server = varlink_server_unref(m->varlink_notification_server);
 }
index 6d4176df525d7db1ac8197b67b1abab878397c49..1f36e9cb2b2b338b4d74c930d6bc03961059e568 100644 (file)
@@ -32,3 +32,4 @@
 #DNSStubListenerExtra=
 #ReadEtcHosts=yes
 #ResolveUnicastSingleLabel=no
+#Monitor=no
index 686313cc2ce7cda2ecf9fc66391998141a043f10..c12c6b33965f159aa114e27a386b6351fff6d0e2 100644 (file)
@@ -19,3 +19,4 @@ $ORIGIN onlinesign.test.
 
 ; No A/AAAA record for the $ORIGIN
 sub                  A 10.0.0.133
+secondsub            A 10.0.0.134
index 5158536f4989c09d46c7436f752eea51bfed11f5..26ad109538585eb99ff2f9fa53f5f836fd6347be 100755 (executable)
@@ -8,6 +8,8 @@ set -o pipefail
 : >/failed
 
 RUN_OUT="$(mktemp)"
+NOTIFICATION_SUBSCRIPTION_SCRIPT="/tmp/subscribe.sh"
+NOTIFICATION_LOGS="/tmp/notifications.txt"
 
 run() {
     "$@" |& tee "$RUN_OUT"
@@ -34,10 +36,22 @@ DNSSEC=allow-downgrade
 DNS=10.0.0.1
 EOF
 
+# Script to dump DNS notifications to a txt file
+cat >$NOTIFICATION_SUBSCRIPTION_SCRIPT <<EOF
+#!/bin/sh
+printf '
+{
+  "method": "io.systemd.Resolve.Monitor.SubscribeDnsResolves",
+  "more": true
+}\0' | nc -U /run/systemd/resolve/io.systemd.Resolve.Monitor > $NOTIFICATION_LOGS
+EOF
+chmod a+x $NOTIFICATION_SUBSCRIPTION_SCRIPT
+
 {
     echo "FallbackDNS="
     echo "DNSSEC=allow-downgrade"
     echo "DNSOverTLS=opportunistic"
+    echo "Monitor=yes"
 } >>/etc/systemd/resolved.conf
 ln -svf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf
 # Override the default NTA list, which turns off DNSSEC validation for (among
@@ -78,6 +92,13 @@ networkctl status
 resolvectl status
 resolvectl log-level debug
 
+# Verify that DNS notifications are enabled (Monitor=yes)
+run busctl get-property org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager Monitor
+grep -qF 'b true' "$RUN_OUT"
+
+# Start monitoring DNS notifications
+systemd-run $NOTIFICATION_SUBSCRIPTION_SCRIPT
+
 # We need to manually propagate the DS records of onlinesign.test. to the parent
 # zone, since they're generated online
 knotc zone-begin test.
@@ -99,6 +120,7 @@ knotc reload
 # Sanity check
 run getent -s resolve hosts ns1.unsigned.test
 grep -qE "^10\.0\.0\.1\s+ns1\.unsigned\.test" "$RUN_OUT"
+grep -aF "ns1.unsigned.test" $NOTIFICATION_LOGS | grep -qF "[10,0,0,1]"
 
 # Issue: https://github.com/systemd/systemd/issues/18812
 # PR: https://github.com/systemd/systemd/pull/18896
@@ -191,6 +213,7 @@ grep -qF "; fully validated" "$RUN_OUT"
 run resolvectl query -t A cname-chain.signed.test
 grep -qF "follow14.final.signed.test IN A 10.0.0.14" "$RUN_OUT"
 grep -qF "authenticated: yes" "$RUN_OUT"
+grep -aF "cname-chain.signed.test" $NOTIFICATION_LOGS | grep -qF "[10,0,0,14]"
 # Non-existing RR + CNAME chain
 run dig +dnssec AAAA cname-chain.signed.test
 grep -qF "status: NOERROR" "$RUN_OUT"
@@ -226,6 +249,10 @@ run resolvectl query -t TXT this.should.be.authenticated.wild.onlinesign.test
 grep -qF 'this.should.be.authenticated.wild.onlinesign.test IN TXT "this is an onlinesign wildcard"' "$RUN_OUT"
 grep -qF "authenticated: yes" "$RUN_OUT"
 
+# Resolve via dbus method
+run busctl call org.freedesktop.resolve1 /org/freedesktop/resolve1 org.freedesktop.resolve1.Manager ResolveHostname 'isit' 0 secondsub.onlinesign.test 0 0
+grep -qF '10 0 0 134 "secondsub.onlinesign.test"' "$RUN_OUT"
+grep -aF "secondsub.onlinesign.test" $NOTIFICATION_LOGS | grep -qF "[10,0,0,134]"
 
 : "--- ZONE: untrusted.test (DNSSEC without propagated DS records) ---"
 run dig +short untrusted.test
@@ -244,6 +271,7 @@ grep -qF "authenticated: no" "$RUN_OUT"
 #run dig +dnssec this.does.not.exist.untrusted.test
 #grep -qF "status: NXDOMAIN" "$RUN_OUT"
 
+cat $NOTIFICATION_LOGS
 
 touch /testok
 rm /failed