From f8bb6e10c45221c2565249615b838076173f159b Mon Sep 17 00:00:00 2001 From: David Yu Date: Sat, 12 Sep 2026 21:29:08 -0700 Subject: [PATCH] dns: reach nameservers over IPv6 The c-ares integration created every UDP socket as AF_INET and rejected any AF_INET6 sockaddr with "No ipv6 yet", so a host whose resolv.conf lists an IPv6 nameserver could not resolve anything: c-ares reported ARES_ECONNREFUSED for every server. That is what an IPv6-only Kubernetes pod looks like (CoreDNS over IPv6). - do_socket creates the datagram channel with the family c-ares asked for instead of hardcoding AF_INET. - sock_addr accepts AF_INET6 and checks the sockaddr length. - do_recvfrom copies the source address by its real length; sockaddr is only wide enough for AF_INET, so an IPv6 source was being truncated and its length misreported. - options.servers is installed with ares_set_servers_csv after channel creation; ARES_OPT_SERVERS carries in_addr only. - ARES_ECONNREFUSED is rendered with c-ares' own text, "Could not contact DNS servers", so it stops reading like a refused TCP connect. IPv4 nameservers take the same AF_INET branches as before. Adds a unit test that answers a query from a mock nameserver bound to [::1]. --- src/net/dns.cc | 70 ++++++++++++++++++++++++++++-------------- tests/unit/dns_test.cc | 65 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 24 deletions(-) diff --git a/src/net/dns.cc b/src/net/dns.cc index 1bd721baf14..da991fdfa4a 100644 --- a/src/net/dns.cc +++ b/src/net/dns.cc @@ -43,6 +43,7 @@ #include #include +#include #include #include @@ -115,7 +116,9 @@ class ares_error_category : public std::error_category { case ARES_EBADNAME: return "Bad name"; case ARES_EBADFAMILY: return "Bad family"; case ARES_EBADRESP: return "Bad response"; - case ARES_ECONNREFUSED :return "Connection refused"; + // c-ares raises this when no nameserver could be reached at + // all, not for a refused TCP connect; use its own wording. + case ARES_ECONNREFUSED: return "Could not contact DNS servers"; case ARES_ETIMEOUT: return "Timeout"; case ARES_EOF: return "EOF"; case ARES_EFILE: return "File error"; @@ -370,19 +373,9 @@ dns_resolver::impl::impl(network_stack& stack, const options& opts) a_opts.flags = ARES_FLAG_USEVC | ARES_FLAG_PRIMARY; flags |= ARES_OPT_FLAGS; } - std::vector addr_tmp; - if (opts.servers) { - std::transform(opts.servers->begin(), opts.servers->end(), std::back_inserter(addr_tmp), [](const inet_address& a) { - if (a.in_family() != inet_address::family::INET) { - throw std::invalid_argument("Servers must be ipv4 addresses"); - } - in_addr in = a; - return in; - }); - a_opts.servers = addr_tmp.data(); - a_opts.nservers = int(addr_tmp.size()); - flags |= ARES_OPT_SERVERS; - } + // opts.servers is installed after ares_init_options, below: + // ARES_OPT_SERVERS carries in_addr only, so it cannot express an IPv6 + // nameserver. std::vector dom_tmp; if (opts.domains) { std::transform(opts.domains->begin(), opts.domains->end(), std::back_inserter(dom_tmp), [](const sstring& s) { @@ -403,6 +396,19 @@ dns_resolver::impl::impl(network_stack& stack, const options& opts) check_ares_error(ares_init_options(&_channel, &a_opts, flags)); + if (opts.servers) { + // A server given without a port inherits the channel's udp/tcp + // defaults, so ARES_OPT_UDP_PORT/ARES_OPT_TCP_PORT above still apply. + sstring csv; + for (auto& server : *opts.servers) { + if (!csv.empty()) { + csv += ","; + } + csv += fmt::format("{}", server); + } + check_ares_error(ares_set_servers_csv(_channel, csv.c_str())); + } + // Set up custom socket functions to integrate with Seastar's networking stack // Note: These work together with ARES_OPT_SOCK_STATE_CB (when available) #if ARES_VERSION >= 0x012200 // ares_set_socket_functions_ex available since 1.34.0 @@ -1112,8 +1118,8 @@ dns_resolver::impl::do_socket(int af, int type, int protocol) { dns_log.trace("Created tcp socket {}", fd); break; case SOCK_DGRAM: - _sockets.emplace(fd, _stack.make_unbound_datagram_channel(AF_INET)); - dns_log.trace("Created udp socket {}", fd); + _sockets.emplace(fd, _stack.make_unbound_datagram_channel(af)); + dns_log.trace("Created udp socket {} (family {})", fd, af); break; default: return -1; } @@ -1168,11 +1174,21 @@ dns_resolver::impl::do_close(ares_socket_t fd) { socket_address dns_resolver::impl::sock_addr(const sockaddr * addr, socklen_t len) { - if (addr->sa_family != AF_INET) { - throw std::invalid_argument("No ipv6 yet"); + switch (addr->sa_family) { + case AF_INET: + if (len < static_cast(sizeof(sockaddr_in))) { + throw std::invalid_argument("Truncated AF_INET address"); + } + return *reinterpret_cast(addr); + case AF_INET6: + if (len < static_cast(sizeof(sockaddr_in6))) { + throw std::invalid_argument("Truncated AF_INET6 address"); + } + return *reinterpret_cast(addr); + default: + throw std::invalid_argument( + format("Unsupported address family {}", addr->sa_family)); } - auto in = reinterpret_cast(addr); - return *in; } int @@ -1303,10 +1319,18 @@ dns_resolver::impl::do_recvfrom(ares_socket_t fd, void * dst, size_t len, int fl dns_log.trace("Read {}. {} bytes available from {}", fd, available, udp.in->get_src()); if (from != nullptr) { - *from = socket_address(udp.in->get_src()).as_posix_sockaddr(); + // sockaddr is only wide enough for AF_INET, so an + // AF_INET6 source has to be copied by its real length. + // from_len is value-result: on entry it bounds the + // caller's buffer, on exit it reports the source size. + auto src = socket_address(udp.in->get_src()); + auto capacity = from_len != nullptr + ? *from_len + : static_cast(sizeof(sockaddr)); + std::memcpy(from, &src.as_posix_sockaddr(), + std::min(src.length(), capacity)); if (from_len != nullptr) { - // TODO: ipvv6 - *from_len = sizeof(sockaddr_in); + *from_len = src.length(); } } diff --git a/tests/unit/dns_test.cc b/tests/unit/dns_test.cc index 89d8c51036d..067317a57b7 100644 --- a/tests/unit/dns_test.cc +++ b/tests/unit/dns_test.cc @@ -21,6 +21,7 @@ */ #include #include +#include #include #include @@ -54,7 +55,7 @@ static void write_be32(std::vector& out, uint32_t v) { out.push_back(char(v)); } -static std::vector make_tcp_dns_a_response(const temporary_buffer& query) { +static std::vector make_dns_a_response(const temporary_buffer& query) { BOOST_REQUIRE_GE(query.size(), 12); BOOST_REQUIRE_EQUAL(read_be16(query.get() + 4), 1); @@ -87,12 +88,29 @@ static std::vector make_tcp_dns_a_response(const temporary_buffer& q msg.push_back(char(0)); msg.push_back(char(42)); + return msg; +} + +static std::vector make_tcp_dns_a_response(const temporary_buffer& query) { + auto msg = make_dns_a_response(query); std::vector tcp_response; write_be16(tcp_response, msg.size()); tcp_response.insert(tcp_response.end(), msg.begin(), msg.end()); return tcp_response; } +// Answers exactly one query, over whatever family the channel is bound to. +static future<> serve_udp_dns_response(net::datagram_channel& chan) { + auto dg = co_await chan.receive(); + auto bufs = dg.get_buffers(); + BOOST_REQUIRE(!bufs.empty()); + // A DNS query for a single name fits one buffer. + auto query = temporary_buffer(bufs.front().get(), bufs.front().size()); + auto response = make_dns_a_response(query); + auto out = temporary_buffer(response.data(), response.size()); + co_await chan.send(dg.get_src(), std::span>(&out, 1)); +} + static future<> serve_split_tcp_dns_response(server_socket& listener) { auto ar = co_await listener.accept(); auto socket = std::move(ar.connection); @@ -276,6 +294,51 @@ SEASTAR_TEST_CASE(test_resolve_tcp_split_response) { } } +// The resolver must be able to *talk to* an IPv6 nameserver. The record it +// asks for is still an A record: nameserver transport family and answer +// family are independent, and only the former was broken. +SEASTAR_TEST_CASE(test_resolve_udp_ipv6_nameserver) { + if (!engine().net().supports_ipv6()) { + BOOST_TEST_MESSAGE("No ipv6 support, skipping test"); + co_return; + } + + auto chan = make_bound_datagram_channel(socket_address(ipv6_addr{"::1", 0})); + auto server = serve_udp_dns_response(chan); + + dns_resolver::options opts; + opts.servers = std::vector({ inet_address("::1") }); + opts.udp_port = chan.local_address().port(); + opts.timeout = std::chrono::seconds(30); + + auto d = ::make_lw_shared(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("v6ns.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(); + chan.shutdown_input(); + try { + co_await std::move(server); + } catch (...) { + if (!ex) { + ex = std::current_exception(); + } + } + chan.close(); + + if (ex) { + std::rethrow_exception(ex); + } +} + SEASTAR_TEST_CASE(test_resolve_tcp, *enable_if_with_networking()) { dns_resolver::options opts;