From: Suraj Krishnan <72937403+surajkrishnan14@users.noreply.github.com> Date: Tue, 26 Apr 2022 22:09:02 +0000 (-0500) Subject: Implement DNS notifications from resolved via varlink X-Git-Tag: v252-rc1~221 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=cb456374e096f0ebe9b70d7ddd98e16a4be24ee6;p=thirdparty%2Fsystemd.git Implement DNS notifications from resolved via varlink * 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 --- diff --git a/man/org.freedesktop.resolve1.xml b/man/org.freedesktop.resolve1.xml index 54f0a18418e..d3aedbc13e3 100644 --- a/man/org.freedesktop.resolve1.xml +++ b/man/org.freedesktop.resolve1.xml @@ -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 { + + @@ -634,6 +637,8 @@ node /org/freedesktop/resolve1 { enabled. Possible values are yes (enabled), no (disabled), udp (only the UDP listener is enabled), and tcp (only the TCP listener is enabled). + + The Monitor boolean property reports whether DNS monitoring is enabled. diff --git a/man/resolved.conf.xml b/man/resolved.conf.xml index 3c56b767486..a0ccaec3995 100644 --- a/man/resolved.conf.xml +++ b/man/resolved.conf.xml @@ -329,6 +329,15 @@ DNSStubListenerExtra=udp:[2001:db8:0:f102::13]:9953 url="https://www.iab.org/documents/correspondence-reports-documents/2013-2/iab-statement-dotless-domains-considered-harmful/">IAB Statement, and may create a privacy and security risk. + + + Monitor= + Takes a boolean argument. If true, + systemd-resolved will enable a varlink interface at + /run/systemd/resolve/io.systemd.Resolve.Monitor that exposes methods for clients to subscribe to + DNS resolution notifications on the system. If false (the default), the interface is disabled. + + diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c index 1304965d4e2..044448ad106 100644 --- a/src/resolve/resolved-bus.c +++ b/src/resolve/resolved-bus.c @@ -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), diff --git a/src/resolve/resolved-dns-query.c b/src/resolve/resolved-dns-query.c index d9c61148364..584495c779d 100644 --- a/src/resolve/resolved-dns-query.c +++ b/src/resolve/resolved-dns-query.c @@ -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); diff --git a/src/resolve/resolved-dns-query.h b/src/resolve/resolved-dns-query.h index 43a833a08a2..0b00465008d 100644 --- a/src/resolve/resolved-dns-query.h +++ b/src/resolve/resolved-dns-query.h @@ -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; diff --git a/src/resolve/resolved-gperf.gperf b/src/resolve/resolved-gperf.gperf index eab4c7ee14a..ee0c9b71e72 100644 --- a/src/resolve/resolved-gperf.gperf +++ b/src/resolve/resolved-gperf.gperf @@ -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) diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index 86acd7ef8cf..8385543fdf7 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -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, diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h index 35e0068a83e..a55ac90b8ee 100644 --- a/src/resolve/resolved-manager.h +++ b/src/resolve/resolved-manager.h @@ -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); diff --git a/src/resolve/resolved-varlink.c b/src/resolve/resolved-varlink.c index dc5a98acbd5..96c526a2144 100644 --- a/src/resolve/resolved-varlink.c +++ b/src/resolve/resolved-varlink.c @@ -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); } diff --git a/src/resolve/resolved.conf.in b/src/resolve/resolved.conf.in index 6d4176df525..1f36e9cb2b2 100644 --- a/src/resolve/resolved.conf.in +++ b/src/resolve/resolved.conf.in @@ -32,3 +32,4 @@ #DNSStubListenerExtra= #ReadEtcHosts=yes #ResolveUnicastSingleLabel=no +#Monitor=no diff --git a/test/knot-data/zones/onlinesign.test.zone b/test/knot-data/zones/onlinesign.test.zone index 686313cc2ce..c12c6b33965 100644 --- a/test/knot-data/zones/onlinesign.test.zone +++ b/test/knot-data/zones/onlinesign.test.zone @@ -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 diff --git a/test/units/testsuite-75.sh b/test/units/testsuite-75.sh index 5158536f498..26ad1095385 100755 --- a/test/units/testsuite-75.sh +++ b/test/units/testsuite-75.sh @@ -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 < $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