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"