mirror of
https://github.com/systemd/systemd.git
synced 2025-02-25 21:57:32 +03:00
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
This commit is contained in:
parent
761787fc88
commit
cb456374e0
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -32,3 +32,4 @@
|
||||
#DNSStubListenerExtra=
|
||||
#ReadEtcHosts=yes
|
||||
#ResolveUnicastSingleLabel=no
|
||||
#Monitor=no
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user