]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
machined: implement resolve hook in machined
authorLennart Poettering <lennart@poettering.net>
Thu, 9 Oct 2025 20:01:04 +0000 (22:01 +0200)
committerLennart Poettering <lennart@poettering.net>
Sat, 15 Nov 2025 06:44:24 +0000 (07:44 +0100)
This basically implements nss-myhostname, but natively in
systemd-resolved, so that the logic becomes available also for clients
using the local DNS stub for resolution or the D-Bus or Varlink APIs.

17 files changed:
src/machine/machine.c
src/machine/machined-resolve-hook.c [new file with mode: 0644]
src/machine/machined-resolve-hook.h [new file with mode: 0644]
src/machine/machined-varlink.c
src/machine/machined.c
src/machine/machined.h
src/machine/meson.build
src/shared/dns-answer.c
src/shared/dns-answer.h
src/shared/dns-question.c
src/shared/dns-question.h
src/shared/dns-rr.c
src/shared/dns-rr.h
src/shared/meson.build
src/shared/resolve-hook-util.c [new file with mode: 0644]
src/shared/resolve-hook-util.h [new file with mode: 0644]
test/units/TEST-13-NSPAWN.nss-mymachines.sh

index 0350c16df682363c8695ad1c0c911598e9f52421..f097bfe380108f3924f72e0268c2a45ada4b2194 100644 (file)
@@ -24,6 +24,7 @@
 #include "log.h"
 #include "machine.h"
 #include "machine-dbus.h"
+#include "machined-resolve-hook.h"
 #include "machined.h"
 #include "mkdir-label.h"
 #include "namespace-util.h"
@@ -674,6 +675,8 @@ int machine_start(Machine *m, sd_bus_message *properties, sd_bus_error *error) {
 
         machine_send_signal(m, "MachineNew");
 
+        (void) manager_notify_hook_filters(m->manager);
+
         return 0;
 }
 
