/* * Copyright (c) 2025, Altomani Gianluca * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include 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 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) { TRY_OR_FAIL(resolver.when_socket_ready()->await()); 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); }) .when_rejected([&](auto& error) { outln("Failed to resolve: {}", error); loop.quit(1); }); 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 { Core::SocketAddress address { IPv4Address { 127, 0, 0, 1 }, server_port }; return DNS::Resolver::SocketResult { TRY(Core::BufferedSocket::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> 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(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 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 { 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::create(move(socket))), DNS::Resolver::ConnectionMode::TCP, }; } }; expect_successful_lookup(resolver, loop); } 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 { 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()) saw_loopback_v4 = a->address == IPv4Address { 127, 0, 0, 1 }; else if (auto const* aaaa = record.record.template get_pointer()) 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); }