2025-02-18 09:05:42 -03:00
|
|
|
/*
|
|
|
|
|
* Copyright (c) 2025, Altomani Gianluca <altomanigianluca@gmail.com>
|
|
|
|
|
*
|
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
|
*/
|
|
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
#include <AK/ByteBuffer.h>
|
|
|
|
|
#include <AK/Endian.h>
|
|
|
|
|
#include <AK/IPv4Address.h>
|
|
|
|
|
#include <AK/IPv6Address.h>
|
|
|
|
|
#include <AK/MemoryStream.h>
|
|
|
|
|
#include <LibCore/EventLoop.h>
|
2025-02-18 09:05:42 -03:00
|
|
|
#include <LibCore/Socket.h>
|
2026-06-17 04:21:12 -03:00
|
|
|
#include <LibCore/TCPServer.h>
|
|
|
|
|
#include <LibCore/UDPServer.h>
|
2025-02-18 09:05:42 -03:00
|
|
|
#include <LibDNS/Resolver.h>
|
|
|
|
|
#include <LibTest/TestCase.h>
|
|
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
#include <AK/Windows.h>
|
2025-02-18 09:05:42 -03:00
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
namespace {
|
2025-02-18 09:05:42 -03:00
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
// 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)
|
|
|
|
|
{
|
|
|
|
|
FixedMemoryStream stream { query_bytes };
|
|
|
|
|
auto query = TRY(DNS::Messages::Message::from_raw(stream));
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
{
|
2025-02-19 10:27:31 -03:00
|
|
|
TRY_OR_FAIL(resolver.when_socket_ready()->await());
|
2025-02-18 09:05:42 -03:00
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
resolver.lookup("example.com", DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA })
|
2025-02-18 09:05:42 -03:00
|
|
|
->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());
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TEST_CASE(test_udp)
|
2025-02-18 09:05:42 -03:00
|
|
|
{
|
|
|
|
|
Core::EventLoop loop;
|
|
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
auto server = Core::UDPServer::construct();
|
|
|
|
|
EXPECT(server->bind(IPv4Address { 127, 0, 0, 1 }, 0));
|
|
|
|
|
auto server_port = server->local_port().value();
|
2025-02-19 10:27:31 -03:00
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
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));
|
|
|
|
|
};
|
2025-02-19 10:27:31 -03:00
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
DNS::Resolver resolver {
|
|
|
|
|
[server_port] -> ErrorOr<DNS::Resolver::SocketResult> {
|
|
|
|
|
Core::SocketAddress address { IPv4Address { 127, 0, 0, 1 }, server_port };
|
2025-02-18 09:05:42 -03:00
|
|
|
return DNS::Resolver::SocketResult {
|
2026-06-17 04:21:12 -03:00
|
|
|
TRY(Core::BufferedSocket<Core::UDPSocket>::create(TRY(Core::UDPSocket::connect(address)))),
|
|
|
|
|
DNS::Resolver::ConnectionMode::UDP,
|
2025-02-18 09:05:42 -03:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
expect_successful_lookup(resolver, loop);
|
|
|
|
|
}
|
2025-02-18 09:05:42 -03:00
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
TEST_CASE(test_tcp)
|
|
|
|
|
{
|
|
|
|
|
Core::EventLoop loop;
|
2025-02-18 09:05:42 -03:00
|
|
|
|
2026-06-17 04:21:12 -03:00
|
|
|
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 {
|
|
|
|
|
[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(socket))),
|
|
|
|
|
DNS::Resolver::ConnectionMode::TCP,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect_successful_lookup(resolver, loop);
|
2025-02-18 09:05:42 -03:00
|
|
|
}
|
|
|
|
|
|
2026-05-29 12:08:36 -03:00
|
|
|
TEST_CASE(test_localhost_resolves_to_loopback_without_a_socket)
|
|
|
|
|
{
|
|
|
|
|
Core::EventLoop loop;
|
|
|
|
|
|
|
|
|
|
// The socket factory must never run: localhost names are answered in-process, never sent upstream.
|
|
|
|
|
DNS::Resolver resolver {
|
|
|
|
|
[&] -> ErrorOr<DNS::Resolver::SocketResult> {
|
|
|
|
|
return Error::from_string_literal("DNS socket should not be created for a localhost lookup");
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
auto expect_loopback = [&](StringView name) {
|
|
|
|
|
bool saw_loopback_v4 = false;
|
|
|
|
|
bool saw_loopback_v6 = false;
|
|
|
|
|
resolver.lookup(name, DNS::Messages::Class::IN, { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA })
|
|
|
|
|
->when_resolved([&](auto& result) {
|
|
|
|
|
for (auto const& record : result->records()) {
|
|
|
|
|
if (auto const* a = record.record.template get_pointer<DNS::Messages::Records::A>())
|
|
|
|
|
saw_loopback_v4 = a->address == IPv4Address { 127, 0, 0, 1 };
|
|
|
|
|
else if (auto const* aaaa = record.record.template get_pointer<DNS::Messages::Records::AAAA>())
|
|
|
|
|
saw_loopback_v6 = aaaa->address == IPv6Address::loopback();
|
|
|
|
|
}
|
|
|
|
|
loop.quit(0);
|
|
|
|
|
})
|
|
|
|
|
.when_rejected([&](auto& error) {
|
|
|
|
|
outln("Failed to resolve {}: {}", name, error);
|
|
|
|
|
loop.quit(1);
|
|
|
|
|
});
|
|
|
|
|
EXPECT_EQ(0, loop.exec());
|
|
|
|
|
EXPECT(saw_loopback_v4);
|
|
|
|
|
EXPECT(saw_loopback_v6);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
expect_loopback("localhost"sv);
|
|
|
|
|
// A multi-label subdomain: the case that the host resolver / upstream server may not map to loopback.
|
|
|
|
|
expect_loopback("test-host.localhost"sv);
|
|
|
|
|
}
|