]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
resolvectl: add new "monitor" verb
authorLennart Poettering <lennart@poettering.net>
Wed, 28 Sep 2022 10:46:21 +0000 (12:46 +0200)
committerLennart Poettering <lennart@poettering.net>
Fri, 30 Sep 2022 12:24:08 +0000 (14:24 +0200)
man/resolvectl.xml
src/resolve/resolvectl.c

index 19fb0780b5becf13adf2a54c89485123e421d5b5..a9cdfe91872ce6c3ad7bdfd6465f1e6505dd0e01 100644 (file)
         automatically, an explicit reverting is not necessary in that case.</para></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><command>monitor</command></term>
+
+        <listitem><para>Show a continous stream of local client resolution queries and their
+        responses. Whenever a local query is completed the query's DNS resource lookup key and resource
+        records are shown. Note that this displays queries issued locally only, and does not immediately
+        relate to DNS requests submitted to configured DNS servers or the LLMNR or MulticastDNS zones, as
+        lookups may be answered from the local cache, or might result in multiple DNS transactions (for
+        example to validate DNSSEC information). If CNAME/CNAME redirection chains are followed, a separate
+        query will be displayed for each element of the chain. Use <option>--json=</option> to enable JSON
+        output.</para></listitem>
+      </varlistentry>
+
       <xi:include href="systemctl.xml" xpointer="log-level" />
     </variablelist>
   </refsect1>
         query response are shown. Otherwise, this output is suppressed.</para></listitem>
       </varlistentry>
 
+      <xi:include href="standard-options.xml" xpointer="json" />
+
+      <varlistentry>
+        <term><option>-j</option></term>
+
+        <listitem><para>Short for <option>--json=auto</option></para></listitem>
+      </varlistentry>
+
+      <xi:include href="standard-options.xml" xpointer="no-pager" />
       <xi:include href="standard-options.xml" xpointer="help" />
       <xi:include href="standard-options.xml" xpointer="version" />
-      <xi:include href="standard-options.xml" xpointer="no-pager" />
     </variablelist>
   </refsect1>
 
index c069763e155af3aeb35d24fe073be3ada92f1e3d..b2a5b7263f0bfd92b4ebb85372811ecfce1b46f7 100644 (file)
 #include "bus-map-properties.h"
 #include "bus-message-util.h"
 #include "dns-domain.h"
+#include "errno-list.h"
 #include "escape.h"
 #include "format-table.h"
 #include "format-util.h"
 #include "gcrypt-util.h"
 #include "hostname-util.h"
+#include "json.h"
 #include "main-func.h"
 #include "missing_network.h"
 #include "netlink-util.h"
@@ -41,6 +43,7 @@
 #include "strv.h"
 #include "terminal-util.h"
 #include "utf8.h"
+#include "varlink.h"
 #include "verb-log-control.h"
 #include "verbs.h"
 
@@ -51,6 +54,7 @@ static uint16_t arg_type = 0;
 static uint16_t arg_class = 0;
 static bool arg_legend = true;
 static uint64_t arg_flags = 0;
+static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
 static PagerFlags arg_pager_flags = 0;
 bool arg_ifindex_permissive = false; /* If true, don't generate an error if the specified interface index doesn't exist */
 static const char *arg_service_family = NULL;
@@ -2503,6 +2507,227 @@ static int verb_log_level(int argc, char *argv[], void *userdata) {
         return verb_log_control_common(bus, "org.freedesktop.resolve1", argv[0], argc == 2 ? argv[1] : NULL);
 }
 
