From 32b3aabfe7874dcf4ed5517c14f078a9d5a9c47a Mon Sep 17 00:00:00 2001
From: dongshengyuan <545258830@qq.com>
Date: Fri, 7 Aug 2026 14:19:06 +0800
Subject: [PATCH] resolvectl: render DNS errors as JSON
When --json is requested, resolution errors returned by resolved still went
through text logging. That made scripts parse stderr even though the
varlink error already carried structured fields such as rcode and EDE.
Add a small JSON error dumper for resolvectl query errors. It outputs the
queried name and varlink error id alongside the original varlink parameters
so DNSError and DNSSECValidationFailed expose rcode, extendedDNSErrorCode,
extendedDNSErrorMessage, queryString, and result without duplicating the
schema in resolvectl.
Keep non-JSON text output and silent warn_missing NXDOMAIN handling
unchanged. Document the JSON resolution error object in resolvectl(1),
including the distinction between resolved errors and local invocation or
connection errors.
Closes #42904
---
man/resolvectl.xml | 8 +++++
src/resolve/resolvectl.c | 56 +++++++++++++++++++++++++++-------
test/units/TEST-75-RESOLVED.sh | 6 ++++
3 files changed, 59 insertions(+), 11 deletions(-)
diff --git a/man/resolvectl.xml b/man/resolvectl.xml
index 7d6d4e3fbe..b7a28b47bb 100644
--- a/man/resolvectl.xml
+++ b/man/resolvectl.xml
@@ -72,6 +72,14 @@
If combined with (only supported in combination with
) will output the resource record data in a JSON object.
+ If systemd-resolved returns a resolution error while
+ is enabled, the error is output as a JSON object. It contains the queried
+ name, the resolved varlink error, and any error fields
+ supplied by systemd-resolved, such as rcode,
+ queryString, extendedDNSErrorCode,
+ extendedDNSErrorMessage, and result. Local invocation or
+ connection errors are still reported as regular errors.
+
diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c
index cbe4899162..9af5626059 100644
--- a/src/resolve/resolvectl.c
+++ b/src/resolve/resolvectl.c
@@ -278,6 +278,31 @@ static void print_ifindex_comment(int printed_so_far, int ifindex) {
ansi_grey(), ifname, ansi_normal());
}
+static int dump_resolve_error_json(const char *name, const char *error_id, sd_json_variant *parameters, int ret) {
+ _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
+ int r;
+
+ assert(name);
+ assert(!isempty(error_id));
+
+ if (parameters)
+ j = sd_json_variant_ref(parameters);
+
+ r = sd_json_variant_set_field_string(&j, "name", name);
+ if (r < 0)
+ return r;
+
+ r = sd_json_variant_set_field_string(&j, "error", error_id);
+ if (r < 0)
+ return r;
+
+ r = sd_json_variant_dump(j, arg_json_format_flags, /* f= */ NULL, /* prefix= */ NULL);
+ if (r < 0)
+ return r;
+
+ return ret;
+}
+
static int varlink_log_resolve_error(const char *name, const char *error_id, sd_json_variant *reply, bool warn_missing) {
int r;
@@ -285,6 +310,26 @@ static int varlink_log_resolve_error(const char *name, const char *error_id, sd_
assert(!isempty(error_id));
int ret = sd_varlink_error_to_errno(error_id, reply);
+ _cleanup_(resolve_error_done) ResolveError error = {
+ .rcode = _DNS_RCODE_INVALID,
+ .ede_rcode = _DNS_EDE_RCODE_INVALID,
+ };
+ if (reply) {
+ r = dispatch_resolve_error(/* name = */ NULL, reply, SD_JSON_LOG, &error);
+ if (r < 0)
+ log_debug_errno(r, "Failed to dispatch error JSON, ignoring: %m");
+ }
+
+ if (error.rcode == DNS_RCODE_NXDOMAIN && !warn_missing)
+ return -ENXIO;
+
+ if (sd_json_format_enabled(arg_json_format_flags))
+ return dump_resolve_error_json(
+ name,
+ error_id,
+ reply,
+ error.rcode == DNS_RCODE_NXDOMAIN ? -ENXIO : ret);
+
static const struct {
const char *error_id;
const char *msg;
@@ -318,14 +363,6 @@ static int varlink_log_resolve_error(const char *name, const char *error_id, sd_
if (streq(error_id, "io.systemd.Resolve.InconsistentServiceRecords"))
return log_error_errno(ret, "%s: resolve call failed: '%s' does not provide a consistent set of service resource records", name, name);
- _cleanup_(resolve_error_done) ResolveError error = {
- .rcode = _DNS_RCODE_INVALID,
- .ede_rcode = _DNS_EDE_RCODE_INVALID,
- };
- r = dispatch_resolve_error(/* name = */ NULL, reply, SD_JSON_LOG, &error);
- if (r < 0)
- log_debug_errno(r, "Failed to dispatch error JSON, ignoring: %m");
-
_cleanup_free_ char *msg_extended = NULL;
if (error.ede_rcode >= 0) {
msg_extended = strjoin(" (",
@@ -342,9 +379,6 @@ static int varlink_log_resolve_error(const char *name, const char *error_id, sd_
if (error.rcode != _DNS_RCODE_INVALID) {
if (error.rcode == DNS_RCODE_NXDOMAIN) {
- if (!warn_missing)
- return -ENXIO;
-
return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "%s: resolve call failed: Name '%s' not found%s%s",
name, error.query_string ?: name, error.ede_rcode >= 0 ? ":" : "", strempty(msg_extended));
}
diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh
index 41436aac38..ce7675e8ce 100755
--- a/test/units/TEST-75-RESOLVED.sh
+++ b/test/units/TEST-75-RESOLVED.sh
@@ -768,10 +768,14 @@ testcase_08_resolved() {
(! run resolvectl query nope.forwarded.test)
grep -qF "nope.forwarded.test" "$RUN_OUT"
grep -qF "not found" "$RUN_OUT"
+ (! run resolvectl --json=short query -t A nope.forwarded.test)
+ jq -e '.name == "nope.forwarded.test" and .error == "io.systemd.Resolve.DNSError" and .rcode == 3 and .queryString == "nope.forwarded.test"' "$RUN_OUT"
# SERVFAIL + EDE code 6: DNSSEC Bogus
(! run resolvectl query edns-bogus-dnssec.forwarded.test)
grep -qE "^edns-bogus-dnssec.forwarded.test:.+: upstream-failure \(DNSSEC Bogus\)" "$RUN_OUT"
+ (! run resolvectl --json=short query -t A edns-bogus-dnssec.forwarded.test)
+ jq -e '.name == "edns-bogus-dnssec.forwarded.test" and .error == "io.systemd.Resolve.DNSSECValidationFailed" and .result == "upstream-failure" and .extendedDNSErrorCode == 6' "$RUN_OUT"
# Same thing, but over Varlink
(! run varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveHostname '{"name" : "edns-bogus-dnssec.forwarded.test"}')
grep -qF "io.systemd.Resolve.DNSSECValidationFailed" "$RUN_OUT"
@@ -782,6 +786,8 @@ testcase_08_resolved() {
# SERVFAIL + EDE code 16: Censored + extra text
(! run resolvectl query edns-extra-text.forwarded.test)
grep -qE "^edns-extra-text.forwarded.test.+: SERVFAIL \(Censored: Nothing to see here!\)" "$RUN_OUT"
+ (! run resolvectl --json=short query -t A edns-extra-text.forwarded.test)
+ jq -e '.name == "edns-extra-text.forwarded.test" and .error == "io.systemd.Resolve.DNSError" and .rcode == 2 and .extendedDNSErrorCode == 16 and .extendedDNSErrorMessage == "Nothing to see here!" and .queryString == "edns-extra-text.forwarded.test"' "$RUN_OUT"
(! run varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveHostname '{"name" : "edns-extra-text.forwarded.test"}')
grep -qF "io.systemd.Resolve.DNSError" "$RUN_OUT"
grep -qF '{"rcode":2,"extendedDNSErrorCode":16,"extendedDNSErrorMessage":"Nothing to see here!","queryString":"edns-extra-text.forwarded.test"}' "$RUN_OUT"