Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions src/net/dns.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include <cstring>
#include <memory>
#include <concepts>
#include <net/if.h>
#include <netdb.h>
#include <poll.h>

Expand Down Expand Up @@ -141,7 +142,10 @@ class ares_error_category : public std::error_category {
/* More error codes */
case ARES_ECANCELLED: return "Cancelled";
default:
return "Unknown error";
// Codes this table predates (ARES_ESERVICE, ARES_ENOSERVER, ...)
// get c-ares' own text. The texts above stay as they are:
// applications and their tests match on them.
return ares_strerror(error);
}
}
};
Expand Down Expand Up @@ -438,8 +442,18 @@ dns_resolver::impl::impl(network_stack& stack, const options& opts)
},
.agetsockname = nullptr, // Not needed
.abind = nullptr, // Not needed
.aif_nametoindex = nullptr, // Not needed
.aif_indextoname = nullptr, // Not needed
// c-ares resolves "%iface" on link-local nameservers through these;
// with them null it silently drops such servers.
.aif_nametoindex = [](const char * ifname, void *) -> unsigned int {
return if_nametoindex(ifname);
},
.aif_indextoname = [](unsigned int ifindex, char * ifname_buf, size_t ifname_buf_len, void *) -> const char * {
char tmp[IF_NAMESIZE];
if (if_indextoname(ifindex, tmp) == nullptr || strlen(tmp) >= ifname_buf_len) {
return nullptr;
}
return strcpy(ifname_buf, tmp);
},
};

