systemd-resolved handle ESERVFAIL/EREFUSED on single label names (#863)
systemd-resolved will return `ESERVFAIL` or `EREFUSED` by default on
single label domain names. See
https://github.com/systemd/systemd/issues/34101
They've basically labeled this as a non-issue even though it appears to
be in violation of the RFCs for downstream systems being able to support
negative caching. I haven't tested with the suggested
`ResolveUnicastSingleLabel=yes`, but that's off by default so its
unlikely people even knows this exists.
Since systemd is very prevalent these days, we really don't have much of
a choice but to work around their design decisions. This PR implements
this support, but *only* on single label names as it likely isn't valid
otherwise. It also adds test cases to confirm this behavior.
Fixes #852
Authored-By: Brad House (@bradh352)
diff --git a/src/lib/ares__close_sockets.c b/src/lib/ares__close_sockets.c
index 27bbaac..71c7e64 100644
--- a/src/lib/ares__close_sockets.c
+++ b/src/lib/ares__close_sockets.c
@@ -37,7 +37,7 @@
ares__tvnow(&now);
while ((query = ares__llist_first_val(conn->queries_to_conn)) != NULL) {
- ares__requeue_query(query, &now, requeue_status, ARES_TRUE);
+ ares__requeue_query(query, &now, requeue_status, ARES_TRUE, NULL);
}
}
diff --git a/src/lib/ares_cookie.c b/src/lib/ares_cookie.c
index 0680fe9..bf9d1ba 100644
--- a/src/lib/ares_cookie.c
+++ b/src/lib/ares_cookie.c
@@ -427,7 +427,8 @@
/* Resend the request, hopefully it will work the next time as we should
* have recorded a server cookie */
ares__requeue_query(query, now, ARES_SUCCESS,
- ARES_FALSE /* Don't increment try count */);
+ ARES_FALSE /* Don't increment try count */,
+ NULL);
/* Parent needs to drop this response */
return ARES_EBADRESP;
diff --git a/src/lib/ares_getaddrinfo.c b/src/lib/ares_getaddrinfo.c
index f67acde..713acf7 100644
--- a/src/lib/ares_getaddrinfo.c
+++ b/src/lib/ares_getaddrinfo.c
@@ -528,6 +528,13 @@
hquery->nodata_cnt++;
}
next_lookup(hquery, hquery->nodata_cnt ? ARES_ENODATA : status);
+ } else if (
+ (status == ARES_ESERVFAIL || status == ARES_EREFUSED) &&
+ ares__name_label_cnt(hquery->names[hquery->next_name_idx-1]) == 1
+ ) {
+ /* Issue #852, systemd-resolved may return SERVFAIL or REFUSED on a
+ * single label domain name. */
+ next_lookup(hquery, hquery->nodata_cnt ? ARES_ENODATA : status);
} else {
end_hquery(hquery, status);
}
diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
index b85ecb5..263c2a6 100644
--- a/src/lib/ares_private.h
+++ b/src/lib/ares_private.h
@@ -462,10 +462,14 @@
/* Returns one of the normal ares status codes like ARES_SUCCESS */
ares_status_t ares__send_query(ares_query_t *query, const ares_timeval_t *now);
-ares_status_t ares__requeue_query(ares_query_t *query,
- const ares_timeval_t *now,
- ares_status_t status,
- ares_bool_t inc_try_count);
+ares_status_t ares__requeue_query(ares_query_t *query,
+ const ares_timeval_t *now,
+ ares_status_t status,
+ ares_bool_t inc_try_count,
+ const ares_dns_record_t *dnsrec);
+
+/*! Count the number of labels (dots+1) in a domain */
+size_t ares__name_label_cnt(const char *name);
/*! Retrieve a list of names to use for searching. The first successful
* query in the list wins. This function also uses the HOSTSALIASES file
diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
index 65ee673..f05f67d 100644
--- a/src/lib/ares_process.c
+++ b/src/lib/ares_process.c
@@ -571,7 +571,7 @@
conn = query->conn;
server_increment_failures(conn->server, query->using_tcp);
- ares__requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE);
+ ares__requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL);
}
}
@@ -711,7 +711,7 @@
}
server_increment_failures(server, query->using_tcp);
- ares__requeue_query(query, now, status, ARES_TRUE);
+ ares__requeue_query(query, now, status, ARES_TRUE, rdnsrec);
/* Should any of these cause a connection termination?
* Maybe SERVER_FAILURE? */
@@ -756,10 +756,11 @@
ares__close_connection(conn, failure_status);
}
-ares_status_t ares__requeue_query(ares_query_t *query,
- const ares_timeval_t *now,
- ares_status_t status,
- ares_bool_t inc_try_count)
+ares_status_t ares__requeue_query(ares_query_t *query,
+ const ares_timeval_t *now,
+ ares_status_t status,
+ ares_bool_t inc_try_count,
+ const ares_dns_record_t *dnsrec)
{
ares_channel_t *channel = query->channel;
size_t max_tries = ares__slist_len(channel->servers) * channel->tries;
@@ -783,7 +784,7 @@
query->error_status = ARES_ETIMEOUT;
}
- end_query(channel, NULL, query, query->error_status, NULL);
+ end_query(channel, NULL, query, query->error_status, dnsrec);
return ARES_ETIMEOUT;
}
@@ -1078,7 +1079,7 @@
case ARES_ECONNREFUSED:
case ARES_EBADFAMILY:
server_increment_failures(server, query->using_tcp);
- return ares__requeue_query(query, now, status, ARES_TRUE);
+ return ares__requeue_query(query, now, status, ARES_TRUE, NULL);
/* Anything else is not retryable, likely ENOMEM */
default:
@@ -1104,7 +1105,7 @@
case ARES_ECONNREFUSED:
case ARES_EBADFAMILY:
handle_conn_error(conn, ARES_TRUE, status);
- status = ares__requeue_query(query, now, status, ARES_TRUE);
+ status = ares__requeue_query(query, now, status, ARES_TRUE, NULL);
if (status == ARES_ETIMEOUT) {
status = ARES_ECONNREFUSED;
}
@@ -1114,7 +1115,7 @@
* just requeue to a different server/connection. */
default:
server_increment_failures(server, query->using_tcp);
- status = ares__requeue_query(query, now, status, ARES_TRUE);
+ status = ares__requeue_query(query, now, status, ARES_TRUE, NULL);
return status;
}
diff --git a/src/lib/ares_search.c b/src/lib/ares_search.c
index ae98df3..2d3c0fc 100644
--- a/src/lib/ares_search.c
+++ b/src/lib/ares_search.c
@@ -107,26 +107,37 @@
{
struct search_query *squery = (struct search_query *)arg;
ares_channel_t *channel = squery->channel;
- ares_dns_rcode_t rcode;
- size_t ancount;
+
ares_status_t mystatus;
ares_bool_t skip_cleanup = ARES_FALSE;
squery->timeouts += timeouts;
- if (status != ARES_SUCCESS) {
- end_squery(squery, status, dnsrec);
- return;
+ if (dnsrec) {
+ ares_dns_rcode_t rcode = ares_dns_record_get_rcode(dnsrec);
+ size_t ancount = ares_dns_record_rr_cnt(dnsrec,
+ ARES_SECTION_ANSWER);
+ mystatus = ares_dns_query_reply_tostatus(rcode, ancount);
+ } else {
+ mystatus = status;
}
- rcode = ares_dns_record_get_rcode(dnsrec);
- ancount = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER);
- mystatus = ares_dns_query_reply_tostatus(rcode, ancount);
-
- if (mystatus != ARES_ENODATA && mystatus != ARES_ESERVFAIL &&
- mystatus != ARES_ENOTFOUND) {
- end_squery(squery, mystatus, dnsrec);
- return;
+ switch (mystatus) {
+ case ARES_ENODATA:
+ case ARES_ENOTFOUND:
+ break;
+ case ARES_ESERVFAIL:
+ case ARES_EREFUSED:
+ /* Issue #852, systemd-resolved may return SERVFAIL or REFUSED on a
+ * single label domain name. */
+ if (ares__name_label_cnt(squery->names[squery->next_name_idx-1]) != 1) {
+ end_squery(squery, mystatus, dnsrec);
+ return;
+ }
+ break;
+ default:
+ end_squery(squery, mystatus, dnsrec);
+ return;
}
/* If we ever get ARES_ENODATA along the way, record that; if the search
@@ -147,7 +158,6 @@
return;
}
-
/* We have no more domains to search, return an appropriate response. */
if (mystatus == ARES_ENOTFOUND && squery->ever_got_nodata) {
end_squery(squery, ARES_ENODATA, NULL);
@@ -176,6 +186,25 @@
return ARES_TRUE;
}
+size_t ares__name_label_cnt(const char *name)
+{
+ const char *p;
+ size_t ndots = 0;
+
+ if (name == NULL) {
+ return 0;
+ }
+
+ for (p = name; p != NULL && *p != 0; p++) {
+ if (*p == '.') {
+ ndots++;
+ }
+ }
+
+ /* Label count is 1 greater than ndots */
+ return ndots+1;
+}
+
ares_status_t ares__search_name_list(const ares_channel_t *channel,
const char *name, char ***names,
size_t *names_len)
@@ -186,7 +215,6 @@
char *alias = NULL;
size_t ndots = 0;
size_t idx = 0;
- const char *p;
size_t i;
/* Perform HOSTALIASES resolution */
@@ -223,12 +251,10 @@
goto done;
}
- /* Count the number of dots in name */
- ndots = 0;
- for (p = name; *p != 0; p++) {
- if (*p == '.') {
- ndots++;
- }
+ /* Count the number of dots in name, 1 less than label count */
+ ndots = ares__name_label_cnt(name);
+ if (ndots > 0) {
+ ndots--;
}
/* Allocate an entry for each search domain, plus one for as-is */
diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc
index 1e3e735..b4a4f99 100644
--- a/test/ares-test-mock-ai.cc
+++ b/test/ares-test-mock-ai.cc
@@ -404,6 +404,123 @@
EXPECT_EQ("{addr=[1.2.3.4]}", ss.str());
}
+
+// Issue #852, systemd-resolved returns SERVFAIL (and possibly REFUSED) on
+// single label domains. We need to work around this by continuing to go
+// to the next in the search list. See also
+// https://github.com/systemd/systemd/issues/34101
+TEST_P(MockExtraOptsNDots0TestAI, SystemdServFail) {
+ DNSPacket rsp_ndots0;
+ rsp_ndots0.set_response().set_rcode(SERVFAIL)
+ .add_question(new DNSQuestion("ndots0", T_A));
+ EXPECT_CALL(server_, OnRequest("ndots0", T_A))
+ // Will call until it hits max retries
+ .WillRepeatedly(SetReply(&server_, &rsp_ndots0));
+
+ DNSPacket rsp_ndots0_first;
+ rsp_ndots0_first.set_response().set_aa()
+ .add_question(new DNSQuestion("ndots0.first.com", T_A))
+ .add_answer(new DNSARR("ndots0.first.com", 100, {1, 2, 3, 4}));
+ EXPECT_CALL(server_, OnRequest("ndots0.first.com", T_A))
+ .WillOnce(SetReply(&server_, &rsp_ndots0_first));
+
+ AddrInfoResult result;
+ struct ares_addrinfo_hints hints = {0, 0, 0, 0};
+ hints.ai_family = AF_INET;
+ hints.ai_flags = ARES_AI_NOSORT;
+ ares_getaddrinfo(channel_, "ndots0", NULL, &hints, AddrInfoCallback, &result);
+ Process();
+ EXPECT_TRUE(result.done_);
+ EXPECT_EQ(ARES_SUCCESS, result.status_);
+ std::stringstream ss;
+ ss << result.ai_;
+ EXPECT_EQ("{addr=[1.2.3.4]}", ss.str());
+}
+TEST_P(MockExtraOptsNDots0TestAI, SystemdServFailSearch) {
+ DNSPacket rsp_ndots0;
+ rsp_ndots0.set_response().set_rcode(SERVFAIL)
+ .add_question(new DNSQuestion("ndots0", T_A));
+ EXPECT_CALL(server_, OnRequest("ndots0", T_A))
+ // Will call until it hits max retries
+ .WillRepeatedly(SetReply(&server_, &rsp_ndots0));
+
+ DNSPacket rsp_ndots0_first;
+ rsp_ndots0_first.set_response().set_aa()
+ .add_question(new DNSQuestion("ndots0.first.com", T_A))
+ .add_answer(new DNSARR("ndots0.first.com", 100, {1, 2, 3, 4}));
+ EXPECT_CALL(server_, OnRequest("ndots0.first.com", T_A))
+ .WillOnce(SetReply(&server_, &rsp_ndots0_first));
+
+ QueryResult result;
+ ares_dns_record_t *dnsrec = NULL;
+ ares_dns_record_create(&dnsrec, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, ARES_RCODE_NOERROR);
+ ares_dns_record_query_add(dnsrec, "ndots0", ARES_REC_TYPE_A, ARES_CLASS_IN);
+ ares_search_dnsrec(channel_, dnsrec, QueryCallback, &result);
+ ares_dns_record_destroy(dnsrec);
+ Process();
+ EXPECT_TRUE(result.done_);
+ EXPECT_EQ(ARES_SUCCESS, result.status_);
+
+ // QueryResult doesn't provide an easy way to retrieve the address, just ignore,
+ // success is probably good enough
+}
+TEST_P(MockExtraOptsNDots0TestAI, SystemdRefused) {
+ DNSPacket rsp_ndots0;
+ rsp_ndots0.set_response().set_rcode(REFUSED)
+ .add_question(new DNSQuestion("ndots0", T_A));
+ EXPECT_CALL(server_, OnRequest("ndots0", T_A))
+ // Will call until it hits max retries
+ .WillRepeatedly(SetReply(&server_, &rsp_ndots0));
+
+ DNSPacket rsp_ndots0_first;
+ rsp_ndots0_first.set_response().set_aa()
+ .add_question(new DNSQuestion("ndots0.first.com", T_A))
+ .add_answer(new DNSARR("ndots0.first.com", 100, {1, 2, 3, 4}));
+ EXPECT_CALL(server_, OnRequest("ndots0.first.com", T_A))
+ .WillOnce(SetReply(&server_, &rsp_ndots0_first));
+
+ AddrInfoResult result;
+ struct ares_addrinfo_hints hints = {0, 0, 0, 0};
+ hints.ai_family = AF_INET;
+ hints.ai_flags = ARES_AI_NOSORT;
+ ares_getaddrinfo(channel_, "ndots0", NULL, &hints, AddrInfoCallback, &result);
+ Process();
+ EXPECT_TRUE(result.done_);
+ EXPECT_EQ(ARES_SUCCESS, result.status_);
+ std::stringstream ss;
+ ss << result.ai_;
+ EXPECT_EQ("{addr=[1.2.3.4]}", ss.str());
+}
+TEST_P(MockExtraOptsNDots0TestAI, SystemdRefusedSearch) {
+ DNSPacket rsp_ndots0;
+ rsp_ndots0.set_response().set_rcode(REFUSED)
+ .add_question(new DNSQuestion("ndots0", T_A));
+ EXPECT_CALL(server_, OnRequest("ndots0", T_A))
+ // Will call until it hits max retries
+ .WillRepeatedly(SetReply(&server_, &rsp_ndots0));
+
+ DNSPacket rsp_ndots0_first;
+ rsp_ndots0_first.set_response().set_aa()
+ .add_question(new DNSQuestion("ndots0.first.com", T_A))
+ .add_answer(new DNSARR("ndots0.first.com", 100, {1, 2, 3, 4}));
+ EXPECT_CALL(server_, OnRequest("ndots0.first.com", T_A))
+ .WillOnce(SetReply(&server_, &rsp_ndots0_first));
+
+ QueryResult result;
+ ares_dns_record_t *dnsrec = NULL;
+ ares_dns_record_create(&dnsrec, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, ARES_RCODE_NOERROR);
+ ares_dns_record_query_add(dnsrec, "ndots0", ARES_REC_TYPE_A, ARES_CLASS_IN);
+ ares_search_dnsrec(channel_, dnsrec, QueryCallback, &result);
+ ares_dns_record_destroy(dnsrec);
+ Process();
+ EXPECT_TRUE(result.done_);
+ EXPECT_EQ(ARES_SUCCESS, result.status_);
+
+ // QueryResult doesn't provide an easy way to retrieve the address, just ignore,
+ // success is probably good enough
+}
+
+
class MockFlagsChannelOptsTestAI
: public MockChannelOptsTest,
public ::testing::WithParamInterface< std::pair<int, bool> > {