resolve: provide service resolve over varlink

ported the d-bus implementation of service resolve to varlink
extended TEST-75-RESOLVED to cover this use-case
This commit is contained in:
Vishal Chillara Srinivas 2023-11-15 12:38:23 +05:30 committed by Lennart Poettering
parent 92d1419eb8
commit e1634bb832
4 changed files with 610 additions and 40 deletions

View File

@ -17,6 +17,15 @@ typedef struct LookupParameters {
char *name;
} LookupParameters;
typedef struct LookupParametersResolveService {
const char *name;
const char *type;
const char *domain;
int family;
int ifindex;
uint64_t in_flags;
} LookupParametersResolveService;
static void lookup_parameters_destroy(LookupParameters *p) {
assert(p);
free(p->name);
@ -168,14 +177,62 @@ static bool validate_and_mangle_flags(
return true;
}
static int find_addr_records(
JsonVariant **array,
DnsQuestion *question,
DnsQuery *q,
DnsResourceRecord **canonical,
const char *search_domain) {
DnsResourceRecord *rr;
int ifindex, r;
DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
_cleanup_(json_variant_unrefp) JsonVariant *entry = NULL;
int family;
const void *p;
r = dns_question_matches_rr(question, rr, search_domain);
if (r < 0)
return r;
if (r == 0)
continue;
if (rr->key->type == DNS_TYPE_A) {
family = AF_INET;
p = &rr->a.in_addr;
} else if (rr->key->type == DNS_TYPE_AAAA) {
family = AF_INET6;
p = &rr->aaaa.in6_addr;
} else {
return -EAFNOSUPPORT;
}
r = json_build(&entry,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)),
JSON_BUILD_PAIR("family", JSON_BUILD_INTEGER(family)),
JSON_BUILD_PAIR("address", JSON_BUILD_BYTE_ARRAY(p, FAMILY_ADDRESS_SIZE(family)))));
if (r < 0)
return r;
r = json_variant_append_array(array, entry);
if (r < 0)
return r;
if (canonical && !*canonical)
*canonical = dns_resource_record_ref(rr);
}
return 0;
}
static void vl_method_resolve_hostname_complete(DnsQuery *query) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *array = NULL;
_cleanup_(dns_query_freep) DnsQuery *q = query;
_cleanup_free_ char *normalized = NULL;
DnsResourceRecord *rr;
DnsQuestion *question;
int ifindex, r;
int r;
assert(q);
@ -199,43 +256,9 @@ static void vl_method_resolve_hostname_complete(DnsQuery *query) {
question = dns_query_question_for_protocol(q, q->answer_protocol);
DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
_cleanup_(json_variant_unrefp) JsonVariant *entry = NULL;
int family;
const void *p;
r = dns_question_matches_rr(question, rr, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
if (r < 0)
goto finish;
if (r == 0)
continue;
if (rr->key->type == DNS_TYPE_A) {
family = AF_INET;
p = &rr->a.in_addr;
} else if (rr->key->type == DNS_TYPE_AAAA) {
family = AF_INET6;
p = &rr->aaaa.in6_addr;
} else {
r = -EAFNOSUPPORT;
goto finish;
}
r = json_build(&entry,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR_CONDITION(ifindex > 0, "ifindex", JSON_BUILD_INTEGER(ifindex)),
JSON_BUILD_PAIR("family", JSON_BUILD_INTEGER(family)),
JSON_BUILD_PAIR("address", JSON_BUILD_BYTE_ARRAY(p, FAMILY_ADDRESS_SIZE(family)))));
if (r < 0)
goto finish;
if (!canonical)
canonical = dns_resource_record_ref(rr);
r = json_variant_append_array(&array, entry);
if (r < 0)
goto finish;
}
r = find_addr_records(&array, question, q, &canonical, DNS_SEARCH_DOMAIN_NAME(q->answer_search_domain));
if (r < 0)
goto finish;
if (json_variant_is_blank_object(array)) {
r = varlink_error(q->varlink_request, "io.systemd.Resolve.NoSuchResourceRecord", NULL);
@ -538,6 +561,509 @@ static int vl_method_resolve_address(Varlink *link, JsonVariant *parameters, Var
return 1;
}
static int append_txt(JsonVariant **txt, DnsResourceRecord *rr) {
int r;
assert(txt);
assert(rr);
assert(rr->key);
if (rr->key->type != DNS_TYPE_TXT)
return 0;
LIST_FOREACH(items, i, rr->txt.items) {
_cleanup_(json_variant_unrefp) JsonVariant *entry = NULL;
if (i->length <= 0)
continue;
r = json_variant_new_base64(&entry, i->data, i->length);
if (r < 0)
return r;
r = json_variant_append_array(txt, entry);
if (r < 0)
return r;
}
return 1;
}
static int append_srv(DnsQuery *q,
JsonVariant **ret_srv,
JsonVariant **ret_addr,
char **ret_norm,
DnsResourceRecord *rr) {
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
_cleanup_free_ char *normalized = NULL;
_cleanup_(json_variant_unrefp) JsonVariant *srv = NULL, *addr = NULL;
int r;
assert(q);
assert(rr);
assert(rr->key);
if (rr->key->type != DNS_TYPE_SRV)
return 0;
if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) {
/* First, let's see if we could find an appropriate A or AAAA
* record for the SRV record */
LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) {
DnsResourceRecord *zz;
DnsQuestion *question;
if (aux->state != DNS_TRANSACTION_SUCCESS)
continue;
if (aux->auxiliary_result != 0)
continue;
question = dns_query_question_for_protocol(aux, aux->answer_protocol);
r = dns_name_equal(dns_question_first_name(question), rr->srv.name);
if (r < 0)
return r;
if (r == 0)
continue;
DNS_ANSWER_FOREACH(zz, aux->answer) {
r = dns_question_matches_rr(question, zz, NULL);
if (r < 0)
return r;
if (r == 0)
continue;
canonical = dns_resource_record_ref(zz);
break;
}
if (canonical)
break;
}
/* Is there are successful A/AAAA lookup for this SRV RR? If not, don't add it */
if (!canonical)
return 0;
}
r = dns_name_normalize(rr->srv.name, 0, &normalized);
if (r < 0)
return r;
r = json_build(&srv,
JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("priority", JSON_BUILD_UNSIGNED(rr->srv.priority)),
JSON_BUILD_PAIR("weight", JSON_BUILD_UNSIGNED(rr->srv.weight)),
JSON_BUILD_PAIR("port", JSON_BUILD_UNSIGNED(rr->srv.port)),
JSON_BUILD_PAIR("hostname", JSON_BUILD_STRING(normalized))));
if (r < 0)
return r;
if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) {
LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) {
DnsQuestion *question;
if (aux->state != DNS_TRANSACTION_SUCCESS)
continue;
if (aux->auxiliary_result != 0)
continue;
question = dns_query_question_for_protocol(aux, aux->answer_protocol);
r = dns_name_equal(dns_question_first_name(question), rr->srv.name);
if (r < 0)
return r;
if (r == 0)
continue;
r = find_addr_records(&addr, question, aux, NULL, NULL);
if (r < 0)
return r;
}
}
if (canonical) {
normalized = mfree(normalized);
r = dns_name_normalize(dns_resource_key_name(canonical->key), 0, &normalized);
if (r < 0)
return r;
}
*ret_srv = TAKE_PTR(srv);
*ret_addr = TAKE_PTR(addr);
*ret_norm = TAKE_PTR(normalized);
return 1;
}
static Varlink *get_vl_link_aux_query(DnsQuery *aux) {
assert(aux);
/* Find the main query */
while (aux->auxiliary_for)
aux = aux->auxiliary_for;
return aux->varlink_request;
}
static void resolve_service_all_complete(DnsQuery *query) {
_cleanup_(dns_query_freep) DnsQuery *q = query;
_cleanup_(json_variant_unrefp) JsonVariant *srv = NULL, *addr = NULL, *txt = NULL;
_cleanup_free_ char *name = NULL, *type = NULL, *domain = NULL, *norm = NULL;
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *canonical = NULL;
DnsQuestion *question;
DnsResourceRecord *rr;
unsigned added = 0;
int r;
assert(q);
if (q->block_all_complete > 0) {
TAKE_PTR(q);
return;
}
if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) {
DnsQuery *bad = NULL;
bool have_success = false;
LIST_FOREACH(auxiliary_queries, aux, q->auxiliary_queries) {
switch (aux->state) {
case DNS_TRANSACTION_PENDING:
/* If an auxiliary query is still pending, let's wait */
TAKE_PTR(q);
return;
case DNS_TRANSACTION_SUCCESS:
if (aux->auxiliary_result == 0)
have_success = true;
else
bad = aux;
break;
default:
bad = aux;
break;
}
}
if (!have_success) {
/* We can only return one error, hence pick the last error we encountered */
assert(bad);
if (bad->state == DNS_TRANSACTION_SUCCESS) {
assert(bad->auxiliary_result != 0);
if (bad->auxiliary_result == -ELOOP) {
r = varlink_error(query->varlink_request, "io.systemd.Resolve.CNAMELoop", NULL);
goto finish;
}
assert(bad->auxiliary_result < 0);
r = bad->auxiliary_result;
goto finish;
}
bad->varlink_request = get_vl_link_aux_query(bad);
r = reply_query_state(bad);
bad->varlink_request = NULL;
goto finish;
}
}
question = dns_query_question_for_protocol(q, q->answer_protocol);
DNS_ANSWER_FOREACH(rr, q->answer) {
r = dns_question_matches_rr(question, rr, NULL);
if (r < 0)
goto finish;
if (r == 0)
continue;
r = append_srv(q, &srv, &addr, &norm, rr);
if (r < 0)
goto finish;
if (r == 0) /* not an SRV record */
continue;
if (!canonical)
canonical = dns_resource_record_ref(rr);
added++;
}
if (added <= 0) {
r = varlink_error(query->varlink_request, "io.systemd.Resolve.NoSuchResourceRecord", NULL);
goto finish;
}
DNS_ANSWER_FOREACH(rr, q->answer) {
r = dns_question_matches_rr(question, rr, NULL);
if (r < 0)
goto finish;
if (r == 0)
continue;
if (rr->key->type != DNS_TYPE_TXT)
continue;
r = append_txt(&txt, rr);
if (r < 0)
goto finish;
}
assert(canonical);
r = dns_service_split(dns_resource_key_name(canonical->key), &name, &type, &domain);
if (r < 0)
goto finish;
r = varlink_replyb(query->varlink_request, JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("srv", JSON_BUILD_VARIANT(srv)),
JSON_BUILD_PAIR("addr", JSON_BUILD_VARIANT(addr)),
JSON_BUILD_PAIR("txt", JSON_BUILD_VARIANT(txt)),
JSON_BUILD_PAIR("normalized", JSON_BUILD_STRING(norm)),
JSON_BUILD_PAIR("canonical", JSON_BUILD_OBJECT(
JSON_BUILD_PAIR("name", JSON_BUILD_STRING(name)),
JSON_BUILD_PAIR("type", JSON_BUILD_STRING(type)),
JSON_BUILD_PAIR("domain", JSON_BUILD_STRING(domain))))));
finish:
if (r < 0) {
log_error_errno(r, "Failed to resolve service: %m");
r = varlink_error_errno(q->varlink_request, r);
}
}
static void resolve_service_hostname_complete(DnsQuery *q) {
int r;
assert(q);
assert(q->auxiliary_for);
if (q->state != DNS_TRANSACTION_SUCCESS) {
resolve_service_all_complete(q->auxiliary_for);
return;
}
r = dns_query_process_cname_many(q);
if (r == DNS_QUERY_CNAME) /* This was a cname, and the query was restarted. */
return;
/* This auxiliary lookup is finished or failed, let's see if all are finished now. */
q->auxiliary_result = r < 0 ? r : 0;
resolve_service_all_complete(q->auxiliary_for);
}
static int resolve_service_hostname(DnsQuery *q, DnsResourceRecord *rr, int ifindex) {
_cleanup_(dns_question_unrefp) DnsQuestion *question = NULL;
_cleanup_(dns_query_freep) DnsQuery *aux = NULL;
int r;
assert(q);
assert(rr);
assert(rr->key);
assert(rr->key->type == DNS_TYPE_SRV);
/* OK, we found an SRV record for the service. Let's resolve
* the hostname included in it */
r = dns_question_new_address(&question, q->request_family, rr->srv.name, false);
if (r < 0)
return r;
r = dns_query_new(q->manager, &aux, question, question, NULL, ifindex, q->flags|SD_RESOLVED_NO_SEARCH);
if (r < 0)
return r;
aux->request_family = q->request_family;
aux->complete = resolve_service_hostname_complete;
r = dns_query_make_auxiliary(aux, q);
if (r == -EAGAIN)
/* Too many auxiliary lookups? If so, don't complain,
* let's just not add this one, we already have more
* than enough */
return 0;
if (r < 0)
return r;
/* Note that auxiliary queries do not track the original
* client, only the primary request does that. */
r = dns_query_go(aux);
if (r < 0)
return r;
TAKE_PTR(aux);
return 1;
}
static void vl_method_resolve_service_complete(DnsQuery *query) {
_cleanup_(dns_query_freep) DnsQuery *q = query;
bool has_root_domain = false;
DnsResourceRecord *rr;
DnsQuestion *question;
unsigned found = 0;
int ifindex, r;
assert(q);
if (q->state != DNS_TRANSACTION_SUCCESS) {
r = reply_query_state(q);
goto finish;
}
r = dns_query_process_cname_many(q);
if (r == -ELOOP) {
r = varlink_error(q->varlink_request, "io.systemd.Resolve.CNAMELoop", NULL);
goto finish;
}
if (r < 0)
goto finish;
if (r == DNS_QUERY_CNAME) {
/* This was a cname, and the query was restarted. */
TAKE_PTR(q);
return;
}
question = dns_query_question_for_protocol(q, q->answer_protocol);
DNS_ANSWER_FOREACH_IFINDEX(rr, ifindex, q->answer) {
r = dns_question_matches_rr(question, rr, NULL);
if (r < 0)
goto finish;
if (r == 0)
continue;
if (rr->key->type != DNS_TYPE_SRV)
continue;
if (dns_name_is_root(rr->srv.name)) {
has_root_domain = true;
continue;
}
if ((q->flags & SD_RESOLVED_NO_ADDRESS) == 0) {
q->block_all_complete++;
r = resolve_service_hostname(q, rr, ifindex);
q->block_all_complete--;
if (r < 0)
goto finish;
}
found++;
}
if (has_root_domain && found <= 0) {
/* If there's exactly one SRV RR and it uses the root domain as hostname, then the service is
* explicitly not offered on the domain. Report this as a recognizable error. See RFC 2782,
* Section "Usage Rules". */
r = varlink_error(q->varlink_request, "io.systemd.Resolve.ServiceNotProvided", NULL);
goto finish;
}
if (found <= 0) {
r = varlink_error(q->varlink_request, "io.systemd.Resolve.NoSuchResourceRecord", NULL);
goto finish;
}
/* Maybe we are already finished? check now... */
resolve_service_all_complete(TAKE_PTR(q));
return;
finish:
if (r < 0) {
log_error_errno(r, "Failed to send address reply: %m");
r = varlink_error_errno(q->varlink_request, r);
}
}
static int vl_method_resolve_service(Varlink* link, JsonVariant* parameters, VarlinkMethodFlags flags, void* userdata) {
static const JsonDispatch dispatch_table[] = {
{ "name", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParametersResolveService, name), JSON_MANDATORY },
{ "type", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParametersResolveService, type), JSON_MANDATORY },
{ "domain", JSON_VARIANT_STRING, json_dispatch_const_string, offsetof(LookupParametersResolveService, domain), JSON_MANDATORY },
{ "ifindex", JSON_VARIANT_UNSIGNED, json_dispatch_int, offsetof(LookupParametersResolveService, ifindex), 0 },
{ "family", JSON_VARIANT_INTEGER, json_dispatch_int, offsetof(LookupParametersResolveService, family), 0 },
{ "flags", JSON_VARIANT_UNSIGNED, json_dispatch_uint64, offsetof(LookupParametersResolveService, in_flags), 0 },
{}
};
_cleanup_(dns_question_unrefp) DnsQuestion *question_idna = NULL, *question_utf8 = NULL;
LookupParametersResolveService p = {
.family = AF_UNSPEC,
};
_cleanup_(dns_query_freep) DnsQuery *q = NULL;
Manager *m;
int r;
assert(link);
m = varlink_server_get_userdata(varlink_get_server(link));
assert(m);
if (FLAGS_SET(flags, VARLINK_METHOD_ONEWAY))
return -EINVAL;
r = varlink_dispatch(link, parameters, dispatch_table, &p);
if (r < 0)
return r;
if (p.ifindex < 0)
return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("ifindex"));
if (!IN_SET(p.family, AF_INET, AF_INET6, AF_UNSPEC))
return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("family"));
if (isempty(p.name))
p.name = NULL;
else if (!dns_service_name_is_valid(p.name))
return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("name"));
if (isempty(p.type))
p.type = NULL;
else if (!dns_srv_type_is_valid(p.type))
return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("type"));
r = dns_name_is_valid(p.domain);
if (r < 0)
return r;
if (r == 0)
return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("domain"));
if (!validate_and_mangle_flags(p.name, &p.in_flags, 0))
return varlink_error_invalid_parameter(link, JSON_VARIANT_STRING_CONST("flags"));
r = dns_question_new_service(&question_utf8, p.name, p.type, p.domain, !(p.in_flags & SD_RESOLVED_NO_TXT), false);
if (r < 0)
return r;
r = dns_question_new_service(&question_idna, p.name, p.type, p.domain, !(p.in_flags & SD_RESOLVED_NO_TXT), true);
if (r < 0)
return r;
r = dns_query_new(m, &q, question_utf8, question_idna, NULL, p.ifindex, p.in_flags|SD_RESOLVED_NO_SEARCH);
if (r < 0)
return r;
q->varlink_request = varlink_ref(link);
q->request_family = p.family;
q->complete = vl_method_resolve_service_complete;
varlink_set_userdata(link, q);
r = dns_query_go(q);
if (r < 0)
return r;
TAKE_PTR(q);
return 1;
}
static int vl_method_subscribe_query_results(Varlink *link, JsonVariant *parameters, VarlinkMethodFlags flags, void *userdata) {
Manager *m;
int r;
@ -762,7 +1288,8 @@ static int varlink_main_server_init(Manager *m) {
r = varlink_server_bind_method_many(
s,
"io.systemd.Resolve.ResolveHostname", vl_method_resolve_hostname,
"io.systemd.Resolve.ResolveAddress", vl_method_resolve_address);
"io.systemd.Resolve.ResolveAddress", vl_method_resolve_address,
"io.systemd.Resolve.ResolveService", vl_method_resolve_service);
if (r < 0)
return log_error_errno(r, "Failed to register varlink methods: %m");

View File

@ -32,6 +32,33 @@ static VARLINK_DEFINE_METHOD(
VARLINK_DEFINE_OUTPUT_BY_TYPE(names, ResolvedName, VARLINK_ARRAY),
VARLINK_DEFINE_OUTPUT(flags, VARLINK_INT, 0));
static VARLINK_DEFINE_STRUCT_TYPE(
ResolvedService,
VARLINK_DEFINE_FIELD(priority, VARLINK_INT, 0),
VARLINK_DEFINE_FIELD(weight, VARLINK_INT, 0),
VARLINK_DEFINE_FIELD(port, VARLINK_INT, 0),
VARLINK_DEFINE_FIELD(hostname, VARLINK_STRING, 0));
static VARLINK_DEFINE_STRUCT_TYPE(
ResolvedCanonical,
VARLINK_DEFINE_FIELD(name, VARLINK_STRING, 0),
VARLINK_DEFINE_FIELD(type, VARLINK_STRING, 0),
VARLINK_DEFINE_FIELD(domain, VARLINK_STRING, 0));
static VARLINK_DEFINE_METHOD(
ResolveService,
VARLINK_DEFINE_INPUT(name, VARLINK_STRING, 0),
VARLINK_DEFINE_INPUT(type, VARLINK_STRING, 0),
VARLINK_DEFINE_INPUT(domain, VARLINK_STRING, 0),
VARLINK_DEFINE_INPUT(ifindex, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(family, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_INPUT(flags, VARLINK_INT, VARLINK_NULLABLE),
VARLINK_DEFINE_OUTPUT_BY_TYPE(srv, ResolvedService, 0),
VARLINK_DEFINE_OUTPUT_BY_TYPE(addr, ResolvedAddress, VARLINK_ARRAY),
VARLINK_DEFINE_OUTPUT(txt, VARLINK_STRING, VARLINK_ARRAY),
VARLINK_DEFINE_OUTPUT(normalized, VARLINK_STRING, 0),
VARLINK_DEFINE_OUTPUT_BY_TYPE(canonical, ResolvedCanonical, 0));
static VARLINK_DEFINE_ERROR(NoNameServers);
static VARLINK_DEFINE_ERROR(NoSuchResourceRecord);
static VARLINK_DEFINE_ERROR(QueryTimedOut);
@ -61,8 +88,11 @@ VARLINK_DEFINE_INTERFACE(
"io.systemd.Resolve",
&vl_method_ResolveHostname,
&vl_method_ResolveAddress,
&vl_method_ResolveService,
&vl_type_ResolvedAddress,
&vl_type_ResolvedName,
&vl_type_ResolvedService,
&vl_type_ResolvedCanonical,
&vl_error_NoNameServers,
&vl_error_NoSuchResourceRecord,
&vl_error_QueryTimedOut,

View File

@ -53,6 +53,7 @@ follow14.final A 10.0.0.14
myservice A 10.0.0.20
myservice AAAA fd00:dead:beef:cafe::17
_mysvc._tcp SRV 10 5 1234 myservice
_mysvc._tcp TXT "This is TXT for myservice"
_invalidsvc._udp SRV 5 5 1111 invalidservice

View File

@ -434,6 +434,18 @@ grep -qF "myservice.signed.test:1234" "$RUN_OUT"
grep -qF "10.0.0.20" "$RUN_OUT"
grep -qF "fd00:dead:beef:cafe::17" "$RUN_OUT"
grep -qF "authenticated: yes" "$RUN_OUT"
# Test service resolve over Varlink
run varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveService '{"name":"","type":"_mysvc._tcp","domain":"signed.test"}'
grep -qF '"srv":{"priority":10,"weight":5,"port":1234,"hostname":"myservice.signed.test"}' "$RUN_OUT"
grep -qF '"addr":[{"ifindex":' "$RUN_OUT"
grep -qF '"family":10,"address":[253,0,222,173,190,239,202,254,0,0,0,0,0,0,0,23]' "$RUN_OUT"
grep -qF '"family":2,"address":[10,0,0,20]' "$RUN_OUT"
grep -qF '"normalized":"myservice.signed.test"' "$RUN_OUT"
grep -qF '"canonical":{"name":null,"type":"_mysvc._tcp","domain":"signed.test"}' "$RUN_OUT"
TXT_OUT=$(grep -a -o -P '(?<=\"txt\"\:\[\").*(?=\"\])' "$RUN_OUT" | base64 --decode)
assert_in "This is TXT for myservice" "$TXT_OUT"
(! run resolvectl service _invalidsvc._udp signed.test)
grep -qE "invalidservice\.signed\.test' not found" "$RUN_OUT"
run resolvectl service _untrustedsvc._udp signed.test