mirror of
https://github.com/morgan9e/systemd
synced 2026-04-14 00:14:32 +09:00
resolved: add hook api
This introduces /run/systemd/resolve.hook/ as a new directory that local (privileged) programs can bind a Varlink socket into. If they do they'll get a method call for each attempted resolved lookup, which they can then either process themselves (and generate new records for, or return errors to block stuff) or let pass so that the regular resolution is done. Usecase for this is primarily two things: 1. in machined we can add local resolution of machine names to their IP addresses, similar in fashion to nss-mymachines, but working also if the non-NSS interfaces to name resolution are used, i.e. the local DNS responder. In fact, I think we should eventually remove nss-mymachines from our tree, as soon as this code in resolved is setlled. 2. in networkd we can add local resolution of names specified in DHCP leases we hand out. But beyond that there should be many other uses, for example people could write "dns firewalls" with this if they like where they dynamically block certain names from resolution. Fixes: #8518
This commit is contained in:
@@ -497,6 +497,7 @@ node /org/freedesktop/resolve1 {
|
||||
#define SD_RESOLVED_FROM_ZONE (UINT64_C(1) << 21)
|
||||
#define SD_RESOLVED_FROM_TRUST_ANCHOR (UINT64_C(1) << 22)
|
||||
#define SD_RESOLVED_FROM_NETWORK (UINT64_C(1) << 23)
|
||||
#define SD_RESOLVED_FROM_HOOK (UINT64_C(1) << 27)
|
||||
</programlisting>
|
||||
|
||||
<para>On input, the first five flags control the protocols to use for the look-up. They refer to
|
||||
|
||||
@@ -28,6 +28,7 @@ systemd_resolved_extract_sources = files(
|
||||
'resolved-dnssd-bus.c',
|
||||
'resolved-dnssd.c',
|
||||
'resolved-etc-hosts.c',
|
||||
'resolved-hook.c',
|
||||
'resolved-link-bus.c',
|
||||
'resolved-link.c',
|
||||
'resolved-llmnr.c',
|
||||
|
||||
@@ -253,13 +253,14 @@ static void print_source(uint64_t flags, usec_t rtt) {
|
||||
ansi_normal());
|
||||
|
||||
if ((flags & (SD_RESOLVED_FROM_MASK|SD_RESOLVED_SYNTHETIC)) != 0)
|
||||
printf("%s-- Data from:%s%s%s%s%s%s\n",
|
||||
printf("%s-- Data from:%s%s%s%s%s%s%s\n",
|
||||
ansi_grey(),
|
||||
FLAGS_SET(flags, SD_RESOLVED_SYNTHETIC) ? " synthetic" : "",
|
||||
FLAGS_SET(flags, SD_RESOLVED_FROM_CACHE) ? " cache" : "",
|
||||
FLAGS_SET(flags, SD_RESOLVED_FROM_ZONE) ? " zone" : "",
|
||||
FLAGS_SET(flags, SD_RESOLVED_FROM_TRUST_ANCHOR) ? " trust-anchor" : "",
|
||||
FLAGS_SET(flags, SD_RESOLVED_FROM_NETWORK) ? " network" : "",
|
||||
FLAGS_SET(flags, SD_RESOLVED_FROM_HOOK) ? " hook" : "",
|
||||
ansi_normal());
|
||||
}
|
||||
|
||||
|
||||
@@ -1017,7 +1017,7 @@ static void resolve_service_all_complete(DnsQuery *query) {
|
||||
|
||||
assert(q);
|
||||
|
||||
if (q->block_all_complete > 0) {
|
||||
if (q->hook_query || q->block_all_complete > 0) {
|
||||
TAKE_PTR(q);
|
||||
return;
|
||||
}
|
||||
@@ -1028,6 +1028,12 @@ static void resolve_service_all_complete(DnsQuery *query) {
|
||||
|
||||
LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) {
|
||||
|
||||
if (aux->hook_query) {
|
||||
/* If an auxiliary query's hook is still pending, let's wait */
|
||||
TAKE_PTR(q);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (aux->state) {
|
||||
|
||||
case DNS_TRANSACTION_PENDING:
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "resolved-dns-synthesize.h"
|
||||
#include "resolved-dns-transaction.h"
|
||||
#include "resolved-etc-hosts.h"
|
||||
#include "resolved-hook.h"
|
||||
#include "resolved-manager.h"
|
||||
#include "resolved-timeouts.h"
|
||||
#include "set.h"
|
||||
@@ -524,6 +525,8 @@ DnsQuery *dns_query_free(DnsQuery *q) {
|
||||
|
||||
dns_service_browser_unref(q->service_browser_request);
|
||||
|
||||
hook_query_free(q->hook_query);
|
||||
|
||||
if (q->manager) {
|
||||
LIST_REMOVE(queries, q->manager->dns_queries, q);
|
||||
q->manager->n_dns_queries--;
|
||||
@@ -907,24 +910,17 @@ static int dns_query_try_etc_hosts(DnsQuery *q) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dns_query_go(DnsQuery *q) {
|
||||
DnsScopeMatch found = DNS_SCOPE_NO;
|
||||
DnsScope *first = NULL;
|
||||
static int dns_query_go_scopes(DnsQuery *q) {
|
||||
int r;
|
||||
|
||||
assert(q);
|
||||
assert(!q->hook_query);
|
||||
assert(q->state == DNS_TRANSACTION_NULL);
|
||||
|
||||
if (q->state != DNS_TRANSACTION_NULL)
|
||||
return 0;
|
||||
|
||||
r = dns_query_try_etc_hosts(q);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) {
|
||||
dns_query_complete(q, DNS_TRANSACTION_SUCCESS);
|
||||
return 1;
|
||||
}
|
||||
/* Start the lookup via the scopes */
|
||||
|
||||
DnsScopeMatch found = DNS_SCOPE_NO;
|
||||
DnsScope *first = NULL;
|
||||
LIST_FOREACH(scopes, s, q->manager->dns_scopes) {
|
||||
DnsScopeMatch match;
|
||||
|
||||
@@ -999,6 +995,72 @@ fail:
|
||||
return r;
|
||||
}
|
||||
|
||||
static void on_hook_complete(HookQuery *hq, int rcode, DnsAnswer *answer, void *userdata) {
|
||||
DnsQuery *q = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
assert(hq);
|
||||
assert(q->hook_query == hq);
|
||||
assert(q->state == DNS_TRANSACTION_NULL);
|
||||
|
||||
q->hook_query = hook_query_free(q->hook_query);
|
||||
TAKE_PTR(hq);
|
||||
|
||||
if (rcode < 0) {
|
||||
log_debug("Hook yielded no results, proceeding.");
|
||||
r = dns_query_go_scopes(q);
|
||||
if (r < 0) {
|
||||
dns_query_reset_answer(q);
|
||||
q->answer_errno = r;
|
||||
dns_query_complete(q, DNS_TRANSACTION_ERRNO);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dns_query_reset_answer(q);
|
||||
|
||||
q->answer = dns_answer_ref(answer);
|
||||
q->answer_rcode = rcode;
|
||||
q->answer_protocol = dns_synthesize_protocol(q->flags);
|
||||
q->answer_family = dns_synthesize_family(q->flags);
|
||||
q->answer_query_flags = SD_RESOLVED_FROM_HOOK;
|
||||
dns_query_complete(q, rcode == DNS_RCODE_SUCCESS ? DNS_TRANSACTION_SUCCESS : DNS_TRANSACTION_RCODE_FAILURE);
|
||||
}
|
||||
|
||||
int dns_query_go(DnsQuery *q) {
|
||||
int r;
|
||||
|
||||
assert(q);
|
||||
|
||||
/* Already ongoing? Then suppress */
|
||||
if (q->hook_query ||
|
||||
q->state != DNS_TRANSACTION_NULL)
|
||||
return 0;
|
||||
|
||||
r = dns_query_try_etc_hosts(q);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) {
|
||||
dns_query_complete(q, DNS_TRANSACTION_SUCCESS);
|
||||
return 1;
|
||||
}
|
||||
|
||||
r = manager_hook_query(
|
||||
q->manager,
|
||||
q->question_bypass ? q->question_bypass->question : q->question_idna,
|
||||
q->question_bypass ? q->question_bypass->question : q->question_utf8,
|
||||
on_hook_complete,
|
||||
q,
|
||||
&q->hook_query);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r > 0) /* hook calls are pending */
|
||||
return 0;
|
||||
|
||||
return dns_query_go_scopes(q);
|
||||
}
|
||||
|
||||
static void dns_query_accept(DnsQuery *q, DnsQueryCandidate *c) {
|
||||
DnsTransactionState state = DNS_TRANSACTION_NO_SERVERS;
|
||||
bool has_authenticated = false, has_non_authenticated = false, has_confidential = false, has_non_confidential = false;
|
||||
@@ -1137,6 +1199,9 @@ void dns_query_ready(DnsQuery *q) {
|
||||
* after calling this function, unless the block_ready
|
||||
* counter was explicitly bumped before doing so. */
|
||||
|
||||
if (q->hook_query)
|
||||
return;
|
||||
|
||||
if (q->block_ready > 0)
|
||||
return;
|
||||
|
||||
|
||||
@@ -110,6 +110,9 @@ typedef struct DnsQuery {
|
||||
DnssdDiscoveredService *dnsservice_request;
|
||||
DnsServiceBrowser *service_browser_request;
|
||||
|
||||
/* Pending query to any installed hooks */
|
||||
HookQuery *hook_query;
|
||||
|
||||
/* Completion callback */
|
||||
void (*complete)(DnsQuery* q);
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ typedef struct DnsSvcParam DnsSvcParam;
|
||||
typedef struct DnsTransaction DnsTransaction;
|
||||
typedef struct DnsTxtItem DnsTxtItem;
|
||||
typedef struct DnsZoneItem DnsZoneItem;
|
||||
typedef struct HookQuery HookQuery;
|
||||
typedef struct Link Link;
|
||||
typedef struct LinkAddress LinkAddress;
|
||||
typedef struct Manager Manager;
|
||||
|
||||
881
src/resolve/resolved-hook.c
Normal file
881
src/resolve/resolved-hook.c
Normal file
@@ -0,0 +1,881 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "sd-event.h"
|
||||
#include "sd-varlink.h"
|
||||
|
||||
#include "dirent-util.h"
|
||||
#include "dns-domain.h"
|
||||
#include "env-util.h"
|
||||
#include "errno-util.h"
|
||||
#include "fd-util.h"
|
||||
#include "hash-funcs.h"
|
||||
#include "iovec-util.h"
|
||||
#include "json-util.h"
|
||||
#include "ratelimit.h"
|
||||
#include "resolved-hook.h"
|
||||
#include "resolved-manager.h"
|
||||
#include "set.h"
|
||||
#include "stat-util.h"
|
||||
#include "varlink-util.h"
|
||||
|
||||
/* Controls how many idle connections to keep around at max. This is purely an optimization: an established
|
||||
* socket that has gone through connect()/accept() already is just quicker to use. Since we might get a flood
|
||||
* of resolution requests we keep multiple connections open thus, but not too many. */
|
||||
#define HOOK_IDLE_CONNECTIONS_MAX 4U
|
||||
|
||||
/* Encapsulates a specific hook, i.e. bound socket in in the /run/systemd/resolve.hook/ directory */
|
||||
typedef struct Hook {
|
||||
unsigned n_ref;
|
||||
|
||||
Manager *manager;
|
||||
char *socket_path;
|
||||
|
||||
sd_varlink *filter_link;
|
||||
Set *idle_links; /* we retry to recycle varlink connections */
|
||||
|
||||
/* This hook only shall be applied to names matching the following filter parameters */
|
||||
Set *filter_domains; /* if NULL → no filtering; if empty → do not accept anything */
|
||||
unsigned filter_labels_min; /* minimum number of labels */
|
||||
unsigned filter_labels_max; /* maximum number of labels (this is useful to hook only into single-label lookups á la LLMNR) */
|
||||
|
||||
/* timestamp we last saw this in CLOCK_MONOTONIC, for GC handling */
|
||||
uint64_t seen_usec;
|
||||
|
||||
/* When a hook never responds correctly, we'll eventually give up trying */
|
||||
RateLimit reconnect_ratelimit;
|
||||
} Hook;
|
||||
|
||||
static Hook* hook_free(Hook *h) {
|
||||
if (!h)
|
||||
return NULL;
|
||||
|
||||
mfree(h->socket_path);
|
||||
sd_varlink_unref(h->filter_link);
|
||||
set_free(h->idle_links);
|
||||
|
||||
set_free(h->filter_domains);
|
||||
|
||||
return mfree(h);
|
||||
}
|
||||
|
||||
DEFINE_PRIVATE_TRIVIAL_REF_UNREF_FUNC(Hook, hook, hook_free);
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(Hook*, hook_unref);
|
||||
|
||||
static Hook *hook_unlink(Hook *h) {
|
||||
if (!h)
|
||||
return NULL;
|
||||
|
||||
if (!h->manager)
|
||||
return NULL;
|
||||
|
||||
if (h->socket_path)
|
||||
hashmap_remove(h->manager->hooks, h->socket_path);
|
||||
h->manager = NULL;
|
||||
|
||||
return hook_unref(h);
|
||||
}
|
||||
|
||||
static int dispatch_filter_domains(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
Hook *h = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
if (!sd_json_variant_is_array(variant))
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
|
||||
|
||||
/* Let's explicitly allocate the set here, since we want that a NULL set means: let everything
|
||||
* through; but an empty set shall mean: let nothing through */
|
||||
r = set_ensure_allocated(&h->filter_domains, &dns_name_hash_ops_free);
|
||||
if (r < 0)
|
||||
return json_log_oom(variant, flags);
|
||||
|
||||
sd_json_variant *i;
|
||||
JSON_VARIANT_ARRAY_FOREACH(i, variant) {
|
||||
if (!sd_json_variant_is_string(i))
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL),
|
||||
"Element of JSON field '%s' is not a string.", strna(name));
|
||||
|
||||
r = set_put_strdup_full(&h->filter_domains, &dns_name_hash_ops_free, sd_json_variant_string(i));
|
||||
if (r < 0 && r != -EEXIST)
|
||||
return json_log_oom(variant, flags);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void hook_reset_filter(Hook *h) {
|
||||
assert(h);
|
||||
|
||||
h->filter_domains = set_free(h->filter_domains);
|
||||
h->filter_labels_min = UINT_MAX;
|
||||
h->filter_labels_max = UINT_MAX;
|
||||
}
|
||||
|
||||
static int hook_acquire_filter(Hook *h);
|
||||
|
||||
static int on_filter_reply(
|
||||
sd_varlink *link,
|
||||
sd_json_variant *parameters,
|
||||
const char *error_id,
|
||||
sd_varlink_reply_flags_t flags,
|
||||
void *userdata) {
|
||||
|
||||
Hook *h = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
if (error_id) {
|
||||
if (streq(error_id, SD_VARLINK_ERROR_DISCONNECTED)) {
|
||||
/* When we are are disconnected, that's fine, maybe the other side wants to clean up
|
||||
* open connections every now and then, or is being restarted and thus a moment
|
||||
* offline. Try to reconnect immediately to recover. However, a service that
|
||||
* continously fails should not be able to get us into a busy loop, hence we apply a
|
||||
* ratelimit, and when it is hit we stop reconnecting. */
|
||||
if (ratelimit_below(&h->reconnect_ratelimit)) {
|
||||
log_debug("Connection terminated while querying filter of hook '%s', trying to reconnect.", h->socket_path);
|
||||
|
||||
h->filter_link = sd_varlink_unref(h->filter_link);
|
||||
|
||||
r = hook_acquire_filter(h);
|
||||
if (r < 0)
|
||||
goto terminate;
|
||||
} else
|
||||
log_warning("Connection terminated while querying filter of hook '%s', and reconnection attempts failed too quickly, giving up.", h->socket_path);
|
||||
|
||||
goto terminate;
|
||||
}
|
||||
|
||||
if (streq(error_id, SD_VARLINK_ERROR_METHOD_NOT_FOUND)) {
|
||||
log_debug("Hook '%s' does not implement querying filter.", h->socket_path);
|
||||
goto terminate;
|
||||
}
|
||||
|
||||
log_warning("Received error while requesting query filter: %s", error_id);
|
||||
goto terminate;
|
||||
}
|
||||
|
||||
if (!FLAGS_SET(flags, SD_VARLINK_REPLY_CONTINUES)) {
|
||||
log_debug("Final message received while querying filter, terminating connection.");
|
||||
goto terminate;
|
||||
}
|
||||
|
||||
hook_reset_filter(h);
|
||||
|
||||
static const struct sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "filterDomains", SD_JSON_VARIANT_ARRAY, dispatch_filter_domains, 0, SD_JSON_NULLABLE },
|
||||
{ "filterLabelsMin", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, offsetof(Hook, filter_labels_min), 0 },
|
||||
{ "filterLabelsMax", SD_JSON_VARIANT_INTEGER, sd_json_dispatch_uint, offsetof(Hook, filter_labels_max), 0 },
|
||||
{},
|
||||
};
|
||||
|
||||
r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, h);
|
||||
if (r < 0)
|
||||
goto terminate;
|
||||
|
||||
return 1;
|
||||
|
||||
terminate:
|
||||
h->filter_link = sd_varlink_unref(h->filter_link);
|
||||
hook_reset_filter(h);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int hook_varlink_connect(Hook *h, int64_t priority, sd_varlink **ret) {
|
||||
int r;
|
||||
|
||||
assert(h);
|
||||
assert(ret);
|
||||
|
||||
_cleanup_(sd_varlink_unrefp) sd_varlink *v = NULL;
|
||||
r = sd_varlink_connect_address(&v, h->socket_path);
|
||||
if (ERRNO_IS_NEG_DISCONNECT(r) || r == -ENOENT) {
|
||||
log_debug_errno(r, "Socket '%s' is not connectible, probably stale, ignoring: %m", h->socket_path);
|
||||
*ret = NULL;
|
||||
return 0; /* dead socket */
|
||||
}
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to connect to '%s': %m", h->socket_path);
|
||||
|
||||
_cleanup_free_ char *bn = NULL;
|
||||
r = path_extract_filename(h->socket_path, &bn);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to extract filename from path '%s': %m", h->socket_path);
|
||||
|
||||
_cleanup_free_ char *j = strjoin("hook-", bn);
|
||||
if (!j)
|
||||
return log_oom();
|
||||
|
||||
(void) sd_varlink_set_description(v, j);
|
||||
|
||||
r = sd_varlink_attach_event(v, h->manager->event, priority);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to attach Varlink connection to event loop: %m");
|
||||
|
||||
*ret = TAKE_PTR(v);
|
||||
return 1; /* worked */
|
||||
}
|
||||
|
||||
static int hook_acquire_filter(Hook *h) {
|
||||
int r;
|
||||
|
||||
assert(h);
|
||||
assert(h->manager);
|
||||
|
||||
if (h->filter_link)
|
||||
return 0;
|
||||
|
||||
_cleanup_(sd_varlink_unrefp) sd_varlink *v = NULL;
|
||||
r = hook_varlink_connect(h, SD_EVENT_PRIORITY_NORMAL-10, &v); /* Give the querying of the filter a bit of priority */
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
/* Turn off timeout, after all we want to continously monitor filter changes */
|
||||
r = sd_varlink_set_relative_timeout(v, UINT64_MAX);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to disable timeout on Varlink connection %m");
|
||||
|
||||
sd_varlink_set_userdata(v, h);
|
||||
r = sd_varlink_bind_reply(v, on_filter_reply);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to set filter reply callback on Varlink connection: %m");
|
||||
|
||||
r = sd_varlink_observe(
|
||||
v,
|
||||
"io.systemd.Resolve.Hook.QueryFilter",
|
||||
/* parameters= */ NULL);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to issue QueryFilter() varlink call: %m");
|
||||
|
||||
h->filter_link = TAKE_PTR(v);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int hook_test_filter(Hook *h, DnsQuestion *question) {
|
||||
int r;
|
||||
|
||||
assert(h);
|
||||
assert(question);
|
||||
|
||||
const char *name = dns_question_first_name(question);
|
||||
if (!name)
|
||||
return -EINVAL;
|
||||
|
||||
if (h->filter_labels_max != UINT_MAX || h->filter_labels_min != UINT_MAX) {
|
||||
int n = dns_name_count_labels(name);
|
||||
if (n < 0)
|
||||
return n;
|
||||
|
||||
if (h->filter_labels_max != UINT_MAX && (unsigned) n > h->filter_labels_max)
|
||||
return false;
|
||||
if (h->filter_labels_min != UINT_MAX && (unsigned) n < h->filter_labels_min)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (h->filter_domains)
|
||||
for (const char *p = name;;) {
|
||||
if (set_contains(h->filter_domains, p))
|
||||
break;
|
||||
|
||||
r = dns_name_parent(&p);
|
||||
if (r < 0)
|
||||
return r;
|
||||
if (r == 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int hook_compare(const Hook *a, const Hook *b) {
|
||||
assert(a);
|
||||
|
||||
/* Hooks take preference based on the name of their socket */
|
||||
return path_compare(a->socket_path, b->socket_path);
|
||||
}
|
||||
|
||||
static void hook_recycle_varlink(Hook *h, sd_varlink *vl) {
|
||||
int r;
|
||||
|
||||
assert(h);
|
||||
assert(vl);
|
||||
|
||||
/* Disable any potential callbacks while we are recycling the thing */
|
||||
sd_varlink_set_userdata(vl, NULL);
|
||||
sd_varlink_bind_reply(vl, NULL);
|
||||
|
||||
if (set_size(h->idle_links) > HOOK_IDLE_CONNECTIONS_MAX)
|
||||
return;
|
||||
|
||||
/* If we are done with a lookup don't close the connection right-away, but keep it open so that we
|
||||
* can possibly reuse it later, and can save a bit of time on future lookups. We only keep a few
|
||||
* around however. */
|
||||
|
||||
r = set_ensure_put(&h->idle_links, &varlink_hash_ops, vl);
|
||||
if (r < 0)
|
||||
log_debug_errno(r, "Failed to add varlink connection to idle set, ignoring: %m");
|
||||
else
|
||||
sd_varlink_ref(vl);
|
||||
}
|
||||
|
||||
static void manager_gc_hooks(Manager *m, usec_t seen_usec) {
|
||||
assert(m);
|
||||
|
||||
Hook *h;
|
||||
HASHMAP_FOREACH(h, m->hooks) {
|
||||
/* Keep hooks around that have been seen in this iteration */
|
||||
if (h->seen_usec == seen_usec)
|
||||
continue;
|
||||
|
||||
hook_unlink(h);
|
||||
}
|
||||
}
|
||||
|
||||
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
|
||||
hook_hash_ops,
|
||||
char, string_hash_func, string_compare_func,
|
||||
Hook, hook_unlink);
|
||||
|
||||
static int manager_hook_add(Manager *m, const char *p, usec_t seen_usec) {
|
||||
int r;
|
||||
|
||||
assert(m);
|
||||
assert(p);
|
||||
|
||||
Hook *found = hashmap_get(m->hooks, p);
|
||||
if (found) {
|
||||
found->seen_usec = seen_usec;
|
||||
return 0;
|
||||
}
|
||||
|
||||
_cleanup_free_ char *s = strdup(p);
|
||||
if (!s)
|
||||
return log_oom();
|
||||
|
||||
_cleanup_(hook_unrefp) Hook *h = new(Hook, 1);
|
||||
if (!h)
|
||||
return log_oom();
|
||||
|
||||
*h = (Hook) {
|
||||
.n_ref = 1,
|
||||
.socket_path = TAKE_PTR(s),
|
||||
.filter_labels_min = UINT_MAX,
|
||||
.filter_labels_max = UINT_MAX,
|
||||
.reconnect_ratelimit = { 1 * USEC_PER_SEC, 5 },
|
||||
.seen_usec = seen_usec,
|
||||
};
|
||||
|
||||
if (hashmap_ensure_put(&m->hooks, &hook_hash_ops, h->socket_path, h) < 0)
|
||||
return log_oom();
|
||||
|
||||
hook_ref(h);
|
||||
h->manager = m;
|
||||
|
||||
r = hook_acquire_filter(h);
|
||||
if (r < 0) {
|
||||
hook_unlink(h);
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int manager_hook_discover(Manager *m) {
|
||||
/* You might wonder, why is this /run/systemd/resolve.hook/ and not /run/systemd/resolve/hook/?
|
||||
* That's because of permissions: resolved runs as "systemd-resolve" user and owns
|
||||
* /run/systemd/resolve/, but the hook directory is where other privileged code shall bind a socket
|
||||
* in (and where root ownership hence makes sense). Hence we do not nest the directories, but put
|
||||
* them side by side, so that they can have different ownership. */
|
||||
static const char dp[] = "/run/systemd/resolve.hook";
|
||||
_cleanup_closedir_ DIR *d = NULL;
|
||||
int r;
|
||||
|
||||
assert(m);
|
||||
|
||||
usec_t seen_usec = now(CLOCK_MONOTONIC);
|
||||
|
||||
struct stat st;
|
||||
if (stat(dp, &st) < 0) {
|
||||
if (errno == ENOENT)
|
||||
r = 0;
|
||||
else
|
||||
r = log_warning_errno(errno, "Failed to stat %s/: %m", dp);
|
||||
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (stat_inode_unmodified(&st, &m->hook_stat))
|
||||
return 0;
|
||||
|
||||
d = opendir(dp);
|
||||
if (!d) {
|
||||
if (errno == ENOENT)
|
||||
r = 0;
|
||||
else
|
||||
r = log_warning_errno(errno, "Failed to enumerate %s/ contents: %m", dp);
|
||||
|
||||
goto finish;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
errno = 0;
|
||||
struct dirent *de = readdir_no_dot(d);
|
||||
if (!de) {
|
||||
if (errno == 0) /* EOD */
|
||||
break;
|
||||
|
||||
r = log_error_errno(errno, "Failed to enumerate %s/: %m", dp);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if (!IN_SET(de->d_type, DT_SOCK, DT_UNKNOWN))
|
||||
continue;
|
||||
|
||||
_cleanup_free_ char *p = path_join(dp, de->d_name);
|
||||
if (!p) {
|
||||
r = log_oom();
|
||||
goto finish;
|
||||
}
|
||||
|
||||
(void) manager_hook_add(m, p, seen_usec);
|
||||
}
|
||||
|
||||
m->hook_stat = st;
|
||||
r = 0;
|
||||
|
||||
finish:
|
||||
manager_gc_hooks(m, seen_usec);
|
||||
return r;
|
||||
}
|
||||
|
||||
typedef struct HookQuery HookQuery;
|
||||
typedef struct HookQueryCandidate HookQueryCandidate;
|
||||
|
||||
/* Encapsulates a query currently being processed by various hooks */
|
||||
struct HookQuery {
|
||||
/* Question */
|
||||
DnsQuestion *question_idna;
|
||||
DnsQuestion *question_utf8;
|
||||
|
||||
/* Selected answer */
|
||||
DnsAnswer *answer;
|
||||
int answer_rcode;
|
||||
Hook *answer_hook;
|
||||
|
||||
/* Candidates for a reply, i.e, one entry for each hook */
|
||||
LIST_HEAD(HookQueryCandidate, candidates);
|
||||
|
||||
/* Completion callback to invoke */
|
||||
void (*complete)(HookQuery *q, int answer_rcode, DnsAnswer *answer, void *userdata);
|
||||
void *userdata;
|
||||
};
|
||||
|
||||
/* Encapsulates the state of a hook query to one specific hook */
|
||||
struct HookQueryCandidate {
|
||||
HookQuery *query;
|
||||
Hook *hook;
|
||||
sd_varlink *link;
|
||||
LIST_FIELDS(HookQueryCandidate, candidates);
|
||||
};
|
||||
|
||||
static HookQueryCandidate* hook_query_candidate_free(HookQueryCandidate *c) {
|
||||
if (!c)
|
||||
return NULL;
|
||||
|
||||
c->link = sd_varlink_unref(c->link);
|
||||
|
||||
if (c->query)
|
||||
LIST_REMOVE(candidates, c->query->candidates, c);
|
||||
|
||||
hook_unref(c->hook);
|
||||
return mfree(c);
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(HookQueryCandidate*, hook_query_candidate_free);
|
||||
|
||||
HookQuery* hook_query_free(HookQuery *hq) {
|
||||
if (!hq)
|
||||
return NULL;
|
||||
|
||||
/* Free candidates as long as there are candidates */
|
||||
while (hq->candidates)
|
||||
hook_query_candidate_free(hq->candidates);
|
||||
|
||||
dns_question_unref(hq->question_utf8);
|
||||
dns_question_unref(hq->question_idna);
|
||||
dns_answer_unref(hq->answer);
|
||||
hook_unref(hq->answer_hook);
|
||||
|
||||
return mfree(hq);
|
||||
}
|
||||
|
||||
DEFINE_TRIVIAL_CLEANUP_FUNC(HookQuery*, hook_query_free);
|
||||
|
||||
static void hook_query_ready(HookQuery *hq) {
|
||||
assert(hq);
|
||||
|
||||
bool done = true;
|
||||
LIST_FOREACH(candidates, c, hq->candidates)
|
||||
if (c->link) { /* ongoing connection? */
|
||||
done = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!done)
|
||||
return;
|
||||
|
||||
/* The complete() callback quite likely will destroy 'hq', which might be what keeps the answer
|
||||
* object alive. Let's take an explicit ref here hence, so that it definitely remains alive for the
|
||||
* whole callback lifetime */
|
||||
_cleanup_(dns_answer_unrefp) DnsAnswer *answer = dns_answer_ref(hq->answer);
|
||||
hq->complete(hq, hq->answer_rcode, answer, hq->userdata);
|
||||
}
|
||||
|
||||
static int dispatch_rcode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
int *u = ASSERT_PTR(userdata), r;
|
||||
|
||||
assert(variant);
|
||||
|
||||
int rcode;
|
||||
r = sd_json_dispatch_int(name, variant, flags, &rcode);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (rcode < 0 || rcode >= _DNS_RCODE_MAX)
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(ERANGE), "JSON field '%s' contains an invalid DNS rcode.", strna(name));
|
||||
|
||||
*u = rcode;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dispatch_answer(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
DnsAnswer **a = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
assert(variant);
|
||||
|
||||
if (sd_json_variant_is_null(variant)) {
|
||||
*a = dns_answer_unref(*a);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!sd_json_variant_is_array(variant))
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not an array.", strna(name));
|
||||
|
||||
_cleanup_(dns_answer_unrefp) DnsAnswer *l = NULL;
|
||||
sd_json_variant *e;
|
||||
JSON_VARIANT_ARRAY_FOREACH(e, variant) {
|
||||
if (!sd_json_variant_is_object(e))
|
||||
return json_log(e, flags, SYNTHETIC_ERRNO(EINVAL), "JSON array element is not an object");
|
||||
|
||||
_cleanup_(iovec_done) struct iovec iovec = {};
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "raw", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, 0, SD_JSON_MANDATORY },
|
||||
{ "rr", SD_JSON_VARIANT_OBJECT, NULL, 0, 0 },
|
||||
{}
|
||||
};
|
||||
|
||||
r = sd_json_dispatch(e, dispatch_table, flags|SD_JSON_ALLOW_EXTENSIONS, &iovec);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
|
||||
r = dns_resource_record_new_from_raw(&rr, iovec.iov_base, iovec.iov_len);
|
||||
if (r < 0)
|
||||
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL),
|
||||
"JSON field '%s' contains an invalid resource record.", strna(name));
|
||||
|
||||
if (dns_answer_add_extend(&l, rr, /* ifindex= */ 0, /* flags= */ 0, /* rrsig= */ NULL) < 0)
|
||||
return json_log_oom(e, flags);
|
||||
}
|
||||
|
||||
dns_answer_unref(*a);
|
||||
*a = TAKE_PTR(l);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef struct QueryReplyParameters {
|
||||
int rcode;
|
||||
DnsAnswer *answer;
|
||||
} QueryReplyParameters;
|
||||
|
||||
static void query_reply_parameters_done(QueryReplyParameters *p) {
|
||||
assert(p);
|
||||
|
||||
p->answer = dns_answer_unref(p->answer);
|
||||
}
|
||||
|
||||
static int on_query_reply(
|
||||
sd_varlink *link,
|
||||
sd_json_variant *parameters,
|
||||
const char *error_id,
|
||||
sd_varlink_reply_flags_t flags,
|
||||
void *userdata) {
|
||||
|
||||
HookQueryCandidate *qc = ASSERT_PTR(userdata);
|
||||
HookQuery *q = ASSERT_PTR(qc->query); /* save early in case we destroy 'qc' half-way through this function */
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
|
||||
_cleanup_(query_reply_parameters_done) QueryReplyParameters p = {
|
||||
.rcode = -1,
|
||||
};
|
||||
|
||||
if (error_id) {
|
||||
log_notice("Query on hook '%s' failed with error '%s', ignoring.", qc->hook->socket_path, error_id);
|
||||
r = -EBADR;
|
||||
goto destroy;
|
||||
}
|
||||
|
||||
static const sd_json_dispatch_field dispatch_table[] = {
|
||||
{ "rcode", _SD_JSON_VARIANT_TYPE_INVALID, dispatch_rcode, offsetof(QueryReplyParameters, rcode), 0 },
|
||||
{ "answer", SD_JSON_VARIANT_ARRAY, dispatch_answer, offsetof(QueryReplyParameters, answer), 0 },
|
||||
{},
|
||||
};
|
||||
|
||||
r = sd_json_dispatch(parameters, dispatch_table, SD_JSON_LOG|SD_JSON_ALLOW_EXTENSIONS, &p);
|
||||
if (r < 0)
|
||||
goto destroy;
|
||||
|
||||
if (p.rcode < 0) {
|
||||
/* If no rcode is specified, then this means "continue with regular DNS based resolving" to us */
|
||||
log_debug("Query on hook '%s' returned empty reply, skipping.", qc->hook->socket_path);
|
||||
r = 0;
|
||||
goto destroy;
|
||||
}
|
||||
|
||||
bool win = false;
|
||||
if (p.rcode == DNS_RCODE_SUCCESS)
|
||||
/* if this is a successful lookup, let it win if the so far best lookup was a failure or
|
||||
* empty, or ordered later than us */
|
||||
win = q->answer_rcode != DNS_RCODE_SUCCESS ||
|
||||
dns_answer_isempty(q->answer) ||
|
||||
(!dns_answer_isempty(p.answer) &&
|
||||
hook_compare(qc->hook, q->answer_hook) < 0);
|
||||
else
|
||||
/* if this is a failure lookup, let it win if we so far haven't seen any reply at all, or the
|
||||
* winner so far us ordered later than us. */
|
||||
win = q->answer_rcode < 0 ||
|
||||
hook_compare(qc->hook, q->answer_hook) < 0;
|
||||
|
||||
if (win) {
|
||||
/* This reply wins over whatever was stored before. Let's track that */
|
||||
dns_answer_unref(q->answer);
|
||||
q->answer = TAKE_PTR(p.answer);
|
||||
q->answer_rcode = p.rcode;
|
||||
hook_unref(q->answer_hook);
|
||||
q->answer_hook = hook_ref(qc->hook);
|
||||
}
|
||||
|
||||
hook_recycle_varlink(qc->hook, qc->link);
|
||||
qc->link = sd_varlink_unref(qc->link);
|
||||
|
||||
/* Check if we are ready now, and have processed all hooks on this query (this might destroy our
|
||||
* candidate and our hook query!) */
|
||||
hook_query_ready(q);
|
||||
return 0;
|
||||
|
||||
destroy:
|
||||
qc = hook_query_candidate_free(qc);
|
||||
hook_query_ready(q);
|
||||
return r;
|
||||
}
|
||||
|
||||
static int dns_questions_to_json(DnsQuestion *a, DnsQuestion *b, sd_json_variant **ret) {
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
/* Takes both questions and turns them into a JSON array of objects with the key. Note this takes two
|
||||
* questions, one in IDNA and one in UTF-8 encoding, and merges them, removing duplicates. */
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *l = NULL;
|
||||
|
||||
DnsResourceKey *key;
|
||||
DNS_QUESTION_FOREACH(key, a) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
|
||||
r = dns_resource_key_to_json(key, &v);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_json_variant_append_arraybo(&l, SD_JSON_BUILD_PAIR_VARIANT("key", v));
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (a != b) {
|
||||
DNS_QUESTION_FOREACH(key, b) {
|
||||
if (dns_question_contains_key(a, key))
|
||||
continue;
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
|
||||
r = dns_resource_key_to_json(key, &v);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_json_variant_append_arraybo(&l, SD_JSON_BUILD_PAIR_VARIANT("key", v));
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
*ret = TAKE_PTR(l);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int hook_query_add_candidate(HookQuery *hq, Hook *h) {
|
||||
int r;
|
||||
|
||||
assert(hq);
|
||||
assert(h);
|
||||
|
||||
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
|
||||
for (;;) {
|
||||
/* Before we create a new connection, let's see if there are still idle connections we can
|
||||
* use. */
|
||||
vl = set_steal_first(h->idle_links);
|
||||
if (!vl) {
|
||||
/* Nope, there's nothing, let's create a new connection */
|
||||
r = hook_varlink_connect(h, SD_EVENT_PRIORITY_NORMAL, &vl);
|
||||
if (r <= 0)
|
||||
return r;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
r = sd_varlink_is_connected(vl);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to check if varlink connection is connected: %m");
|
||||
if (r > 0)
|
||||
break;
|
||||
|
||||
vl = sd_varlink_unref(vl);
|
||||
}
|
||||
|
||||
/* Set a short timeout for hooks. Hooks should not be able to cause the DNS part of the lookup to fail. */
|
||||
r = sd_varlink_set_relative_timeout(vl, SD_RESOLVED_QUERY_TIMEOUT_USEC/4);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to set Varlink connection timeout: %m");
|
||||
|
||||
r = sd_varlink_bind_reply(vl, on_query_reply);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to bind reply callback to Varlink connection: %m");
|
||||
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *jq = NULL;
|
||||
r = dns_questions_to_json(hq->question_idna, hq->question_utf8, &jq);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to convert question to JSON: %m");
|
||||
|
||||
r = sd_varlink_invokebo(
|
||||
vl,
|
||||
"io.systemd.Resolve.Hook.ResolveRecord",
|
||||
SD_JSON_BUILD_PAIR_VARIANT("question", jq));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to enqueue question onto Varlink connection: %m");
|
||||
|
||||
_cleanup_(hook_query_candidate_freep) HookQueryCandidate *qc = new(HookQueryCandidate, 1);
|
||||
if (!qc)
|
||||
return log_oom();
|
||||
|
||||
qc->query = hq;
|
||||
qc->hook = hook_ref(h);
|
||||
qc->link = TAKE_PTR(vl);
|
||||
LIST_PREPEND(candidates, hq->candidates, qc);
|
||||
|
||||
sd_varlink_set_userdata(qc->link, qc);
|
||||
|
||||
TAKE_PTR(qc);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool use_hooks(void) {
|
||||
static int cache = -1;
|
||||
int r;
|
||||
|
||||
if (cache >= 0)
|
||||
return cache;
|
||||
|
||||
r = secure_getenv_bool("SYSTEMD_RESOLVED_HOOK");
|
||||
if (r < 0) {
|
||||
if (r != -ENXIO)
|
||||
log_debug_errno(r, "Failed to parse $SYSTEMD_RESOLVED_HOOK, ignoring: %m");
|
||||
|
||||
return (cache = true);
|
||||
}
|
||||
|
||||
return (cache = r);
|
||||
}
|
||||
|
||||
int manager_hook_query(
|
||||
Manager *m,
|
||||
DnsQuestion *question_idna,
|
||||
DnsQuestion *question_utf8,
|
||||
HookCompleteCallback complete_cb,
|
||||
void *userdata,
|
||||
HookQuery **ret) {
|
||||
|
||||
int r;
|
||||
|
||||
assert(m);
|
||||
assert(ret);
|
||||
|
||||
if (!use_hooks()) {
|
||||
*ret = NULL;
|
||||
return 0; /* no relevant hooks, continue immediately */
|
||||
}
|
||||
|
||||
/* Let's bring our list of hooks up-to-date */
|
||||
(void) manager_hook_discover(m);
|
||||
|
||||
_cleanup_(hook_query_freep) HookQuery *hq = NULL;
|
||||
|
||||
Hook *h;
|
||||
HASHMAP_FOREACH(h, m->hooks) {
|
||||
r = hook_test_filter(h, question_idna);
|
||||
if (r < 0) {
|
||||
log_warning_errno(
|
||||
r, "Failed to test if hook '%s' matches IDNA question (%s), assuming not.",
|
||||
h->socket_path, dns_question_first_name(question_idna));
|
||||
continue;
|
||||
}
|
||||
if (r == 0) {
|
||||
r = hook_test_filter(h, question_utf8);
|
||||
if (r < 0) {
|
||||
log_warning_errno(
|
||||
r, "Failed to test if hook '%s' matches UTF-8 question (%s), assuming not.",
|
||||
h->socket_path, dns_question_first_name(question_utf8));
|
||||
continue;
|
||||
}
|
||||
if (r == 0) {
|
||||
log_debug("Hook %s does not match question, skipping.", h->socket_path);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hq) {
|
||||
hq = new(HookQuery, 1);
|
||||
if (!hq)
|
||||
return log_oom();
|
||||
|
||||
*hq = (HookQuery) {
|
||||
.question_idna = dns_question_ref(question_idna),
|
||||
.question_utf8 = dns_question_ref(question_utf8),
|
||||
.answer_rcode = -1,
|
||||
.complete = complete_cb,
|
||||
.userdata = userdata,
|
||||
};
|
||||
}
|
||||
|
||||
r = hook_query_add_candidate(hq, h);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (!hq || !hq->candidates) {
|
||||
*ret = NULL;
|
||||
return 0; /* no relevant hooks, continue immediately */
|
||||
}
|
||||
|
||||
*ret = TAKE_PTR(hq);
|
||||
return 1; /* please wait for the hooks to reply */
|
||||
}
|
||||
10
src/resolve/resolved-hook.h
Normal file
10
src/resolve/resolved-hook.h
Normal file
@@ -0,0 +1,10 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "resolved-forward.h"
|
||||
|
||||
typedef void (HookCompleteCallback)(HookQuery *q, int rcode, DnsAnswer *answer, void *userdata);
|
||||
|
||||
int manager_hook_query(Manager *m, DnsQuestion *question_idna, DnsQuestion *question_utf8, HookCompleteCallback complete_cb, void *userdata, HookQuery **ret);
|
||||
|
||||
HookQuery* hook_query_free(HookQuery *hq);
|
||||
@@ -918,6 +918,8 @@ Manager* manager_free(Manager *m) {
|
||||
dns_service_browser_free(sb);
|
||||
hashmap_free(m->dns_service_browsers);
|
||||
|
||||
hashmap_free(m->hooks);
|
||||
|
||||
return mfree(m);
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,9 @@ typedef struct Manager {
|
||||
|
||||
/* Map varlink links to DnsServiceBrowser instances. */
|
||||
Hashmap *dns_service_browsers;
|
||||
|
||||
Hashmap *hooks;
|
||||
struct stat hook_stat;
|
||||
} Manager;
|
||||
|
||||
/* Manager */
|
||||
|
||||
@@ -700,7 +700,7 @@ static void resolve_service_all_complete(DnsQuery *query) {
|
||||
|
||||
assert(q);
|
||||
|
||||
if (q->block_all_complete > 0) {
|
||||
if (q->hook_query || q->block_all_complete > 0) {
|
||||
TAKE_PTR(q);
|
||||
return;
|
||||
}
|
||||
@@ -710,6 +710,13 @@ static void resolve_service_all_complete(DnsQuery *query) {
|
||||
bool have_success = false;
|
||||
|
||||
LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) {
|
||||
|
||||
if (aux->hook_query) {
|
||||
/* If an auxiliary query's hook is still pending, let's wait */
|
||||
TAKE_PTR(q);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (aux->state) {
|
||||
|
||||
case DNS_TRANSACTION_PENDING:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "sd-event.h"
|
||||
|
||||
#include "dns-answer.h"
|
||||
@@ -890,4 +892,10 @@ TEST(dns_query_go) {
|
||||
exercise_dns_query_go(&cfg, NULL);
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN(LOG_DEBUG);
|
||||
static int intro(void) {
|
||||
/* Disable hooks in order to make test cases hermetic */
|
||||
ASSERT_OK_ERRNO(setenv("SYSTEMD_RESOLVED_HOOK", "0", /* overwrite= */ false));
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
DEFINE_TEST_MAIN_WITH_INTRO(LOG_DEBUG, intro);
|
||||
|
||||
@@ -217,6 +217,7 @@ shared_sources = files(
|
||||
'varlink-io.systemd.PCRLock.c',
|
||||
'varlink-io.systemd.Repart.c',
|
||||
'varlink-io.systemd.Resolve.c',
|
||||
'varlink-io.systemd.Resolve.Hook.c',
|
||||
'varlink-io.systemd.Resolve.Monitor.c',
|
||||
'varlink-io.systemd.Udev.c',
|
||||
'varlink-io.systemd.Unit.c',
|
||||
|
||||
@@ -80,10 +80,13 @@
|
||||
#define SD_RESOLVED_QUERY_CONTINUOUS \
|
||||
(UINT64_C(1) << 26)
|
||||
|
||||
/* Output: Result was answered by hook */
|
||||
#define SD_RESOLVED_FROM_HOOK (UINT64_C(1) << 27)
|
||||
|
||||
#define SD_RESOLVED_LLMNR (SD_RESOLVED_LLMNR_IPV4|SD_RESOLVED_LLMNR_IPV6)
|
||||
#define SD_RESOLVED_MDNS (SD_RESOLVED_MDNS_IPV4|SD_RESOLVED_MDNS_IPV6)
|
||||
#define SD_RESOLVED_PROTOCOLS_ALL (SD_RESOLVED_MDNS|SD_RESOLVED_LLMNR|SD_RESOLVED_DNS)
|
||||
|
||||
#define SD_RESOLVED_FROM_MASK (SD_RESOLVED_FROM_CACHE|SD_RESOLVED_FROM_ZONE|SD_RESOLVED_FROM_TRUST_ANCHOR|SD_RESOLVED_FROM_NETWORK)
|
||||
#define SD_RESOLVED_FROM_MASK (SD_RESOLVED_FROM_CACHE|SD_RESOLVED_FROM_ZONE|SD_RESOLVED_FROM_TRUST_ANCHOR|SD_RESOLVED_FROM_NETWORK|SD_RESOLVED_FROM_HOOK)
|
||||
|
||||
#define SD_RESOLVED_QUERY_TIMEOUT_USEC (120 * USEC_PER_SEC)
|
||||
|
||||
81
src/shared/varlink-io.systemd.Resolve.Hook.c
Normal file
81
src/shared/varlink-io.systemd.Resolve.Hook.c
Normal file
@@ -0,0 +1,81 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include "varlink-io.systemd.Resolve.Hook.h"
|
||||
|
||||
/* We want to reuse the ResourceKey structure from the io.systemd.Resolve interface, hence import it here */
|
||||
#include "varlink-io.systemd.Resolve.h"
|
||||
|
||||
static SD_VARLINK_DEFINE_STRUCT_TYPE(
|
||||
Answer,
|
||||
SD_VARLINK_FIELD_COMMENT("A resource record that shall be looked up. Note that this field is (currently) mostly "
|
||||
"decoration, useful for debugging, and may be omitted. The data actually used is encoded in the "
|
||||
"'raw' field."),
|
||||
SD_VARLINK_DEFINE_FIELD_BY_TYPE(rr, ResourceRecord, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("A resource record encoded in DNS wire format, in turn encoded in Base64. This is the actual data "
|
||||
"returned to the application, and should carry the same information as the 'rr' field, just in a "
|
||||
"different encoding."),
|
||||
SD_VARLINK_DEFINE_FIELD(raw, SD_VARLINK_STRING, 0));
|
||||
|
||||
static SD_VARLINK_DEFINE_STRUCT_TYPE(
|
||||
Question,
|
||||
SD_VARLINK_FIELD_COMMENT("A resource record key that shall be looked up."),
|
||||
SD_VARLINK_DEFINE_FIELD_BY_TYPE(key, ResourceKey, 0));
|
||||
|
||||
static SD_VARLINK_DEFINE_METHOD_FULL(
|
||||
QueryFilter,
|
||||
SD_VARLINK_SUPPORTS_MORE,
|
||||
SD_VARLINK_FIELD_COMMENT("A list of domains this hook is interested in. Lookups for domains not listed here will not be "
|
||||
"passed to the Hook via ResolveRecord(). If this field is not set, requests for all domains "
|
||||
"will be passed to the hook. Note that this applies recursively, i.e. a domain of a lookup is "
|
||||
"considered matching the listed domains both if it exactly matches it, and in case only a suffix "
|
||||
"of it matches it. If this is set to an empty array the hook is disabled."),
|
||||
SD_VARLINK_DEFINE_OUTPUT(filterDomains, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
|
||||
SD_VARLINK_FIELD_COMMENT("Require the specified number of labels or more in a domain for the hook to be considered."),
|
||||
SD_VARLINK_DEFINE_OUTPUT(filterLabelsMin, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("Require the specified number of labels or less in a domain for the hook to be considered."),
|
||||
SD_VARLINK_DEFINE_OUTPUT(filterLabelsMax, SD_VARLINK_INT, SD_VARLINK_NULLABLE));
|
||||
|
||||
static SD_VARLINK_DEFINE_METHOD(
|
||||
ResolveRecord,
|
||||
SD_VARLINK_FIELD_COMMENT("The question being looked up, i.e. a combination of resource record keys. Note that unlike DNS "
|
||||
"queries on the wire these lookups can carry multiple key requests, albeit closely related ones. "
|
||||
"Specifically, lookups for A+AAAA for the the same hostname are submitted as one question, as "
|
||||
"are lookups for TXT+SRV when doing DNS-SD resolution. Moreover, when looking up resources with "
|
||||
"non-ASCII characters, they are placed together in a single question, once with labels encoded in "
|
||||
"UTF-8, and once in IDNA. Hook implementations must be able to deal with these and other similar "
|
||||
"combinations of resource key requests, and reply with all matching answers at once, or fail them "
|
||||
"as one. Partial success/failure combinations are not supported."),
|
||||
SD_VARLINK_DEFINE_INPUT_BY_TYPE(question, Question, SD_VARLINK_ARRAY),
|
||||
SD_VARLINK_FIELD_COMMENT("A DNS response code. If a hook sets this return parameter further processing of the lookup via "
|
||||
"regular proocols such as DNS, LLMNR, mDNS is skipped, and the return code returned immediately. "
|
||||
"In other words, if a hook intends to let the request pass to normal resolution, it should not "
|
||||
"set this return parameter."),
|
||||
SD_VARLINK_DEFINE_OUTPUT(rcode, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
|
||||
SD_VARLINK_FIELD_COMMENT("An answer for a lookup, i.e. a combination of resource records, matching the request. This "
|
||||
"should only be set when the 'rcode' parameter is returned as 0 (SUCCESS)."),
|
||||
SD_VARLINK_DEFINE_OUTPUT_BY_TYPE(answer, Answer, SD_VARLINK_ARRAY|SD_VARLINK_NULLABLE));
|
||||
|
||||
SD_VARLINK_DEFINE_INTERFACE(
|
||||
io_systemd_Resolve_Hook,
|
||||
"io.systemd.Resolve.Hook",
|
||||
SD_VARLINK_INTERFACE_COMMENT("Generic interface for implementing a domain name resolution hook."),
|
||||
SD_VARLINK_SYMBOL_COMMENT("Encapsulates a positive lookup answer"),
|
||||
&vl_type_Answer,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Encapsulates a lookup question"),
|
||||
&vl_type_Question,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Encapsulates the class/type/name part of a DNS resource record."),
|
||||
&vl_type_ResourceKey,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Encapsulates a DNS resource record."),
|
||||
&vl_type_ResourceRecord,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Returns filter parameters for this hook. A hook service can implement this to reduce lookup "
|
||||
"requests, by enabling itself only for certain domains or certain numbers of labels in the name. "
|
||||
"It's recommended to implement this to reduce the number of redundant calls to each hook. Note "
|
||||
"that this is advisory only, and implementing services must be able to gracefully handle lookup "
|
||||
"requests that do not match this filter. This call is usually made with the 'more' flag set, in "
|
||||
"which case the connection is left open after the first reply, and the implementing hook "
|
||||
"services can send updates to the filter at any time. Whenever a further reply is sent the "
|
||||
"filter configured therein fully replaces any previously communicated filter."),
|
||||
&vl_method_QueryFilter,
|
||||
SD_VARLINK_SYMBOL_COMMENT("Sent whenever a resolution request is made. This typically takes the filter paramaters returned "
|
||||
"by QueryFilter() into account, but this is not guaranteed."),
|
||||
&vl_method_ResolveRecord);
|
||||
6
src/shared/varlink-io.systemd.Resolve.Hook.h
Normal file
6
src/shared/varlink-io.systemd.Resolve.Hook.h
Normal file
@@ -0,0 +1,6 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include "sd-varlink-idl.h"
|
||||
|
||||
extern const sd_varlink_interface vl_interface_io_systemd_Resolve_Hook;
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "varlink-io.systemd.PCRLock.h"
|
||||
#include "varlink-io.systemd.Repart.h"
|
||||
#include "varlink-io.systemd.Resolve.h"
|
||||
#include "varlink-io.systemd.Resolve.Hook.h"
|
||||
#include "varlink-io.systemd.Resolve.Monitor.h"
|
||||
#include "varlink-io.systemd.Udev.h"
|
||||
#include "varlink-io.systemd.Unit.h"
|
||||
@@ -192,6 +193,7 @@ TEST(parse_format) {
|
||||
&vl_interface_io_systemd_PCRLock,
|
||||
&vl_interface_io_systemd_Repart,
|
||||
&vl_interface_io_systemd_Resolve,
|
||||
&vl_interface_io_systemd_Resolve_Hook,
|
||||
&vl_interface_io_systemd_Resolve_Monitor,
|
||||
&vl_interface_io_systemd_Udev,
|
||||
&vl_interface_io_systemd_Unit,
|
||||
|
||||
Reference in New Issue
Block a user