+static int monitor_rkey_from_json(JsonVariant *v, DnsResourceKey **ret_key) {
+        _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+        uint16_t type = 0, class = 0;
+        const char *name = NULL;
+        int r;
+
+        JsonDispatch dispatch_table[] = {
+                { "class", JSON_VARIANT_INTEGER, json_dispatch_uint16,       PTR_TO_SIZE(&class), JSON_MANDATORY },
+                { "type",  JSON_VARIANT_INTEGER, json_dispatch_uint16,       PTR_TO_SIZE(&type),  JSON_MANDATORY },
+                { "name",  JSON_VARIANT_STRING,  json_dispatch_const_string, PTR_TO_SIZE(&name),  JSON_MANDATORY },
+                {}
+        };
+
+        assert(v);
+        assert(ret_key);
+
+        r = json_dispatch(v, dispatch_table, NULL, 0, NULL);
+        if (r < 0)
+                return r;
+
+        key = dns_resource_key_new(class, type, name);
+        if (!key)
+                return -ENOMEM;
+
+        *ret_key = TAKE_PTR(key);
+        return 0;
+}
+
+static int print_question(char prefix, const char *color, JsonVariant *question) {
+        JsonVariant *q = NULL;
+        int r;
+
+        assert(color);
+
+        JSON_VARIANT_ARRAY_FOREACH(q, question) {
+                _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL;
+                char buf[DNS_RESOURCE_KEY_STRING_MAX];
+
+                r = monitor_rkey_from_json(q, &key);
+                if (r < 0) {
+                        log_warning_errno(r, "Received monitor message with invalid question key, ignoring: %m");
+                        continue;
+                }
+
+                printf("%s%s %c%s: %s\n",
+                       color,
+                       special_glyph(SPECIAL_GLYPH_ARROW_RIGHT),
+                       prefix,
+                       ansi_normal(),
+                       dns_resource_key_to_string(key, buf, sizeof(buf)));
+        }
+
+        return 0;
+}
+
+static int print_answer(JsonVariant *answer) {
+        JsonVariant *a;
+        int r;
+
+        JSON_VARIANT_ARRAY_FOREACH(a, answer) {
+                _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+                _cleanup_free_ void *d = NULL;
+                JsonVariant *jraw;
+                const char *s;
+                size_t l;
+
+                jraw = json_variant_by_key(a, "raw");
+                if (!jraw) {
+                        log_warning("Received monitor answer lacking valid raw data, ignoring.");
+                        continue;
+                }
+
+                r = json_variant_unbase64(jraw, &d, &l);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to undo base64 encoding of monitor answer raw data, ignoring.");
+                        continue;
+                }
+
+                r = dns_resource_record_new_from_raw(&rr, d, l);
+                if (r < 0) {
+                        log_warning_errno(r, "Failed to parse monitor answer RR, ingoring: %m");
+                        continue;
+                }
+
+                s = dns_resource_record_to_string(rr);
+                if (!s)
+                        return log_oom();
+
+                printf("%s%s A%s: %s\n",
+                       ansi_highlight_yellow(),
+                       special_glyph(SPECIAL_GLYPH_ARROW_LEFT),
+                       ansi_normal(),
+                       s);
+        }
+
+        return 0;
+}
+
+static void monitor_query_dump(JsonVariant *v) {
+        _cleanup_(json_variant_unrefp) JsonVariant *question = NULL, *answer = NULL, *collected_questions = NULL;
+        int rcode = -1, error = 0, r;
+        const char *state = NULL;
+
+        assert(v);
+
+        JsonDispatch dispatch_table[] = {
+                { "question",           JSON_VARIANT_ARRAY,   json_dispatch_variant,      PTR_TO_SIZE(&question),            JSON_MANDATORY },
+                { "answer",             JSON_VARIANT_ARRAY,   json_dispatch_variant,      PTR_TO_SIZE(&answer),              0              },
+                { "collectedQuestions", JSON_VARIANT_ARRAY,   json_dispatch_variant,      PTR_TO_SIZE(&collected_questions), 0              },
+                { "state",              JSON_VARIANT_STRING,  json_dispatch_const_string, PTR_TO_SIZE(&state),               JSON_MANDATORY },
+                { "rcode",              JSON_VARIANT_INTEGER, json_dispatch_int,          PTR_TO_SIZE(&rcode),               0              },
+                { "errno",              JSON_VARIANT_INTEGER, json_dispatch_int,          PTR_TO_SIZE(&error),               0              },
+                {}
+        };
+
+        r = json_dispatch(v, dispatch_table, NULL, 0, NULL);
+        if (r < 0)
+                return (void) log_warning("Received malformed monitor message, ignoring.");
+
+        /* First show the current question */
+        print_question('Q', ansi_highlight_cyan(), question);
+
+        /* And then show the questions that led to this one in case this was a CNAME chain */
+        print_question('C', ansi_highlight_grey(), collected_questions);
+
+        printf("%s%s S%s: %s\n",
+               streq_ptr(state, "success") ? ansi_highlight_green() : ansi_highlight_red(),
+               special_glyph(SPECIAL_GLYPH_ARROW_LEFT),
+               ansi_normal(),
+               strna(streq_ptr(state, "errno") ? errno_to_name(error) :
+                     streq_ptr(state, "rcode-failure") ? dns_rcode_to_string(rcode) :
+                     state));
+
+        print_answer(answer);
+}
+
+static int monitor_reply(
+                Varlink *link,
+                JsonVariant *parameters,
+                const char *error_id,
+                VarlinkReplyFlags flags,
+                void *userdata) {
+
+        assert(link);
+
+        if (error_id) {
+                bool disconnect;
+
+                disconnect = streq(error_id, VARLINK_ERROR_DISCONNECTED);
+                if (disconnect)
+                        log_info("Disconnected.");
+                else
+                        log_error("Varlink error: %s", error_id);
+
+                (void) sd_event_exit(ASSERT_PTR(varlink_get_event(link)), disconnect ? EXIT_SUCCESS : EXIT_FAILURE);
+                return 0;
+        }
+
+        if (json_variant_by_key(parameters, "ready")) {
+                /* The first message coming in will just indicate that we are now subscribed. We let our
+                 * caller know if they asked for it. Once the caller sees this they should know that we are
+                 * not going to miss any queries anymore. */
+                (void) sd_notify(/* unset_environment=false */ false, "READY=1");
+                return 0;
+        }
+
+        if (arg_json_format_flags & JSON_FORMAT_OFF) {
+                monitor_query_dump(parameters);
+                printf("\n");
+        } else
+                json_variant_dump(parameters, arg_json_format_flags, NULL, NULL);
+
+        fflush(stdout);
+
+        return 0;
+}
+
+static int verb_monitor(int argc, char *argv[], void *userdata) {
+        _cleanup_(sd_event_unrefp) sd_event *event = NULL;
+        _cleanup_(varlink_unrefp) Varlink *vl = NULL;
+        int r, c;
+
+        r = sd_event_default(&event);
+        if (r < 0)
+                return log_error_errno(r, "Failed to get event loop: %m");
+
+        r = sd_event_set_signal_exit(event, true);
+        if (r < 0)
+                return log_error_errno(r, "Failed to enable exit on SIGINT/SIGTERM: %m");
+
+        r = varlink_connect_address(&vl, "/run/systemd/resolve/io.systemd.Resolve.Monitor");
+        if (r < 0)
+                return log_error_errno(r, "Failed to connect to query monitoring service /run/systemd/resolve/io.systemd.Resolve.Monitor: %m");
+
+        r = varlink_set_relative_timeout(vl, USEC_INFINITY); /* We want the monitor to run basically forever */
+        if (r < 0)
+                return log_error_errno(r, "Failed to set varlink time-out: %m");
+
+        r = varlink_attach_event(vl, event, SD_EVENT_PRIORITY_NORMAL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to attach varlink connection to event loop: %m");
+
+        r = varlink_bind_reply(vl, monitor_reply);
+        if (r < 0)
+                return log_error_errno(r, "Failed to bind reply callback to varlink connection: %m");
+
+        r = varlink_observe(vl, "io.systemd.Resolve.Monitor.SubscribeQueryResults", NULL);
+        if (r < 0)
+                return log_error_errno(r, "Failed to issue SubscribeQueryResults() varlink call: %m");
+
+        r = sd_event_loop(event);
+        if (r < 0)
+                return log_error_errno(r, "Failed to run event loop: %m");
+
+        r = sd_event_get_exit_code(event, &c);
+        if (r < 0)
+                return log_error_errno(r, "Failed to get exit code: %m");
+
+        return c;
+}
+
 static void help_protocol_types(void) {
         if (arg_legend)
                 puts("Known protocol types:");
@@ -2608,6 +2833,7 @@ static int native_help(void) {
                "  reset-statistics             Reset resolver statistics\n"
                "  flush-caches                 Flush all local DNS caches\n"
                "  reset-server-features        Forget learnt DNS server feature levels\n"
+               "  monitor                      Monitor DNS queries\n"
                "  dns [LINK [SERVER...]]       Get/set per-interface DNS server address\n"
                "  domain [LINK [DOMAIN...]]    Get/set per-interface search domain\n"
                "  default-route [LINK [BOOL]]  Get/set per-interface default route flag\n"
@@ -2636,11 +2862,16 @@ static int native_help(void) {
                "     --cache=BOOL              Allow response from cache (default: yes)\n"
                "     --zone=BOOL               Allow response from locally registered mDNS/LLMNR\n"
                "                               records (default: yes)\n"
-               "     --trust-anchor=BOOL       Allow response from local trust anchor (default: yes)\n"
+               "     --trust-anchor=BOOL       Allow response from local trust anchor (default:\n"
+               "                               yes)\n"
                "     --network=BOOL            Allow response from network (default: yes)\n"
-               "     --search=BOOL             Use search domains for single-label names (default: yes)\n"
+               "     --search=BOOL             Use search domains for single-label names (default:\n"
+               "                               yes)\n"
                "     --raw[=payload|packet]    Dump the answer as binary data\n"
                "     --legend=BOOL             Print headers and additional info (default: yes)\n"
+               "     --json=MODE               Output as JSON\n"
+               "  -j                           Same as --json=pretty on tty, --json=short\n"
+               "                               otherwise\n"
                "\nSee the %s for details.\n",
                program_invocation_short_name,
                ansi_highlight(),
@@ -2987,6 +3218,7 @@ static int native_parse_argv(int argc, char *argv[]) {
                 ARG_RAW,
                 ARG_SEARCH,
                 ARG_NO_PAGER,
+                ARG_JSON,
         };
 
         static const struct option options[] = {
@@ -3009,6 +3241,7 @@ static int native_parse_argv(int argc, char *argv[]) {
                 { "raw",                   optional_argument, NULL, ARG_RAW                   },
                 { "search",                required_argument, NULL, ARG_SEARCH                },
                 { "no-pager",              no_argument,       NULL, ARG_NO_PAGER              },
+                { "json",                  required_argument, NULL, ARG_JSON                  },
                 {}
         };
 
@@ -3017,7 +3250,7 @@ static int native_parse_argv(int argc, char *argv[]) {
         assert(argc >= 0);
         assert(argv);
 
-        while ((c = getopt_long(argc, argv, "h46i:t:c:p:", options, NULL)) >= 0)
+        while ((c = getopt_long(argc, argv, "h46i:t:c:p:j", options, NULL)) >= 0)
                 switch (c) {
 
                 case 'h':
@@ -3192,6 +3425,17 @@ static int native_parse_argv(int argc, char *argv[]) {
                         arg_pager_flags |= PAGER_DISABLE;
                         break;
 
+                case ARG_JSON:
+                        r = parse_json_argument(optarg, &arg_json_format_flags);
+                        if (r <= 0)
+                                return r;
+
+                        break;
+
+                case 'j':
+                        arg_json_format_flags = JSON_FORMAT_PRETTY_AUTO|JSON_FORMAT_COLOR_AUTO;
+                        break;
+
                 case '?':
                         return -EINVAL;
 
@@ -3235,6 +3479,7 @@ static int native_main(int argc, char *argv[], sd_bus *bus) {
                 { "nta",                   VERB_ANY, VERB_ANY, 0,            verb_nta              },
                 { "revert",                VERB_ANY, 2,        0,            verb_revert_link      },
                 { "log-level",             VERB_ANY, 2,        0,            verb_log_level        },
+                { "monitor",               VERB_ANY, 1,        0,            verb_monitor          },
                 {}
         };