diff --git a/Tests/LibDNS/CMakeLists.txt b/Tests/LibDNS/CMakeLists.txt index 9bbfa1414b..c90534bf44 100644 --- a/Tests/LibDNS/CMakeLists.txt +++ b/Tests/LibDNS/CMakeLists.txt @@ -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() diff --git a/Tests/LibDNS/TestDNSResolver.cpp b/Tests/LibDNS/TestDNSResolver.cpp index 48143be33b..6cdbd96ca0 100644 --- a/Tests/LibDNS/TestDNSResolver.cpp +++ b/Tests/LibDNS/TestDNSResolver.cpp @@ -4,28 +4,59 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include +#include +#include +#include +#include +#include #include +#include +#include #include -#include #include -TEST_CASE(test_udp) +#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) { - Core::EventLoop loop; + FixedMemoryStream stream { query_bytes }; + auto query = TRY(DNS::Messages::Message::from_raw(stream)); - DNS::Resolver resolver { - [&] -> ErrorOr { - Core::SocketAddress addr = { IPv4Address::from_string("1.1.1.1"sv).value(), static_cast(53) }; - return DNS::Resolver::SocketResult { - TRY(Core::BufferedSocket::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 { + 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 { - [&] -> ErrorOr { - Core::SocketAddress addr = { IPv4Address::from_string("1.1.1.1"sv).value(), static_cast(53) }; - - auto tcp_socket = TRY(Core::TCPSocket::connect(addr)); - TRY(tcp_socket->set_blocking(false)); - + [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(tcp_socket))), + TRY(Core::BufferedSocket::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 { - Core::SocketAddress addr = { IPv4Address::from_string("1.1.1.1"sv).value(), static_cast(853) }; - - TLS::Options options = {}; - - return DNS::Resolver::SocketResult { - MaybeOwned(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()); -}