ares_status_t status = ares_set_socket_functions_ex(_channel, &callbacks_ex, this);
Expand Down Expand Up @@ -532,11 +546,14 @@ dns_resolver::impl::get_host_by_name(sstring name, opt_family family) {

dns_log.debug("Query name {} ({})", name, family);

if (!family) {
auto res = inet_address::parse_numerical(name);
if (res) {
return make_ready_future<hostent>(hostent({std::move(name)}, {{*res}}, {{*res}}));
// c-ares cannot parse a scope id ("fe80::1%eth0") and ignores the
// requested family for literals, so resolve them here. The family is a
// filter: a literal of the other family is an error, not a fallback.
if (auto res = inet_address::parse_numerical(name)) {
if (family && res->in_family() != *family) {
return make_exception_future<hostent>(std::system_error(ARES_EBADFAMILY, dns::error_category()));
}
return make_ready_future<hostent>(hostent({std::move(name)}, {{*res}}, {{*res}}));
}

auto p = new promise_wrap(std::move(name));
Expand Down
211 changes: 202 additions & 9 deletions tests/unit/dns_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
#include <seastar/net/dns.hh>
#include <seastar/net/inet_address.hh>

#include <net/if.h>

#include "ipv6_support.hh"

using namespace seastar;
Expand All @@ -58,7 +60,27 @@ static void write_be32(std::vector<char>& out, uint32_t v) {
out.push_back(char(v));
}

static std::vector<char> make_dns_a_response(const temporary_buffer<char>& query) {
// Encodes "a.b.c" as DNS labels.
static void write_name(std::vector<char>& out, std::string_view name) {
size_t start = 0;
while (start <= name.size()) {
auto dot = name.find('.', start);
auto label = name.substr(start, dot == std::string_view::npos ? std::string_view::npos : dot - start);
out.push_back(char(label.size()));
out.insert(out.end(), label.begin(), label.end());
if (dot == std::string_view::npos) {
break;
}
start = dot + 1;
}
out.push_back(0);
}

// Answers the single question in `query` with one record of the type it asked
// for: A -> 127.0.0.42, AAAA -> ::42, PTR -> "v6.seastar.test". Everything an
// IPv6 resolver does differently from an IPv4 one is in the answer type, so
// one builder driven by QTYPE covers all of them.
static std::vector<char> make_dns_response(const temporary_buffer<char>& query) {
BOOST_REQUIRE_GE(query.size(), 12);
BOOST_REQUIRE_EQUAL(read_be16(query.get() + 4), 1);

Expand All @@ -71,6 +93,7 @@ static std::vector<char> make_dns_a_response(const temporary_buffer<char>& query
BOOST_REQUIRE_LT(question_end, query.size());
question_end += 1 + sizeof(uint16_t) + sizeof(uint16_t);
BOOST_REQUIRE_LE(question_end, query.size());
const auto qtype = read_be16(query.get() + question_end - 4);

std::vector<char> msg;
write_be16(msg, read_be16(query.get()));
Expand All @@ -82,20 +105,35 @@ static std::vector<char> make_dns_a_response(const temporary_buffer<char>& query
msg.insert(msg.end(), query.get() + 12, query.get() + question_end);

write_be16(msg, 0xc00c);
write_be16(msg, 1);
write_be16(msg, qtype);
write_be16(msg, 1);
write_be32(msg, 60);
write_be16(msg, 4);
msg.push_back(char(127));
msg.push_back(char(0));
msg.push_back(char(0));
msg.push_back(char(42));
switch (qtype) {
case 1: // A
write_be16(msg, 4);
msg.insert(msg.end(), {char(127), char(0), char(0), char(42)});
break;
case 28: // AAAA
write_be16(msg, 16);
msg.insert(msg.end(), 15, char(0));
msg.push_back(char(0x42));
break;
case 12: { // PTR
std::vector<char> name;
write_name(name, "v6.seastar.test");
write_be16(msg, name.size());
msg.insert(msg.end(), name.begin(), name.end());
break;
}
default:
BOOST_FAIL("unexpected qtype " + std::to_string(qtype));
}

return msg;
}

static std::vector<char> make_tcp_dns_a_response(const temporary_buffer<char>& query) {
auto msg = make_dns_a_response(query);
auto msg = make_dns_response(query);
std::vector<char> tcp_response;
write_be16(tcp_response, msg.size());
tcp_response.insert(tcp_response.end(), msg.begin(), msg.end());
Expand All @@ -109,7 +147,7 @@ static future<> serve_udp_dns_response(net::datagram_channel& chan) {
BOOST_REQUIRE(!bufs.empty());
// A DNS query for a single name fits one buffer.
auto query = temporary_buffer<char>(bufs.front().get(), bufs.front().size());
auto response = make_dns_a_response(query);
auto response = make_dns_response(query);
auto out = temporary_buffer<char>(response.data(), response.size());
co_await chan.send(dg.get_src(), std::span<temporary_buffer<char>>(&out, 1));
}
Expand Down Expand Up @@ -390,6 +428,149 @@ SEASTAR_TEST_CASE(test_resolve_udp_ipv6_nameserver) {
}
}

static future<> serve_udp_dns_responses(net::datagram_channel& chan, size_t n_queries) {
for (size_t i = 0; i < n_queries; ++i) {
co_await serve_udp_dns_response(chan);
}
}

// Runs `query` against a mock nameserver bound to [::1] over UDP (the server
// answers `n_queries` questions) and returns the resolver's answer.
template <typename Query>
static future<hostent> ask_ipv6_udp_nameserver(size_t n_queries, Query query) {
auto chan = make_bound_datagram_channel(socket_address(ipv6_addr{"::1", 0}));
auto server = serve_udp_dns_responses(chan, n_queries);

dns_resolver::options opts;
opts.servers = std::vector<inet_address>({ inet_address("::1") });
opts.udp_port = chan.local_address().port();
opts.timeout = std::chrono::seconds(30);
auto d = ::make_lw_shared<dns_resolver>(engine().net(), opts);

std::exception_ptr ex;
std::optional<hostent> answer;
try {
answer = co_await with_timeout(timer<>::clock::now() + std::chrono::seconds(5), query(*d));
} catch (...) {
ex = std::current_exception();
}
co_await d->close();
chan.shutdown_input();
try {
co_await std::move(server);
} catch (...) {
if (!ex) {
ex = std::current_exception();
}
}
chan.close();
if (ex) {
std::rethrow_exception(ex);
}
co_return std::move(*answer);
}

// The answer side of IPv6: an AAAA record has to survive make_hostent.
SEASTAR_TEST_CASE(test_resolve_aaaa_from_ipv6_nameserver) {
if (!seastar::testing::ipv6_available_or_skip()) {
co_return;
}
auto h = co_await ask_ipv6_udp_nameserver(1, [](dns_resolver& d) {
return d.get_host_by_name("v6.seastar.test", inet_address::family::INET6);
});
BOOST_REQUIRE_EQUAL(h.addr_entries.size(), 1);
BOOST_REQUIRE_EQUAL(h.addr_entries.front().addr, inet_address("::42"));
BOOST_REQUIRE_EQUAL(h.addr_entries.front().ttl, std::chrono::seconds(60));
}

// Reverse lookup of an IPv6 address: 16-byte ip6.arpa PTR through get_host_by_addr.
// Not ::1, which /etc/hosts answers before c-ares ever asks the nameserver.
SEASTAR_TEST_CASE(test_reverse_lookup_ipv6_from_ipv6_nameserver) {
if (!seastar::testing::ipv6_available_or_skip()) {
co_return;
}
auto h = co_await ask_ipv6_udp_nameserver(1, [](dns_resolver& d) {
return d.get_host_by_addr(inet_address("2001:db8::42"));
});
BOOST_REQUIRE(!h.names.empty());
BOOST_REQUIRE_EQUAL(h.names.front(), "v6.seastar.test");
}

// TCP transport (use_tcp_query) to a nameserver that is only reachable over IPv6.
SEASTAR_TEST_CASE(test_resolve_tcp_ipv6_nameserver) {
if (!seastar::testing::ipv6_available_or_skip()) {
co_return;
}
listen_options lo;
lo.reuse_address = true;
lo.set_fixed_cpu(this_shard_id());

auto listener = seastar::listen(socket_address(ipv6_addr{"::1", 0}), lo);
auto server = serve_split_tcp_dns_response(listener);

dns_resolver::options opts;
opts.servers = std::vector<inet_address>({ inet_address("::1") });
opts.use_tcp_query = true;
opts.tcp_port = listener.local_address().port();
opts.timeout = std::chrono::seconds(30);
auto d = ::make_lw_shared<dns_resolver>(engine().net(), opts);

std::exception_ptr ex;
try {
auto h = co_await with_timeout(timer<>::clock::now() + std::chrono::seconds(5),
d->get_host_by_name("tcp6.seastar.test", inet_address::family::INET));
BOOST_REQUIRE_EQUAL(h.addr_entries.size(), 1);
BOOST_REQUIRE_EQUAL(h.addr_entries.front().addr, inet_address("127.0.0.42"));
} catch (...) {
ex = std::current_exception();
}
co_await d->close();
listener.abort_accept();
try {
co_await std::move(server);
} catch (...) {
if (!ex) {
ex = std::current_exception();
}
}
if (ex) {
std::rethrow_exception(ex);
}
}

// Numeric literals never reach c-ares, for either family, and the requested
// family is a filter rather than a hint.
SEASTAR_TEST_CASE(test_resolve_numeric_with_family) {
auto d = ::make_lw_shared<dns_resolver>(engine().net(), dns_resolver::options());

auto v6 = co_await d->get_host_by_name("::1", inet_address::family::INET6);
BOOST_REQUIRE_EQUAL(v6.addr_entries.size(), 1);
BOOST_REQUIRE_EQUAL(v6.addr_entries.front().addr, inet_address("::1"));

auto v4 = co_await d->get_host_by_name("127.0.0.1", inet_address::family::INET);
BOOST_REQUIRE_EQUAL(v4.addr_entries.front().addr, inet_address("127.0.0.1"));

for (auto [literal, family] : {std::pair{"::1", inet_address::family::INET}, std::pair{"127.0.0.1", inet_address::family::INET6}}) {
try {
co_await d->get_host_by_name(literal, family);
BOOST_FAIL("literal of the other family must not resolve");
} catch (const std::system_error& e) {
// c-ares is not part of seastar's public interface, so compare the
// rendered text instead of the ARES_EBADFAMILY constant.
BOOST_REQUIRE(e.code().category() == dns::error_category());
BOOST_REQUIRE_EQUAL(e.code().message(), dns::error_category().message(e.code().value()));
BOOST_REQUIRE_NE(e.code().message().find("family"), std::string::npos);
}
}

// A scoped link-local literal must keep its scope; c-ares cannot parse one.
if (auto lo_index = if_nametoindex("lo")) {
auto scoped = co_await d->get_host_by_name("fe80::1%lo", inet_address::family::INET6);
BOOST_REQUIRE_EQUAL(scoped.addr_entries.front().addr.scope(), lo_index);
}
co_await d->close();
}

SEASTAR_TEST_CASE(test_resolve_tcp,
*enable_if_with_networking()) {
dns_resolver::options opts;
Expand Down Expand Up @@ -462,3 +643,15 @@ SEASTAR_TEST_CASE(test_parallel_resolve_name_udp,
d->resolve_name("www.google.com")
).finally([d](auto&...) {}).discard_result();
}

// Applications match on these texts (a startup failure logs as "C-Ares:4,
// <host>: Not found"), so they stay put. The numbers are c-ares' stable
// error codes; c-ares itself is not part of seastar's interface.
SEASTAR_TEST_CASE(test_error_category_texts) {
const auto& cat = dns::error_category();
BOOST_REQUIRE_EQUAL(cat.message(4), "Not found"); // ARES_ENOTFOUND
BOOST_REQUIRE_EQUAL(cat.message(11), "Connection refused"); // ARES_ECONNREFUSED
// ARES_ENOSERVER, which the table predates, gets c-ares' own text.
BOOST_REQUIRE_NE(cat.message(26), "Unknown error");
return make_ready_future();
}
Loading