@@ -732,6 +735,8 @@ int machine_finalize(Machine *m) {
         if (m->started) {
                 machine_send_signal(m, "MachineRemoved");
                 m->started = false;
+
+                (void) manager_notify_hook_filters(m->manager);
         }
 
         return 0;
diff --git a/src/machine/machined-resolve-hook.c b/src/machine/machined-resolve-hook.c
new file mode 100644 (file)
index 0000000..c0bd5fd
--- /dev/null
@@ -0,0 +1,200 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-json.h"
+
+#include "dns-answer.h"
+#include "dns-domain.h"
+#include "dns-packet.h"
+#include "dns-question.h"
+#include "dns-rr.h"
+#include "hashmap.h"
+#include "json-util.h"
+#include "local-addresses.h"
+#include "log.h"
+#include "machine.h"
+#include "machined.h"
+#include "machined-resolve-hook.h"
+#include "resolve-hook-util.h"
+#include "set.h"
+#include "varlink-util.h"
+
+static int manager_make_machine_array(Manager *m, sd_json_variant **ret) {
+        int r;
+
+        assert(m);
+        assert(ret);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
+        Machine *machine;
+        HASHMAP_FOREACH(machine, m->machines) {
+                if (machine->class == MACHINE_HOST)
+                        continue;
+                if (!machine->started)
+                        continue;
+
+                r = sd_json_variant_append_arrayb(&array, SD_JSON_BUILD_STRING(machine->name));
+                if (r < 0)
+                        return r;
+        }
+
+        if (!array)
+                return sd_json_variant_new_array(ret, /* array= */ NULL, /* n= */ 0);
+
+        *ret = TAKE_PTR(array);
+        return 0;
+}
+
+int manager_notify_hook_filters(Manager *m) {
+        int r;
+
+        assert(m);
+
+        /* Called whenever a machine is added or dropped from the list */
+
+        if (set_isempty(m->query_filter_subscriptions))
+                return 0;
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
+        r = manager_make_machine_array(m, &array);
+        if (r < 0)
+                return log_error_errno(r, "Failed to generate JSON array with machine names: %m");
+
+        r = varlink_many_notifybo(m->query_filter_subscriptions, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array));
+        if (r < 0)
+                return log_error_errno(r, "Failed to notify filter subscribers: %m");
+
+        return 0;
+}
+
+int vl_method_query_filter(
+                sd_varlink *link,
+                sd_json_variant *parameters,
+                sd_varlink_method_flags_t flags,
+                void *userdata) {
+
+        Manager *m = ASSERT_PTR(userdata);
+        int r;
+
+        assert(link);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *array = NULL;
+        r = manager_make_machine_array(m, &array);
+        if (r < 0)
+                return r;
+
+        if (flags & SD_VARLINK_METHOD_MORE) {
+                /* If 'more' is set, this is a subscription request, keep track of the link */
+
+                r = sd_varlink_notifybo(link, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to notify filter subscribers: %m");
+
+                r = set_ensure_put(&m->query_filter_subscriptions, &varlink_hash_ops, link);
+                if (r < 0)
+                        return r;
+
+                sd_varlink_ref(link);
+        } else {
+                r = sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_VARIANT("filterDomains", array));
+                if (r < 0)
+                        return log_error_errno(r, "Failed to notify filter subscribers: %m");
+        }
+
+        return 0;
+}
+
+int vl_method_resolve_record(
+                sd_varlink *link,
+                sd_json_variant *parameters,
+                sd_varlink_method_flags_t flags,
+                void *userdata) {
+
+        Manager *m = ASSERT_PTR(userdata);
+        int r;
+
+        assert(link);
+
+        _cleanup_(resolve_record_parameters_done) ResolveRecordParameters p = {};
+        r = sd_varlink_dispatch(link, parameters, resolve_record_parameters_dispatch_table, &p);
+        if (r != 0)
+                return r;
+
+        if (dns_question_isempty(p.question))
+                return sd_varlink_error_invalid_parameter_name(link, "question");
+
+        _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL;
+
+        _cleanup_free_ struct local_address *addresses = NULL;
+        bool found = false, nxdomain = false;
+        int n_addresses = -1;
+
+        DnsResourceKey *key;
+        DNS_QUESTION_FOREACH(key, p.question) {
+                Machine *machine = hashmap_get(m->machines, dns_resource_key_name(key));
+                if (machine) {
+                        /* We found a perfect match, yay! */
+                        found = true;
+
+                        if (!dns_resource_key_is_address(key))
+                                continue;
+
+                        if (n_addresses < 0) {
+                                n_addresses = machine_get_addresses(machine, &addresses);
+                                if (n_addresses < 0)
+                                        return n_addresses;
+                        }
+
+                        int family = dns_type_to_af(key->type);
+                        FOREACH_ARRAY(address, addresses, n_addresses) {
+                                if (address->family != family)
+                                        continue;
+
+                                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+                                r = dns_resource_record_new_address(&rr, address->family, &address->address, machine->name);
+                                if (r < 0)
+                                        return r;
+
+                                r = dns_answer_add_extend(
+                                                &answer,
+                                                rr,
+                                                machine->n_netif == 1 ? machine->netif[0] : -1,
+                                                DNS_ANSWER_AUTHENTICATED,
+                                                /* rrsig= */ NULL);
+                                if (r < 0)
+                                        return r;
+                        }
+                }
+
+                /* So this is not a direct match? Then check if we find a prefix match */
+                const char *q = dns_resource_key_name(key);
+                while (!nxdomain) {
+                        r = dns_name_parent(&q);
+                        if (r < 0)
+                                return r;
+                        if (r == 0)
+                                break;
+
+                        nxdomain = !!hashmap_get(m->machines, q);
+                }
+        }
+
+        if (!found) {
+                /* If we found a prefix match we own the subtree, and thus return NXDOMAIN because we know
+                 * that we only expose the machine A/AAAA records on the primary name, but nothing below. */
+                if (nxdomain)
+                        return sd_varlink_replybo(link, SD_JSON_BUILD_PAIR_INTEGER("rcode", DNS_RCODE_NXDOMAIN));
+
+                /* Otherwise we return an empty response, which means: continue with the usual lookup */
+                return sd_varlink_reply(link, /* parameters= */ NULL);
+        }
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *ja = NULL;
+        r = dns_answer_to_json(answer, &ja);
+        if (r < 0)
+                return r;
+
+        return sd_varlink_replybo(
+                        link,
+                        SD_JSON_BUILD_PAIR_INTEGER("rcode", DNS_RCODE_SUCCESS),
+                        SD_JSON_BUILD_PAIR_VARIANT("answer", ja));
+}
diff --git a/src/machine/machined-resolve-hook.h b/src/machine/machined-resolve-hook.h
new file mode 100644 (file)
index 0000000..df10562
--- /dev/null
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "machine-forward.h"
+
+int vl_method_query_filter(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+int vl_method_resolve_record(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+
+int manager_notify_hook_filters(Manager *m);
index 672bf919a0d0032c263a2ccc0aa8119c2d5406bf..8188aeb4b134c143424583e25a4f0db7a9bd2ec4 100644 (file)
 #include "machine.h"
 #include "machine-varlink.h"
 #include "machined.h"
+#include "machined-resolve-hook.h"
 #include "machined-varlink.h"
 #include "path-lookup.h"
+#include "set.h"
 #include "string-util.h"
 #include "strv.h"
 #include "user-util.h"
 #include "varlink-io.systemd.Machine.h"
 #include "varlink-io.systemd.MachineImage.h"
 #include "varlink-io.systemd.UserDatabase.h"
+#include "varlink-io.systemd.Resolve.Hook.h"
 #include "varlink-io.systemd.service.h"
 #include "varlink-util.h"
 
@@ -847,6 +850,57 @@ static int manager_varlink_init_machine(Manager *m) {
         return 0;
 }
 
+static void on_resolve_hook_disconnect(sd_varlink_server *server, sd_varlink *link, void *userdata) {
+        Manager *m = ASSERT_PTR(userdata);
+
+        if (set_remove(m->query_filter_subscriptions, link))
+                sd_varlink_unref(link);
+}
+
+static int manager_varlink_init_resolve_hook(Manager *m) {
+        _cleanup_(sd_varlink_server_unrefp) sd_varlink_server *s = NULL;
+        int r;
+
+        assert(m);
+
+        if (m->varlink_resolve_hook_server)
+                return 0;
+        if (m->runtime_scope != RUNTIME_SCOPE_SYSTEM) /* no resolved in per-user mode! */
+                return 0;
+
+        r = varlink_server_new(&s, SD_VARLINK_SERVER_ACCOUNT_UID|SD_VARLINK_SERVER_INHERIT_USERDATA, m);
+        if (r < 0)
+                return log_error_errno(r, "Failed to allocate varlink server object: %m");
+
+        (void) sd_varlink_server_set_description(s, "varlink-resolve-hook");
+
+        r = sd_varlink_server_add_interface(s, &vl_interface_io_systemd_Resolve_Hook);
+        if (r < 0)
+                return log_error_errno(r, "Failed to add Resolve.Hook interface to varlink server: %m");
+
+        r = sd_varlink_server_bind_method_many(
+                        s,
+                        "io.systemd.Resolve.Hook.QueryFilter",   vl_method_query_filter,
+                        "io.systemd.Resolve.Hook.ResolveRecord", vl_method_resolve_record);
+        if (r < 0)
+                return log_error_errno(r, "Failed to register varlink methods: %m");
+
+        r = sd_varlink_server_bind_disconnect(s, on_resolve_hook_disconnect);
+        if (r < 0)
+                return log_error_errno(r, "Failed to bind on resolve hook disconnection events: %m");
+
+        r = sd_varlink_server_listen_address(s, "/run/systemd/resolve.hook/io.systemd.Machine", 0666 | SD_VARLINK_SERVER_MODE_MKDIR_0755);
+        if (r < 0)
+                return log_error_errno(r, "Failed to bind to varlink socket: %m");
+
+        r = sd_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_resolve_hook_server = TAKE_PTR(s);
+        return 0;
+}
+
 int manager_varlink_init(Manager *m) {
         int r;
 
@@ -858,6 +912,10 @@ int manager_varlink_init(Manager *m) {
         if (r < 0)
                 return r;
 
+        r = manager_varlink_init_resolve_hook(m);
+        if (r < 0)
+                return r;
+
         return 0;
 }
 
@@ -866,4 +924,5 @@ void manager_varlink_done(Manager *m) {
 
         m->varlink_userdb_server = sd_varlink_server_unref(m->varlink_userdb_server);
         m->varlink_machine_server = sd_varlink_server_unref(m->varlink_machine_server);
+        m->varlink_resolve_hook_server = sd_varlink_server_unref(m->varlink_resolve_hook_server);
 }
index c6b13e6cbabc658fe9b3d52350924721a2ca0dce..b972b182b6e07196881347aff379ac7a82d8b668 100644 (file)
@@ -29,6 +29,7 @@
 #include "operation.h"
 #include "path-lookup.h"
 #include "service-util.h"
+#include "set.h"
 #include "signal-util.h"
 #include "socket-util.h"
 #include "special.h"
@@ -110,6 +111,8 @@ static Manager* manager_unref(Manager *m) {
 
         manager_varlink_done(m);
 
+        m->query_filter_subscriptions = set_free(m->query_filter_subscriptions);
+
         sd_bus_flush_close_unref(m->api_bus);
         sd_bus_flush_close_unref(m->system_bus);
         sd_event_unref(m->event);
index 483d41abedd623976a67a50415822783d90101a4..912b9b0ea786d55840728b18d1c2057eb2e60108 100644 (file)
@@ -32,6 +32,8 @@ typedef struct Manager {
 
         sd_varlink_server *varlink_userdb_server;
         sd_varlink_server *varlink_machine_server;
+        sd_varlink_server *varlink_resolve_hook_server;
+        Set *query_filter_subscriptions;
 
         RuntimeScope runtime_scope;
         char *state_dir;
index 8de7d7d2aea0c577c0408dec86498be48b3b3ada..e8b6c1611fed2e942d7d48187651434c22497d1b 100644 (file)
@@ -16,6 +16,7 @@ systemd_machined_extract_sources = files(
         'machine.c',
         'machined-core.c',
         'machined-dbus.c',
+        'machined-resolve-hook.c',
         'machined-varlink.c',
         'operation.c',
 )
index 7ebbf8795dcd1b1f0af3c0d159fc558f19dbd3f3..eaf2b19a18d7cb5964f2e377284a83b92859d95d 100644 (file)
@@ -2,6 +2,8 @@
 
 #include <stdio.h>
 
+#include "sd-json.h"
+
 #include "alloc-util.h"
 #include "dns-answer.h"
 #include "dns-domain.h"
@@ -866,3 +868,36 @@ uint32_t dns_answer_min_ttl(DnsAnswer *a) {
 
         return ttl;
 }
+
+int dns_answer_to_json(DnsAnswer *answer, sd_json_variant **ret) {
+        int r;
+
+        assert(ret);
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *ja = NULL;
+        DnsResourceRecord *rr;
+        DNS_ANSWER_FOREACH(rr, answer) {
+                _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
+
+                r = dns_resource_record_to_json(rr, &v);
+                if (r < 0)
+                        return r;
+
+                r = dns_resource_record_to_wire_format(rr, /* canonical= */ false);
+                if (r < 0)
+                        return r;
+
+                r = sd_json_variant_append_arraybo(
+                                &ja,
+                                SD_JSON_BUILD_PAIR_VARIANT("rr", v),
+                                SD_JSON_BUILD_PAIR_BASE64("raw", rr->wire_format, rr->wire_format_size));
+                if (r < 0)
+                        return r;
+        }
+
+        if (!ja)
+                return sd_json_variant_new_array(ret, /* array=*/ NULL, /* n= */ 0);
+
+        *ret = TAKE_PTR(ja);
+        return 0;
+}
index 059f896b86f2fa9137d7e8efa97e0902f5a040db..7b12e9be526433d8b4b3fb1a6eef4e9010f5886a 100644 (file)
@@ -104,6 +104,8 @@ void dns_answer_randomize(DnsAnswer *a);
 
 uint32_t dns_answer_min_ttl(DnsAnswer *a);
 
+int dns_answer_to_json(DnsAnswer *answer, sd_json_variant **ret);
+
 DEFINE_TRIVIAL_CLEANUP_FUNC(DnsAnswer*, dns_answer_unref);
 
 typedef struct DnsAnswerIterator {
index d6466e07936e87d4cb4bbed93a6704707a918b7d..ac4cc8e99800753acf0e292aed6ad7f65c09babc 100644 (file)
@@ -7,6 +7,7 @@
 #include "dns-question.h"
 #include "dns-rr.h"
 #include "dns-type.h"
+#include "json-util.h"
 #include "socket-util.h"
 #include "string-util.h"
 
@@ -599,3 +600,37 @@ int dns_question_merge(DnsQuestion *a, DnsQuestion *b, DnsQuestion **ret) {
         *ret = TAKE_PTR(k);
         return 0;
 }
+
+int dns_json_dispatch_question(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
+        DnsQuestion **q = 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));
+
+        _cleanup_(dns_question_unrefp) DnsQuestion *nq = NULL;
+        nq = dns_question_new(sd_json_variant_elements(variant));
+        if (!nq)
+                return json_log_oom(variant, flags);
+
+        sd_json_variant *i;
+        JSON_VARIANT_ARRAY_FOREACH(i, variant) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+
+                static const sd_json_dispatch_field dispatch_table[] = {
+                        { "key", SD_JSON_VARIANT_OBJECT, dns_json_dispatch_resource_key, 0, SD_JSON_MANDATORY },
+                        {}
+                };
+
+                r = sd_json_dispatch(i, dispatch_table, flags, &key);
+                if (r < 0)
+                        return r;
+
+                if (dns_question_add(nq, key, /* flags= */ 0) < 0)
+                        return json_log_oom(variant, flags);
+        }
+
+        dns_question_unref(*q);
+        *q = TAKE_PTR(nq);
+        return 0;
+}
index 4dd3b27713a1b618c302399dfaa6cb0c55960cd3..39650102787b15d90d2084b808f37fc15e9e1a49 100644 (file)
@@ -1,6 +1,8 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 #pragma once
 
+#include "sd-json.h"
+
 #include "shared-forward.h"
 
 /* A simple array of resource keys */
@@ -58,6 +60,8 @@ static inline bool dns_question_isempty(DnsQuestion *q) {
 
 int dns_question_merge(DnsQuestion *a, DnsQuestion *b, DnsQuestion **ret);
 
+int dns_json_dispatch_question(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
+
 DEFINE_TRIVIAL_CLEANUP_FUNC(DnsQuestion*, dns_question_unref);
 
 #define _DNS_QUESTION_FOREACH(u, k, q)                                     \
index 32c26a7612b8d5dcb715558c8ef618c90885c5a6..d0ed39a08b00d48ee0bf9630105c99944229edb6 100644 (file)
@@ -2558,3 +2558,16 @@ static const char* const sshfp_key_type_table[_SSHFP_KEY_TYPE_MAX_DEFINED] = {
         [SSHFP_KEY_TYPE_SHA256]   = "SHA-256",   /* RFC 4255 */
 };
 DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(sshfp_key_type, int, 255);
+
+int dns_json_dispatch_resource_key(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
+        DnsResourceKey **k = ASSERT_PTR(userdata);
+        int r;
+
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *nk = NULL;
+        r = dns_resource_key_from_json(variant, &nk);
+        if (r < 0)
+                return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid resource record key.", strna(name));
+
+        DNS_RESOURCE_KEY_REPLACE(*k, TAKE_PTR(nk));
+        return 0;
+}
index 48f77d3326872396f637a3ffd56f7e5c5e4a0104..23752946e7f58ce2e9af15f1b9b964fad2f77b4d 100644 (file)
@@ -444,3 +444,5 @@ int sshfp_algorithm_from_string(const char *s) _pure_;
 
 int sshfp_key_type_to_string_alloc(int i, char **ret);
 int sshfp_key_type_from_string(const char *s) _pure_;
+
+int dns_json_dispatch_resource_key(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
index 0cf0324f97f3af934c1bbe4eadca059d739f7cd5..acc80d3e347c3d19ea38f7bd9ca87034dd6bfc2d 100644 (file)
@@ -169,6 +169,7 @@ shared_sources = files(
         'recovery-key.c',
         'reread-partition-table.c',
         'resize-fs.c',
+        'resolve-hook-util.c',
         'resolve-util.c',
         'rm-rf.c',
         'seccomp-util.c',
diff --git a/src/shared/resolve-hook-util.c b/src/shared/resolve-hook-util.c
new file mode 100644 (file)
index 0000000..b1e948c
--- /dev/null
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "resolve-hook-util.h"
+#include "dns-question.h"
+
+void resolve_record_parameters_done(ResolveRecordParameters *p) {
+        assert(p);
+
+        dns_question_unref(p->question);
+}
+
+const sd_json_dispatch_field resolve_record_parameters_dispatch_table[] = {
+        { "question", SD_JSON_VARIANT_ARRAY, dns_json_dispatch_question, offsetof(ResolveRecordParameters, question), SD_JSON_MANDATORY },
+        {}
+};
diff --git a/src/shared/resolve-hook-util.h b/src/shared/resolve-hook-util.h
new file mode 100644 (file)
index 0000000..be41eb7
--- /dev/null
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-json.h"
+
+#include "shared-forward.h"
+
+typedef struct ResolveRecordParameters {
+        DnsQuestion *question;
+} ResolveRecordParameters;
+
+void resolve_record_parameters_done(ResolveRecordParameters *p);
+
+extern const sd_json_dispatch_field resolve_record_parameters_dispatch_table[];
index 2735cf61b415dcbc888c61a6b72ed6f50b39209c..5709ec50d706bacbe127169b37b7d05974bae7cd 100755 (executable)
@@ -136,4 +136,12 @@ done
 (! getent group -s mymachines foo 11)
 (! getent passwd -s mymachines foo 11)
 
+# Now check the machined's hook for resolved too
+run_and_grep "10\.1\.0\.2" resolvectl query nss-mymachines-singleip
+
+run_and_grep "fd00:dead:beef:cafe::2" resolvectl query nss-mymachines-manyips
+for i in {100..120}; do
+    run_and_grep "10\.2\.0\.$i" resolvectl query nss-mymachines-manyips
+done
+
 machinectl stop nss-mymachines-{noip,singleip,manyips}