Tests/LibDNS: Make TestDNSResolver independent of live DNS
Problem: TestDNSResolver resolved google.com against 1.1.1.1 over the real network. Whenever a CI runner couldn’t reach external DNS, the lookup returned no records, and the test failed. Fix: Stand up a local UDP and TCP server on 127.0.0.1 that answers the query with a canned A and AAAA record, and point the resolver at it.
This commit is contained in:
parent
d6014c8869
commit
6757971d59
2 changed files with 120 additions and 67 deletions
|
|
@ -4,5 +4,5 @@ set(TEST_SOURCES
|
|||
)
|
||||
|
||||
foreach(source IN LISTS TEST_SOURCES)
|
||||
ladybird_test("${source}" LibDNS LIBS LibDNS LibTLS)
|
||||
ladybird_test("${source}" LibDNS LIBS LibDNS)
|
||||
endforeach()
|
||||
|
|
|
|||
|
|
@ -4,28 +4,59 @@
|
|||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/Endian.h>
|
||||
#include <AK/IPv4Address.h>
|
||||
#include <AK/IPv6Address.h>
|
||||
#include <AK/MemoryStream.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/Socket.h>
|
||||
#include <LibCore/TCPServer.h>
|
||||
#include <LibCore/UDPServer.h>
|
||||
#include <LibDNS/Resolver.h>
|
||||
#include <LibTLS/TLSv12.h>
|
||||
#include <LibTest/TestCase.h>
|
||||
|
||||
TEST_CASE(test_udp)
|
||||
#include <AK/Windows.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Builds a canned DNS response for a query: It echoes the question section and answers with one fixed A and one fixed
|
||||
// AAAA record. The resolver correlates responses to lookups by header ID, and reads the answer section. Enough to drive
|
||||
// a successful lookup while exercising the real wire encode and decode paths — without depending on a live DNS server.
|
||||
ErrorOr<ByteBuffer> build_dns_response(ReadonlyBytes query_bytes)
|
||||
{
|
||||
Core::EventLoop loop;
|
||||
FixedMemoryStream stream { query_bytes };
|
||||
auto query = TRY(DNS::Messages::Message::from_raw(stream));
|
||||
|
||||
DNS::Resolver resolver {
|
||||
[&] -> ErrorOr<DNS::Resolver::SocketResult> {
|
||||
Core::SocketAddress addr = { IPv4Address::from_string("1.1.1.1"sv).value(), static_cast<u16>(53) };
|
||||
return DNS::Resolver::SocketResult {
|
||||
TRY(Core::BufferedSocket<Core::UDPSocket>::create(TRY(Core::UDPSocket::connect(addr)))),
|
||||
DNS::Resolver::ConnectionMode::UDP,
|
||||
};
|
||||
DNS::Messages::Message response;
|
||||
response.header.id = query.header.id;
|
||||
response.header.options.set_recursion_available(true);
|
||||
response.header.options.set_response_code(DNS::Messages::Options::ResponseCode::NoError);
|
||||
response.header.question_count = query.questions.size();
|
||||
response.questions = move(query.questions);
|
||||
|
||||
auto name = response.questions.is_empty()
|
||||
? DNS::Messages::DomainName::from_string("example.com"sv)
|
||||
: response.questions.first().name;
|
||||
|
||||
response.answers.append(DNS::Messages::ResourceRecord {
|
||||
name, DNS::Messages::ResourceType::A, DNS::Messages::Class::IN, 300,
|
||||
DNS::Messages::Records::A { IPv4Address { 192, 0, 2, 1 } }, {} });
|
||||
response.answers.append(DNS::Messages::ResourceRecord {
|
||||
name, DNS::Messages::ResourceType::AAAA, DNS::Messages::Class::IN, 300,
|
||||
DNS::Messages::Records::AAAA { IPv6Address::loopback() }, {} });
|
||||
response.header.answer_count = response.answers.size();
|
||||
|
||||
ByteBuffer out;
|
||||
TRY(response.to_raw(out));
|
||||
return out;
|
||||
}
|
||||
};
|
||||
|
||||
void expect_successful_lookup(DNS::Resolver& resolver, Core::EventLoop& loop)
|
||||
{
|
||||
TRY_OR_FAIL(resolver.when_socket_ready()->await());
|
||||
|
||||
resolver.lookup("google.com", DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA })
|
||||
resolver.lookup("example.com", DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA })
|
||||
->when_resolved([&](auto& result) {
|
||||
EXPECT(!result->records().is_empty());
|
||||
loop.quit(0);
|
||||
|
|
@ -38,40 +69,94 @@ TEST_CASE(test_udp)
|
|||
EXPECT_EQ(0, loop.exec());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_CASE(test_udp)
|
||||
{
|
||||
Core::EventLoop loop;
|
||||
|
||||
auto server = Core::UDPServer::construct();
|
||||
EXPECT(server->bind(IPv4Address { 127, 0, 0, 1 }, 0));
|
||||
auto server_port = server->local_port().value();
|
||||
|
||||
server->on_ready_to_receive = [&] {
|
||||
sockaddr_in from {};
|
||||
auto query = MUST(server->receive(4096, from));
|
||||
auto response = MUST(build_dns_response(query.bytes()));
|
||||
MUST(server->send(response.bytes(), from));
|
||||
};
|
||||
|
||||
DNS::Resolver resolver {
|
||||
[server_port] -> ErrorOr<DNS::Resolver::SocketResult> {
|
||||
Core::SocketAddress address { IPv4Address { 127, 0, 0, 1 }, server_port };
|
||||
return DNS::Resolver::SocketResult {
|
||||
TRY(Core::BufferedSocket<Core::UDPSocket>::create(TRY(Core::UDPSocket::connect(address)))),
|
||||
DNS::Resolver::ConnectionMode::UDP,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
expect_successful_lookup(resolver, loop);
|
||||
}
|
||||
|
||||
TEST_CASE(test_tcp)
|
||||
{
|
||||
Core::EventLoop loop;
|
||||
|
||||
auto server = MUST(Core::TCPServer::try_create());
|
||||
MUST(server->listen(IPv4Address { 127, 0, 0, 1 }, 0, Core::TCPServer::AllowAddressReuse::Yes));
|
||||
auto server_port = server->local_port().value();
|
||||
|
||||
Vector<NonnullOwnPtr<Core::TCPSocket>> connections;
|
||||
ByteBuffer inbox;
|
||||
server->on_ready_to_accept = [&] {
|
||||
// The listening socket is non-blocking — so a readiness notification can outpace the connection. Tolerate
|
||||
// accept() reporting nothing is ready yet.
|
||||
auto accepted = server->accept();
|
||||
if (accepted.is_error())
|
||||
return;
|
||||
connections.append(accepted.release_value());
|
||||
auto& socket = *connections.last();
|
||||
socket.on_ready_to_read = [&socket, &inbox] {
|
||||
// The accepted socket is non-blocking and TCP is a stream — so read what is available, and accumulate it
|
||||
// until the framed message is complete.
|
||||
u8 chunk[1024];
|
||||
auto read = socket.read_some({ chunk, sizeof(chunk) });
|
||||
if (read.is_error() || inbox.try_append(read.value()).is_error())
|
||||
return;
|
||||
|
||||
// DNS over TCP frames each message with a 2-byte length prefix.
|
||||
if (inbox.size() < sizeof(u16))
|
||||
return;
|
||||
u16 const message_size = (static_cast<u16>(inbox[0]) << 8) | inbox[1];
|
||||
if (inbox.size() < sizeof(u16) + message_size)
|
||||
return;
|
||||
|
||||
auto response = MUST(build_dns_response(inbox.bytes().slice(sizeof(u16), message_size)));
|
||||
ByteBuffer framed;
|
||||
NetworkOrdered<u16> framed_size = response.size();
|
||||
MUST(framed.try_append(&framed_size, sizeof(framed_size)));
|
||||
MUST(framed.try_append(response));
|
||||
MUST(socket.write_until_depleted(framed.bytes()));
|
||||
inbox.clear();
|
||||
};
|
||||
};
|
||||
|
||||
DNS::Resolver resolver {
|
||||
[&] -> ErrorOr<DNS::Resolver::SocketResult> {
|
||||
Core::SocketAddress addr = { IPv4Address::from_string("1.1.1.1"sv).value(), static_cast<u16>(53) };
|
||||
|
||||
auto tcp_socket = TRY(Core::TCPSocket::connect(addr));
|
||||
TRY(tcp_socket->set_blocking(false));
|
||||
|
||||
[server_port] -> ErrorOr<DNS::Resolver::SocketResult> {
|
||||
Core::SocketAddress address { IPv4Address { 127, 0, 0, 1 }, server_port };
|
||||
auto socket = TRY(Core::TCPSocket::connect(address));
|
||||
TRY(socket->set_blocking(false));
|
||||
return DNS::Resolver::SocketResult {
|
||||
TRY(Core::BufferedSocket<Core::TCPSocket>::create(move(tcp_socket))),
|
||||
TRY(Core::BufferedSocket<Core::TCPSocket>::create(move(socket))),
|
||||
DNS::Resolver::ConnectionMode::TCP,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
TRY_OR_FAIL(resolver.when_socket_ready()->await());
|
||||
|
||||
resolver.lookup("google.com", DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA })
|
||||
->when_resolved([&](auto& result) {
|
||||
EXPECT(!result->records().is_empty());
|
||||
loop.quit(0);
|
||||
})
|
||||
.when_rejected([&](auto& error) {
|
||||
outln("Failed to resolve: {}", error);
|
||||
loop.quit(1);
|
||||
});
|
||||
|
||||
EXPECT_EQ(0, loop.exec());
|
||||
expect_successful_lookup(resolver, loop);
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc6761#section-6.3
|
||||
TEST_CASE(test_localhost_resolves_to_loopback_without_a_socket)
|
||||
{
|
||||
Core::EventLoop loop;
|
||||
|
|
@ -109,35 +194,3 @@ TEST_CASE(test_localhost_resolves_to_loopback_without_a_socket)
|
|||
// A multi-label subdomain: the case that the host resolver / upstream server may not map to loopback.
|
||||
expect_loopback("test-host.localhost"sv);
|
||||
}
|
||||
|
||||
TEST_CASE(test_tls)
|
||||
{
|
||||
Core::EventLoop loop;
|
||||
|
||||
DNS::Resolver resolver {
|
||||
[&] -> ErrorOr<DNS::Resolver::SocketResult> {
|
||||
Core::SocketAddress addr = { IPv4Address::from_string("1.1.1.1"sv).value(), static_cast<u16>(853) };
|
||||
|
||||
TLS::Options options = {};
|
||||
|
||||
return DNS::Resolver::SocketResult {
|
||||
MaybeOwned<Core::Socket>(TRY(TLS::TLSv12::connect(addr, "1.1.1.1", move(options)))),
|
||||
DNS::Resolver::ConnectionMode::TCP,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
TRY_OR_FAIL(resolver.when_socket_ready()->await());
|
||||
|
||||
resolver.lookup("google.com", DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA })
|
||||
->when_resolved([&](auto& result) {
|
||||
EXPECT(!result->records().is_empty());
|
||||
loop.quit(0);
|
||||
})
|
||||
.when_rejected([&](auto& error) {
|
||||
outln("Failed to resolve: {}", error);
|
||||
loop.quit(1);
|
||||
});
|
||||
|
||||
EXPECT_EQ(0, loop.exec());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue