From e021a64391c29d28b06bd2090ef6e6758db51a2b Mon Sep 17 00:00:00 2001
From: dongshengyuan <545258830@qq.com>
Date: Thu, 13 Aug 2026 10:07:52 +0800
Subject: [PATCH 1/3] resolvectl: avoid TXT lookup for plain SRV service
queries
The service verb documents the one- and two-argument forms as
SRV lookups. They should not request TXT records by default.
Only the three-argument DNS-SD form should request TXT metadata.
Reproducer:
Run resolvectl service _mysvc._tcp signed.test in the
TEST-75-RESOLVED environment, where signed.test contains:
_mysvc._tcp SRV 10 5 1234 myservice
_mysvc._tcp TXT "This is TXT for myservice"
myservice A 10.0.0.20
Before:
resolvectl service _mysvc._tcp signed.test printed TXT data
for the plain SRV form:
_mysvc._tcp/signed.test: myservice.signed.test:1234 [priority=10, weight=5]
10.0.0.20
fd00:dead:beef:cafe::17
This is TXT for myservice
After:
The plain SRV form suppresses TXT data. The DNS-SD form still
shows it:
resolvectl service '' _mysvc._tcp signed.test
Follow-up for: a7a4c60a253a181385764bcb542906a312b68fe9
---
src/resolve/resolvectl.c | 22 +++++++++++++++-------
test/units/TEST-75-RESOLVED.sh | 9 ++++++---
2 files changed, 21 insertions(+), 10 deletions(-)
diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c
index 9af5626059..d77ae3d631 100644
--- a/src/resolve/resolvectl.c
+++ b/src/resolve/resolvectl.c
@@ -71,6 +71,7 @@ static sd_json_format_flags_t arg_json_format_flags = SD_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;
+static bool arg_service_txt_set = false;
static bool arg_ask_password = true;
typedef enum RawType {
@@ -904,7 +905,7 @@ static int verb_query(int argc, char *argv[], uintptr_t _data, void *userdata) {
return ret;
}
-static int resolve_service(const char *name, const char *type, const char *domain) {
+static int resolve_service(const char *name, const char *type, const char *domain, uint64_t flags) {
int r;
assert(domain);
@@ -938,7 +939,7 @@ static int resolve_service(const char *name, const char *type, const char *domai
JSON_BUILD_PAIR_STRING_NON_EMPTY("type", type),
JSON_BUILD_PAIR_CONDITION_UNSIGNED(arg_ifindex > 0, "ifindex", arg_ifindex),
JSON_BUILD_PAIR_CONDITION_UNSIGNED(arg_family != AF_UNSPEC, "family", arg_family),
- JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("flags", arg_flags));
+ JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("flags", flags));
if (r < 0)
return log_error_errno(r, "Failed to issue varlink call: %m");
@@ -1019,15 +1020,20 @@ static int resolve_service(const char *name, const char *type, const char *domai
VERB(verb_service, "service", "[[NAME] TYPE] DOMAIN", 2, 4, 0,
"Resolve service (SRV)");
static int verb_service(int argc, char *argv[], uintptr_t _data, void *userdata) {
+ uint64_t flags = arg_flags;
+
if (sd_json_format_enabled(arg_json_format_flags))
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Use --json=pretty with --type= to acquire resource record information in JSON format.");
+ if (argc < 4 && !arg_service_txt_set)
+ flags |= SD_RESOLVED_NO_TXT;
+
if (argc == 2)
- return resolve_service(NULL, NULL, argv[1]);
- else if (argc == 3)
- return resolve_service(NULL, argv[1], argv[2]);
- else
- return resolve_service(argv[1], argv[2], argv[3]);
+ return resolve_service(NULL, NULL, argv[1], flags);
+ if (argc == 3)
+ return resolve_service(NULL, argv[1], argv[2], flags);
+
+ return resolve_service(argv[1], argv[2], argv[3], flags);
}
#if HAVE_OPENSSL
@@ -3360,6 +3366,7 @@ static int compat_parse_argv(int argc, char *argv[], char ***remaining_args) {
if (r < 0)
return r;
SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0);
+ arg_service_txt_set = true;
break;
OPTION_LONG("openpgp", NULL, "Query OpenPGP public key"):
@@ -3594,6 +3601,7 @@ static int native_parse_argv(int argc, char *argv[], char ***remaining_args) {
if (r < 0)
return r;
SET_FLAG(arg_flags, SD_RESOLVED_NO_TXT, r == 0);
+ arg_service_txt_set = true;
break;
OPTION_LONG("cname", "BOOL", "Follow CNAME redirects (default: yes)"):
diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh
index 85e9e4cc84..9f18f3dcf3 100755
--- a/test/units/TEST-75-RESOLVED.sh
+++ b/test/units/TEST-75-RESOLVED.sh
@@ -579,10 +579,13 @@ testcase_08_resolved() {
# Check SRV support
run resolvectl service _mysvc._tcp signed.test
grep -qF "myservice.signed.test:1234" "$RUN_OUT"
- grep -qF "This is TXT for myservice" "$RUN_OUT"
+ (! grep -qF "This is TXT for myservice" "$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"
+ run resolvectl service "" _mysvc._tcp signed.test
+ grep -qF "myservice.signed.test:1234" "$RUN_OUT"
+ grep -qF "This is TXT for myservice" "$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"}'
@@ -1338,7 +1341,7 @@ testcase_14_refuse_record_types() {
run resolvectl service _mysvc._tcp signed.test
grep -qF "myservice.signed.test:1234" "$RUN_OUT"
- grep -qF "This is TXT for myservice" "$RUN_OUT"
+ (! grep -qF "This is TXT for myservice" "$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"
@@ -1360,7 +1363,7 @@ testcase_14_refuse_record_types() {
run resolvectl service _mysvc._tcp signed.test
grep -qF "myservice.signed.test:1234" "$RUN_OUT"
- grep -qF "This is TXT for myservice" "$RUN_OUT"
+ (! grep -qF "This is TXT for myservice" "$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"
From e7223f2ab2cfa38aaa5449690f80acdad005619b Mon Sep 17 00:00:00 2001
From: dongshengyuan <545258830@qq.com>
Date: Thu, 13 Aug 2026 10:13:53 +0800
Subject: [PATCH 2/3] resolvectl: reject --raw for regular lookups
--raw only affects ResolveRecord output. Without --type= or an
RFC4501 dns: URI, query takes the ResolveHostname/ResolveAddress
paths, where --raw was silently ignored.
Reproducer:
build/resolvectl --raw query localhost
Before:
The command exited successfully and printed regular text output,
such as:
localhost: 127.0.0.1
After:
The command fails before issuing a lookup:
--raw may only be combined with --type= or dns: URIs.
Follow-up for: 05ea7e87b616650b5829bd439deff6bb0bc9e534
---
man/resolvectl.xml | 4 ++++
src/resolve/resolvectl.c | 4 ++++
test/units/TEST-75-RESOLVED.sh | 4 ++++
3 files changed, 12 insertions(+)
diff --git a/man/resolvectl.xml b/man/resolvectl.xml
index b7a28b47bb..e908345d48 100644
--- a/man/resolvectl.xml
+++ b/man/resolvectl.xml
@@ -482,6 +482,10 @@
types: SSHFP, TLSA, OPENPGPKEY where this dumps the key material only; and A, AAAA where
this dumps the address data.
+ This option may only be used with or an RFC 4501
+ dns: URI, which both perform DNS resource record lookups. It may not be combined
+ with regular hostname or address lookups.
+
diff --git a/src/resolve/resolvectl.c b/src/resolve/resolvectl.c
index d77ae3d631..be596841a6 100644
--- a/src/resolve/resolvectl.c
+++ b/src/resolve/resolvectl.c
@@ -894,6 +894,10 @@ static int verb_query(int argc, char *argv[], uintptr_t _data, void *userdata) {
int family, ifindex;
union in_addr_union a;
+ if (arg_raw != RAW_NONE)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "--raw may only be combined with --type= or dns: URIs.");
+
r = in_addr_ifindex_from_string_auto(*p, &family, &a, &ifindex);
if (r >= 0)
RET_GATHER(ret, resolve_address(family, &a, ifindex));
diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh
index 9f18f3dcf3..707a6f3011 100755
--- a/test/units/TEST-75-RESOLVED.sh
+++ b/test/units/TEST-75-RESOLVED.sh
@@ -565,6 +565,10 @@ testcase_08_resolved() {
run resolvectl query signed.test
grep -qF "signed.test: 10.0.0.10" "$RUN_OUT"
grep -qF "authenticated: yes" "$RUN_OUT"
+ (! run resolvectl --raw query localhost)
+ grep -qF -- "--raw may only be combined with --type= or dns: URIs." "$RUN_OUT"
+ resolvectl --raw=packet query --type=A signed.test >"$RUN_OUT"
+ test -s "$RUN_OUT"
run dig @ns1.unsigned.test +short MX signed.test
grep -qF "10 mail.signed.test." "$RUN_OUT"
run resolvectl query --legend=no -t MX signed.test
From 253b34d8ac08e9443d5ab19c5528ee85118ddd5a Mon Sep 17 00:00:00 2001
From: dongshengyuan <545258830@qq.com>
Date: Thu, 13 Aug 2026 10:54:02 +0800
Subject: [PATCH 3/3] resolvconf: reject malformed stdin without changing link
state
The resolvconf compatibility parser ignored errors from
nameserver, domain, and search lines. If one of those lines was
malformed, resolvectl could still send the partially parsed
configuration to resolved and overwrite the previous link state.
Reproducer:
IF=enp1s0
TMP="$(mktemp -d)"
ln -s "$(command -v resolvectl)" "$TMP/resolvconf"
sudo resolvectl dns "$IF" 1.1.1.1
sudo resolvectl domain "$IF" keep.example
resolvectl domain "$IF"
printf 'nameserver 1.1.1.1\nsearch "unterminated\n' |
sudo "$TMP/resolvconf" -a "$IF" || true
resolvectl domain "$IF"
Before:
The malformed search line was ignored and the previous domain
was cleared:
Link 2 (enp1s0): keep.example
Link 2 (enp1s0):
After:
The malformed input fails before updating resolved, so
keep.example remains configured.
Follow-up for: 097f130731513bd934ed74975ac34070197614af
---
NEWS | 3 +++
man/resolvectl.xml | 3 ++-
src/resolve/resolvconf-compat.c | 8 ++++++--
test/units/TEST-75-RESOLVED.sh | 3 +++
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/NEWS b/NEWS
index ccadaa256d..c5686b929f 100644
--- a/NEWS
+++ b/NEWS
@@ -454,6 +454,9 @@ CHANGES WITH 261:
Changes in systemd-resolved:
+ * The resolvconf compatibility mode now rejects malformed stdin input
+ and leaves the existing per-link DNS configuration unchanged.
+
* systemd-resolved will now read additional DNS resource record
definitions to resolve locally from JSON drop-in files in
{/etc,/run,/usr/local/lib,/usr/lib}/systemd/resolve/static.d/. This
diff --git a/man/resolvectl.xml b/man/resolvectl.xml
index e908345d48..fabd64e81e 100644
--- a/man/resolvectl.xml
+++ b/man/resolvectl.xml
@@ -564,7 +564,8 @@
DNS configuration data from its standard input. Relevant fields are nameserver and
domain/search. This command is mostly identical to invoking
resolvectl with a combination of and
- commands.
+ commands. If a relevant input line has invalid syntax, the command fails and leaves the existing
+ per-interface configuration unchanged.
diff --git a/src/resolve/resolvconf-compat.c b/src/resolve/resolvconf-compat.c
index d3ddec755a..dcac5c130e 100644
--- a/src/resolve/resolvconf-compat.c
+++ b/src/resolve/resolvconf-compat.c
@@ -116,7 +116,9 @@ static int parse_stdin(LookupType lookup_type) {
a = first_word(line, "nameserver");
if (a) {
- (void) parse_nameserver(a);
+ r = parse_nameserver(a);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse resolv.conf nameserver line: %m");
continue;
}
@@ -124,7 +126,9 @@ static int parse_stdin(LookupType lookup_type) {
if (!a)
a = first_word(line, "search");
if (a) {
- (void) parse_search_domain(a);
+ r = parse_search_domain(a);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse resolv.conf domain/search line: %m");
continue;
}
diff --git a/test/units/TEST-75-RESOLVED.sh b/test/units/TEST-75-RESOLVED.sh
index 707a6f3011..e3055616cb 100755
--- a/test/units/TEST-75-RESOLVED.sh
+++ b/test/units/TEST-75-RESOLVED.sh
@@ -273,6 +273,9 @@ manual_testcase_01_resolvectl() {
assert_in 'test-domain1.example.com' "$(resolvectl domain hoge)"
assert_in 'test-domain2.example.com' "$(resolvectl domain hoge)"
assert_in 'test-search-domain.example.com' "$(resolvectl domain hoge)"
+ resolvectl domain hoge keep.example
+ (! echo -e "nameserver 10.0.2.1\nsearch \"unterminated" | SYSTEMD_INVOKED_AS=resolvconf resolvectl -a hoge)
+ assert_in 'keep.example' "$(resolvectl domain hoge)"
# Tests for 'resolvconf -x'
echo nameserver 10.0.2.1 | SYSTEMD_INVOKED_AS=resolvconf resolvectl -x -a hoge