Ver código fonte

LibDNS+LibWeb+Ladybird+RequestServer: Let there be DNS over TLS

This commit adds our own DNS resolver, with the aim of implementing DoT
(and eventually DoH, maybe even DNSSEC etc.)
Ali Mohammad Pur 8 meses atrás
pai
commit
7e20f4726f

+ 6 - 0
Libraries/LibDNS/CMakeLists.txt

@@ -0,0 +1,6 @@
+set(SOURCES
+    Message.cpp
+)
+
+serenity_lib(LibDNS dns)
+target_link_libraries(LibDNS PRIVATE LibCore)

+ 1177 - 0
Libraries/LibDNS/Message.cpp

@@ -0,0 +1,1177 @@
+/*
+ * Copyright (c) 2024, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/CountingStream.h>
+#include <AK/MemoryStream.h>
+#include <AK/Stream.h>
+#include <AK/UFixedBigInt.h>
+#include <LibCore/DateTime.h>
+#include <LibDNS/Message.h>
+
+namespace DNS::Messages {
+
+String Options::to_string() const
+{
+    StringBuilder builder;
+    builder.appendff("QR: {}, Opcode: {}, AA: {}, TC: {}, RD: {}, RA: {}, AD: {}, CD: {}, RCODE: {}",
+        is_question() ? "Q" : "R",
+        Messages::to_string(op_code()),
+        is_authoritative_answer(),
+        is_truncated(),
+        recursion_desired(),
+        recursion_available(),
+        authenticated_data(),
+        checking_disabled(),
+        Messages::to_string(response_code()));
+    return MUST(builder.to_string());
+}
+
+StringView to_string(Options::ResponseCode code)
+{
+    switch (code) {
+    case Options::ResponseCode::NoError:
+        return "NoError"sv;
+    case Options::ResponseCode::FormatError:
+        return "FormatError"sv;
+    case Options::ResponseCode::ServerFailure:
+        return "ServerFailure"sv;
+    case Options::ResponseCode::NameError:
+        return "NameError"sv;
+    case Options::ResponseCode::NotImplemented:
+        return "NotImplemented"sv;
+    case Options::ResponseCode::Refused:
+        return "Refused"sv;
+    default:
+        return "UNKNOWN"sv;
+    }
+}
+
+ErrorOr<Message> Message::from_raw(AK::Stream& stream)
+{
+    CountingStream counting_stream { MaybeOwned(stream) };
+    auto context = ParseContext { counting_stream, make<RedBlackTree<u16, DomainName>>() };
+    return from_raw(context);
+}
+
+ErrorOr<Message> Message::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 4.1. (Messages) Format.
+    // | Header      |
+    // | Question    | the question for the name server
+    // | Answer      | RRs answering the question
+    // | Authority   | RRs pointing toward an authority
+    // | Additional  | RRs holding additional information
+    //
+    // The header section is always present.  The header includes fields that
+    // specify which of the remaining sections are present, and also specify
+    // whether the message is a query or a response, a standard query or some
+    // other opcode, etc.
+
+    Header header;
+    Bytes header_bytes { &header, sizeof(Header) };
+    TRY(ctx.stream.read_until_filled(header_bytes));
+
+    Message message {};
+    message.header = header;
+
+    for (size_t i = 0; i < header.question_count; ++i) {
+        auto question = TRY(Question::from_raw(ctx));
+        message.questions.append(move(question));
+    }
+
+    for (size_t i = 0; i < header.answer_count; ++i) {
+        auto answer = TRY(ResourceRecord::from_raw(ctx));
+        message.answers.append(move(answer));
+    }
+
+    for (size_t i = 0; i < header.authority_count; ++i) {
+        auto authority = TRY(ResourceRecord::from_raw(ctx));
+        message.authorities.append(move(authority));
+    }
+
+    for (size_t i = 0; i < header.additional_count; ++i) {
+        auto additional = TRY(ResourceRecord::from_raw(ctx));
+        message.additional_records.append(move(additional));
+    }
+
+    return message;
+}
+
+ErrorOr<size_t> Message::to_raw(ByteBuffer& out) const
+{
+    // NOTE: This is minimally implemented to allow for sending queries,
+    //       server-side responses are not implemented yet.
+    VERIFY(header.answer_count == 0);
+    VERIFY(header.authority_count == 0);
+
+    auto start_size = out.size();
+
+    auto header_bytes = TRY(out.get_bytes_for_writing(sizeof(Header)));
+    memcpy(header_bytes.data(), &header, sizeof(Header));
+
+    for (size_t i = 0; i < header.question_count; i++)
+        TRY(questions[i].to_raw(out));
+
+    for (size_t i = 0; i < header.additional_count; i++)
+        TRY(additional_records[i].to_raw(out));
+
+    return out.size() - start_size;
+}
+
+ErrorOr<String> Message::format_for_log() const
+{
+    StringBuilder builder;
+    builder.appendff("ID: {}\n", header.id);
+    builder.appendff("Flags: {} ({:x})\n", header.options.to_string(), header.options.raw);
+    builder.appendff("qdcount: {}, ancount: {}, nscount: {}, arcount: {}\n", header.question_count, header.answer_count, header.authority_count, header.additional_count);
+
+    if (header.question_count > 0) {
+        builder.appendff("Questions:\n");
+        for (auto& q : questions)
+            builder.appendff("    {} {} {}\n", q.name.to_string(), to_string(q.class_), to_string(q.type));
+    }
+
+    if (header.answer_count > 0) {
+        builder.appendff("Answers:\n");
+        for (auto& a : answers) {
+            builder.appendff("    {} {} {}\n", a.name.to_string(), to_string(a.class_), to_string(a.type));
+            a.record.visit(
+                [&](auto const& record) { builder.appendff("        {}\n", MUST(record.to_string())); },
+                [&](ByteBuffer const& raw) {
+                    builder.appendff("        {:hex-dump}\n", raw.bytes());
+                });
+        }
+    }
+
+    if (header.authority_count > 0) {
+        builder.appendff("Authorities:\n");
+        for (auto& a : authorities) {
+            builder.appendff("    {} {} {}\n", a.name.to_string(), to_string(a.class_), to_string(a.type));
+            a.record.visit(
+                [&](auto const& record) { builder.appendff("        {}\n", MUST(record.to_string())); },
+                [&](ByteBuffer const& raw) {
+                    builder.appendff("        {:hex-dump}\n", raw.bytes());
+                });
+        }
+    }
+
+    if (header.additional_count > 0) {
+        builder.appendff("Additional:\n");
+        for (auto& a : additional_records) {
+            builder.appendff("    {} {} {}\n", a.name.to_string(), to_string(a.type), to_string(a.class_));
+            a.record.visit(
+                [&](auto const& record) { builder.appendff("        {}\n", MUST(record.to_string())); },
+                [&](ByteBuffer const& raw) {
+                    builder.appendff("        {:hex-dump}\n", raw.bytes());
+                });
+        }
+    }
+
+    return builder.to_string();
+}
+
+ErrorOr<Question> Question::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 4.1.2. Question section format.
+    // +        +
+    // | QNAME  | a domain name represented as a sequence of labels
+    // +        +
+    // | QTYPE  | a two octet code which specifies the type of the query
+    // | QCLASS | a two octet code that specifies the class of the query
+
+    auto name = TRY(DomainName::from_raw(ctx));
+    auto type = static_cast<ResourceType>(static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>())));
+    auto class_ = static_cast<Class>(static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>())));
+
+    return Question { move(name), type, class_ };
+}
+
+ErrorOr<void> Question::to_raw(ByteBuffer& out) const
+{
+    TRY(name.to_raw(out));
+
+    auto type_bytes = TRY(out.get_bytes_for_writing(2));
+    auto net_type = static_cast<NetworkOrdered<u16>>(to_underlying(type));
+    memcpy(type_bytes.data(), &net_type, 2);
+
+    auto class_bytes = TRY(out.get_bytes_for_writing(2));
+    auto net_class = static_cast<NetworkOrdered<u16>>(to_underlying(class_));
+    memcpy(class_bytes.data(), &net_class, 2);
+
+    return {};
+}
+
+StringView to_string(ResourceType type)
+{
+    switch (type) {
+    case ResourceType::Reserved:
+        return "Reserved"sv;
+    case ResourceType::A:
+        return "A"sv;
+    case ResourceType::NS:
+        return "NS"sv;
+    case ResourceType::MD:
+        return "MD"sv;
+    case ResourceType::MF:
+        return "MF"sv;
+    case ResourceType::CNAME:
+        return "CNAME"sv;
+    case ResourceType::SOA:
+        return "SOA"sv;
+    case ResourceType::MB:
+        return "MB"sv;
+    case ResourceType::MG:
+        return "MG"sv;
+    case ResourceType::MR:
+        return "MR"sv;
+    case ResourceType::NULL_:
+        return "NULL_"sv;
+    case ResourceType::WKS:
+        return "WKS"sv;
+    case ResourceType::PTR:
+        return "PTR"sv;
+    case ResourceType::HINFO:
+        return "HINFO"sv;
+    case ResourceType::MINFO:
+        return "MINFO"sv;
+    case ResourceType::MX:
+        return "MX"sv;
+    case ResourceType::TXT:
+        return "TXT"sv;
+    case ResourceType::RP:
+        return "RP"sv;
+    case ResourceType::AFSDB:
+        return "AFSDB"sv;
+    case ResourceType::X25:
+        return "X25"sv;
+    case ResourceType::ISDN:
+        return "ISDN"sv;
+    case ResourceType::RT:
+        return "RT"sv;
+    case ResourceType::NSAP:
+        return "NSAP"sv;
+    case ResourceType::NSAP_PTR:
+        return "NSAP_PTR"sv;
+    case ResourceType::SIG:
+        return "SIG"sv;
+    case ResourceType::KEY:
+        return "KEY"sv;
+    case ResourceType::PX:
+        return "PX"sv;
+    case ResourceType::GPOS:
+        return "GPOS"sv;
+    case ResourceType::AAAA:
+        return "AAAA"sv;
+    case ResourceType::LOC:
+        return "LOC"sv;
+    case ResourceType::NXT:
+        return "NXT"sv;
+    case ResourceType::EID:
+        return "EID"sv;
+    case ResourceType::NIMLOC:
+        return "NIMLOC"sv;
+    case ResourceType::SRV:
+        return "SRV"sv;
+    case ResourceType::ATMA:
+        return "ATMA"sv;
+    case ResourceType::NAPTR:
+        return "NAPTR"sv;
+    case ResourceType::KX:
+        return "KX"sv;
+    case ResourceType::CERT:
+        return "CERT"sv;
+    case ResourceType::A6:
+        return "A6"sv;
+    case ResourceType::DNAME:
+        return "DNAME"sv;
+    case ResourceType::SINK:
+        return "SINK"sv;
+    case ResourceType::OPT:
+        return "OPT"sv;
+    case ResourceType::APL:
+        return "APL"sv;
+    case ResourceType::DS:
+        return "DS"sv;
+    case ResourceType::SSHFP:
+        return "SSHFP"sv;
+    case ResourceType::IPSECKEY:
+        return "IPSECKEY"sv;
+    case ResourceType::RRSIG:
+        return "RRSIG"sv;
+    case ResourceType::NSEC:
+        return "NSEC"sv;
+    case ResourceType::DNSKEY:
+        return "DNSKEY"sv;
+    case ResourceType::DHCID:
+        return "DHCID"sv;
+    case ResourceType::NSEC3:
+        return "NSEC3"sv;
+    case ResourceType::NSEC3PARAM:
+        return "NSEC3PARAM"sv;
+    case ResourceType::TLSA:
+        return "TLSA"sv;
+    case ResourceType::SMIMEA:
+        return "SMIMEA"sv;
+    case ResourceType::HIP:
+        return "HIP"sv;
+    case ResourceType::NINFO:
+        return "NINFO"sv;
+    case ResourceType::RKEY:
+        return "RKEY"sv;
+    case ResourceType::TALINK:
+        return "TALINK"sv;
+    case ResourceType::CDS:
+        return "CDS"sv;
+    case ResourceType::CDNSKEY:
+        return "CDNSKEY"sv;
+    case ResourceType::OPENPGPKEY:
+        return "OPENPGPKEY"sv;
+    case ResourceType::CSYNC:
+        return "CSYNC"sv;
+    case ResourceType::ZONEMD:
+        return "ZONEMD"sv;
+    case ResourceType::SVCB:
+        return "SVCB"sv;
+    case ResourceType::HTTPS:
+        return "HTTPS"sv;
+    case ResourceType::SPF:
+        return "SPF"sv;
+    case ResourceType::UINFO:
+        return "UINFO"sv;
+    case ResourceType::UID:
+        return "UID"sv;
+    case ResourceType::GID:
+        return "GID"sv;
+    case ResourceType::UNSPEC:
+        return "UNSPEC"sv;
+    case ResourceType::NID:
+        return "NID"sv;
+    case ResourceType::L32:
+        return "L32"sv;
+    case ResourceType::L64:
+        return "L64"sv;
+    case ResourceType::LP:
+        return "LP"sv;
+    case ResourceType::EUI48:
+        return "EUI48"sv;
+    case ResourceType::EUI64:
+        return "EUI64"sv;
+    case ResourceType::NXNAME:
+        return "NXNAME"sv;
+    case ResourceType::TKEY:
+        return "TKEY"sv;
+    case ResourceType::TSIG:
+        return "TSIG"sv;
+    case ResourceType::IXFR:
+        return "IXFR"sv;
+    case ResourceType::AXFR:
+        return "AXFR"sv;
+    case ResourceType::MAILB:
+        return "MAILB"sv;
+    case ResourceType::MAILA:
+        return "MAILA"sv;
+    case ResourceType::ANY:
+        return "ANY"sv;
+    case ResourceType::URI:
+        return "URI"sv;
+    case ResourceType::CAA:
+        return "CAA"sv;
+    case ResourceType::AVC:
+        return "AVC"sv;
+    case ResourceType::DOA:
+        return "DOA"sv;
+    case ResourceType::AMTRELAY:
+        return "AMTRELAY"sv;
+    case ResourceType::RESINFO:
+        return "RESINFO"sv;
+    case ResourceType::WALLET:
+        return "WALLET"sv;
+    case ResourceType::CLA:
+        return "CLA"sv;
+    case ResourceType::IPN:
+        return "IPN"sv;
+    case ResourceType::TA:
+        return "TA"sv;
+    case ResourceType::DLV:
+        return "DLV"sv;
+    default:
+        return "UNKNOWN"sv;
+    }
+}
+
+Optional<ResourceType> resource_type_from_string(StringView name)
+{
+    if (name == "Reserved"sv)
+        return ResourceType::Reserved;
+    if (name == "A"sv)
+        return ResourceType::A;
+    if (name == "NS"sv)
+        return ResourceType::NS;
+    if (name == "MD"sv)
+        return ResourceType::MD;
+    if (name == "MF"sv)
+        return ResourceType::MF;
+    if (name == "CNAME"sv)
+        return ResourceType::CNAME;
+    if (name == "SOA"sv)
+        return ResourceType::SOA;
+    if (name == "MB"sv)
+        return ResourceType::MB;
+    if (name == "MG"sv)
+        return ResourceType::MG;
+    if (name == "MR"sv)
+        return ResourceType::MR;
+    if (name == "NULL_"sv)
+        return ResourceType::NULL_;
+    if (name == "WKS"sv)
+        return ResourceType::WKS;
+    if (name == "PTR"sv)
+        return ResourceType::PTR;
+    if (name == "HINFO"sv)
+        return ResourceType::HINFO;
+    if (name == "MINFO"sv)
+        return ResourceType::MINFO;
+    if (name == "MX"sv)
+        return ResourceType::MX;
+    if (name == "TXT"sv)
+        return ResourceType::TXT;
+    if (name == "RP"sv)
+        return ResourceType::RP;
+    if (name == "AFSDB"sv)
+        return ResourceType::AFSDB;
+    if (name == "X25"sv)
+        return ResourceType::X25;
+    if (name == "ISDN"sv)
+        return ResourceType::ISDN;
+    if (name == "RT"sv)
+        return ResourceType::RT;
+    if (name == "NSAP"sv)
+        return ResourceType::NSAP;
+    if (name == "NSAP_PTR"sv)
+        return ResourceType::NSAP_PTR;
+    if (name == "SIG"sv)
+        return ResourceType::SIG;
+    if (name == "KEY"sv)
+        return ResourceType::KEY;
+    if (name == "PX"sv)
+        return ResourceType::PX;
+    if (name == "GPOS"sv)
+        return ResourceType::GPOS;
+    if (name == "AAAA"sv)
+        return ResourceType::AAAA;
+    if (name == "LOC"sv)
+        return ResourceType::LOC;
+    if (name == "NXT"sv)
+        return ResourceType::NXT;
+    if (name == "EID"sv)
+        return ResourceType::EID;
+    if (name == "NIMLOC"sv)
+        return ResourceType::NIMLOC;
+    if (name == "SRV"sv)
+        return ResourceType::SRV;
+    if (name == "ATMA"sv)
+        return ResourceType::ATMA;
+    if (name == "NAPTR"sv)
+        return ResourceType::NAPTR;
+    if (name == "KX"sv)
+        return ResourceType::KX;
+    if (name == "CERT"sv)
+        return ResourceType::CERT;
+    if (name == "A6"sv)
+        return ResourceType::A6;
+    if (name == "DNAME"sv)
+        return ResourceType::DNAME;
+    if (name == "SINK"sv)
+        return ResourceType::SINK;
+    if (name == "OPT"sv)
+        return ResourceType::OPT;
+    if (name == "APL"sv)
+        return ResourceType::APL;
+    if (name == "DS"sv)
+        return ResourceType::DS;
+    if (name == "SSHFP"sv)
+        return ResourceType::SSHFP;
+    if (name == "IPSECKEY"sv)
+        return ResourceType::IPSECKEY;
+    if (name == "RRSIG"sv)
+        return ResourceType::RRSIG;
+    if (name == "NSEC"sv)
+        return ResourceType::NSEC;
+    if (name == "DNSKEY"sv)
+        return ResourceType::DNSKEY;
+    if (name == "DHCID"sv)
+        return ResourceType::DHCID;
+    if (name == "NSEC3"sv)
+        return ResourceType::NSEC3;
+    if (name == "NSEC3PARAM"sv)
+        return ResourceType::NSEC3PARAM;
+    if (name == "TLSA"sv)
+        return ResourceType::TLSA;
+    if (name == "SMIMEA"sv)
+        return ResourceType::SMIMEA;
+    if (name == "HIP"sv)
+        return ResourceType::HIP;
+    if (name == "NINFO"sv)
+        return ResourceType::NINFO;
+    if (name == "RKEY"sv)
+        return ResourceType::RKEY;
+    if (name == "TALINK"sv)
+        return ResourceType::TALINK;
+    if (name == "CDS"sv)
+        return ResourceType::CDS;
+    if (name == "CDNSKEY"sv)
+        return ResourceType::CDNSKEY;
+    if (name == "OPENPGPKEY"sv)
+        return ResourceType::OPENPGPKEY;
+    if (name == "CSYNC"sv)
+        return ResourceType::CSYNC;
+    if (name == "ZONEMD"sv)
+        return ResourceType::ZONEMD;
+    if (name == "SVCB"sv)
+        return ResourceType::SVCB;
+    if (name == "HTTPS"sv)
+        return ResourceType::HTTPS;
+    if (name == "SPF"sv)
+        return ResourceType::SPF;
+    if (name == "UINFO"sv)
+        return ResourceType::UINFO;
+    if (name == "UID"sv)
+        return ResourceType::UID;
+    if (name == "GID"sv)
+        return ResourceType::GID;
+    if (name == "UNSPEC"sv)
+        return ResourceType::UNSPEC;
+    if (name == "NID"sv)
+        return ResourceType::NID;
+    if (name == "L32"sv)
+        return ResourceType::L32;
+    if (name == "L64"sv)
+        return ResourceType::L64;
+    if (name == "LP"sv)
+        return ResourceType::LP;
+    if (name == "EUI48"sv)
+        return ResourceType::EUI48;
+    if (name == "EUI64"sv)
+        return ResourceType::EUI64;
+    if (name == "NXNAME"sv)
+        return ResourceType::NXNAME;
+    if (name == "TKEY"sv)
+        return ResourceType::TKEY;
+    if (name == "TSIG"sv)
+        return ResourceType::TSIG;
+    if (name == "IXFR"sv)
+        return ResourceType::IXFR;
+    if (name == "AXFR"sv)
+        return ResourceType::AXFR;
+    if (name == "MAILB"sv)
+        return ResourceType::MAILB;
+    if (name == "MAILA"sv)
+        return ResourceType::MAILA;
+    if (name == "ANY"sv)
+        return ResourceType::ANY;
+    if (name == "URI"sv)
+        return ResourceType::URI;
+    if (name == "CAA"sv)
+        return ResourceType::CAA;
+    if (name == "AVC"sv)
+        return ResourceType::AVC;
+    if (name == "DOA"sv)
+        return ResourceType::DOA;
+    if (name == "AMTRELAY"sv)
+        return ResourceType::AMTRELAY;
+    if (name == "RESINFO"sv)
+        return ResourceType::RESINFO;
+    if (name == "WALLET"sv)
+        return ResourceType::WALLET;
+    if (name == "CLA"sv)
+        return ResourceType::CLA;
+    if (name == "IPN"sv)
+        return ResourceType::IPN;
+    if (name == "TA"sv)
+        return ResourceType::TA;
+    if (name == "DLV"sv)
+        return ResourceType::DLV;
+    return {};
+}
+
+StringView to_string(Class class_)
+{
+    switch (class_) {
+    case Class::IN:
+        return "IN"sv;
+    case Class::CH:
+        return "CH"sv;
+    case Class::HS:
+        return "HS"sv;
+    default:
+        return "UNKNOWN"sv;
+    }
+}
+
+StringView to_string(OpCode code)
+{
+    if ((to_underlying(code) & to_underlying(OpCode::Reserved)) != 0)
+        return "Reserved"sv;
+
+    switch (code) {
+    case OpCode::Query:
+        return "Query"sv;
+    case OpCode::IQuery:
+        return "IQuery"sv;
+    case OpCode::Status:
+        return "Status"sv;
+    case OpCode::Notify:
+        return "Notify"sv;
+    case OpCode::Update:
+        return "Update"sv;
+    case OpCode::DSO:
+        return "DSO"sv;
+    default:
+        return "UNKNOWN"sv;
+    }
+}
+
+DomainName DomainName::from_string(StringView name)
+{
+    DomainName domain_name;
+    name.for_each_split_view('.', SplitBehavior::Nothing, [&](StringView piece) {
+        domain_name.labels.append(piece);
+    });
+    return domain_name;
+}
+
+ErrorOr<DomainName> DomainName::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 4.1.2. Question section format.
+    // QNAME           a domain name represented as a sequence of labels, where
+    //                each label consists of a length octet followed by that
+    //                number of octets.  The domain name terminates with the
+    //                zero length octet for the null label of the root.  Note
+    //                that this field may be an odd number of octets; no
+    //                padding is used.
+    DomainName name;
+    auto input_offset_marker = ctx.stream.read_bytes();
+    while (true) {
+        auto length = TRY(ctx.stream.read_value<u8>());
+        if (length == 0)
+            break;
+
+        constexpr static u8 OffsetMarkerMask = 0b11000000;
+        if ((length & OffsetMarkerMask) == OffsetMarkerMask) {
+            // This is a pointer to a prior domain name.
+            u16 const offset = static_cast<u16>(length & ~OffsetMarkerMask) << 8 | TRY(ctx.stream.read_value<u8>());
+            if (auto it = ctx.pointers->find_largest_not_above_iterator(offset); !it.is_end()) {
+                auto labels = it->labels;
+                for (auto& entry : labels)
+                    name.labels.append(entry);
+                break;
+            }
+            dbgln("Invalid domain name pointer in label, no prior domain name found around offset {}", offset);
+            return Error::from_string_literal("Invalid domain name pointer in label");
+        }
+
+        ByteBuffer content;
+        TRY(ctx.stream.read_until_filled(TRY(content.get_bytes_for_writing(length))));
+        name.labels.append(ByteString::copy(content));
+    }
+
+    ctx.pointers->insert(input_offset_marker, name);
+
+    return name;
+}
+
+ErrorOr<void> DomainName::to_raw(ByteBuffer& out) const
+{
+    for (auto& label : labels) {
+        VERIFY(label.length() <= 63);
+        auto size_bytes = TRY(out.get_bytes_for_writing(1));
+        u8 size = static_cast<u8>(label.length());
+        memcpy(size_bytes.data(), &size, 1);
+
+        auto content_bytes = TRY(out.get_bytes_for_writing(label.length()));
+        memcpy(content_bytes.data(), label.characters(), label.length());
+    }
+
+    TRY(out.try_append(0));
+
+    return {};
+}
+
+String DomainName::to_string() const
+{
+    StringBuilder builder;
+    for (size_t i = 0; i < labels.size(); ++i) {
+        builder.append(labels[i]);
+        builder.append('.');
+    }
+
+    return MUST(builder.to_string());
+}
+
+class RecordingStream final : public Stream {
+public:
+    explicit RecordingStream(Stream& stream)
+        : m_stream(stream)
+    {
+    }
+
+    ByteBuffer take_recorded_data() && { return move(m_recorded_data); }
+
+    virtual ErrorOr<Bytes> read_some(Bytes bytes) override
+    {
+        auto result = TRY(m_stream->read_some(bytes));
+        m_recorded_data.append(result.data(), result.size());
+        return result;
+    }
+    virtual ErrorOr<void> discard(size_t discarded_bytes) override
+    {
+        auto space = TRY(m_recorded_data.get_bytes_for_writing(discarded_bytes));
+        TRY(m_stream->read_until_filled(space));
+        return {};
+    }
+    virtual ErrorOr<size_t> write_some(ReadonlyBytes bytes) override { return m_stream->write_some(bytes); }
+    virtual bool is_eof() const override { return m_stream->is_eof(); }
+    virtual bool is_open() const override { return m_stream->is_open(); }
+    virtual void close() override { m_stream->close(); }
+
+private:
+    MaybeOwned<Stream> m_stream;
+    ByteBuffer m_recorded_data;
+};
+
+ErrorOr<ResourceRecord> ResourceRecord::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 4.1.3. Resource record format.
+    // +           +
+    // | NAME      | a domain name to which this resource record pertains
+    // +           +
+    // | TYPE      | two octets containing one of the RR type codes
+    // | CLASS     | two octets containing one of the RR class codes
+    // | TTL       | a 32-bit unsigned integer that specifies the time interval
+    // |           | that the resource record may be cached
+    // | RDLENGTH  | an unsigned 16-bit integer that specifies the length in
+    // |           | octets of the RDATA field
+    // | RDATA     | a variable length string of octets that describes the resource
+
+    ByteBuffer rdata;
+    ByteBuffer rr_raw_data;
+    DomainName name;
+    ResourceType type;
+    Class class_;
+    u32 ttl;
+    {
+        RecordingStream rr_stream { ctx.stream };
+        CountingStream rr_counting_stream { MaybeOwned<Stream>(rr_stream) };
+        ParseContext rr_ctx { rr_counting_stream, move(ctx.pointers) };
+        ScopeGuard guard([&] { ctx.pointers = move(rr_ctx.pointers); });
+
+        name = TRY(DomainName::from_raw(rr_ctx));
+        type = static_cast<ResourceType>(static_cast<u16>(TRY(rr_ctx.stream.read_value<NetworkOrdered<u16>>())));
+        if (type == ResourceType::OPT) {
+            auto record = ResourceRecord {
+                move(name),
+                type,
+                Class::IN,
+                0,
+                TRY(Records::OPT::from_raw(rr_ctx)),
+                {},
+            };
+            record.raw = move(rr_stream).take_recorded_data();
+            return record;
+        }
+        class_ = static_cast<Class>(static_cast<u16>(TRY(rr_ctx.stream.read_value<NetworkOrdered<u16>>())));
+        ttl = static_cast<u32>(TRY(rr_ctx.stream.read_value<NetworkOrdered<u32>>()));
+        auto rd_length = static_cast<u16>(TRY(rr_ctx.stream.read_value<NetworkOrdered<u16>>()));
+        TRY(rr_ctx.stream.read_until_filled(TRY(rdata.get_bytes_for_writing(rd_length))));
+
+        rr_raw_data = move(rr_stream).take_recorded_data();
+    }
+
+    FixedMemoryStream stream { rdata.bytes() };
+    CountingStream rdata_stream { MaybeOwned<Stream>(stream) };
+    ParseContext rdata_ctx { rdata_stream, move(ctx.pointers) };
+    ScopeGuard guard([&] { ctx.pointers = move(rdata_ctx.pointers); });
+
+#define PARSE_AS_RR(TYPE)                                                                                                                                   \
+    do {                                                                                                                                                    \
+        auto rr = TRY(Records::TYPE::from_raw(rdata_ctx));                                                                                                  \
+        if (!rdata_stream.is_eof()) {                                                                                                                       \
+            dbgln("Extra data ({}) left in stream: {:hex-dump}", rdata.size() - rdata_stream.read_bytes(), rdata.bytes().slice(rdata_stream.read_bytes())); \
+            return Error::from_string_literal("Extra data in " #TYPE " record content");                                                                    \
+        }                                                                                                                                                   \
+        return ResourceRecord { move(name), type, class_, ttl, rr, move(rr_raw_data) };                                                                     \
+    } while (0)
+
+    switch (type) {
+    case ResourceType::A:
+        PARSE_AS_RR(A);
+    case ResourceType::AAAA:
+        PARSE_AS_RR(AAAA);
+    case ResourceType::TXT:
+        PARSE_AS_RR(TXT);
+    case ResourceType::CNAME:
+        PARSE_AS_RR(CNAME);
+    case ResourceType::NS:
+        PARSE_AS_RR(NS);
+    case ResourceType::SOA:
+        PARSE_AS_RR(SOA);
+    case ResourceType::MX:
+        PARSE_AS_RR(MX);
+    case ResourceType::PTR:
+        PARSE_AS_RR(PTR);
+    case ResourceType::SRV:
+        PARSE_AS_RR(SRV);
+    case ResourceType::DNSKEY:
+        PARSE_AS_RR(DNSKEY);
+    case ResourceType::CDNSKEY:
+        PARSE_AS_RR(CDNSKEY);
+    case ResourceType::DS:
+        PARSE_AS_RR(DS);
+    case ResourceType::CDS:
+        PARSE_AS_RR(CDS);
+    case ResourceType::RRSIG:
+        PARSE_AS_RR(RRSIG);
+    // case ResourceType::NSEC:
+    //     PARSE_AS_RR(NSEC);
+    // case ResourceType::NSEC3:
+    //     PARSE_AS_RR(NSEC3);
+    // case ResourceType::NSEC3PARAM:
+    //     PARSE_AS_RR(NSEC3PARAM);
+    // case ResourceType::TLSA:
+    //     PARSE_AS_RR(TLSA);
+    case ResourceType::HINFO:
+        PARSE_AS_RR(HINFO);
+    default:
+        return ResourceRecord { move(name), type, class_, ttl, move(rdata), move(rr_raw_data) };
+    }
+#undef PARSE_AS_RR
+}
+
+ErrorOr<void> ResourceRecord::to_raw(ByteBuffer& buffer) const
+{
+    TRY(name.to_raw(buffer));
+
+    auto type_bytes = TRY(buffer.get_bytes_for_writing(2));
+    auto net_type = static_cast<NetworkOrdered<u16>>(to_underlying(type));
+    memcpy(type_bytes.data(), &net_type, 2);
+
+    if (type != ResourceType::OPT) {
+        auto class_bytes = TRY(buffer.get_bytes_for_writing(2));
+        auto net_class = static_cast<NetworkOrdered<u16>>(to_underlying(class_));
+        memcpy(class_bytes.data(), &net_class, 2);
+
+        auto ttl_bytes = TRY(buffer.get_bytes_for_writing(4));
+        auto net_ttl = static_cast<NetworkOrdered<u32>>(ttl);
+        memcpy(ttl_bytes.data(), &net_ttl, 4);
+    }
+
+    ByteBuffer rdata;
+    TRY(record.visit(
+        [&](auto const& record) { return record.to_raw(rdata); },
+        [&](ByteBuffer const& raw) { return rdata.try_append(raw); }));
+
+    if (type != ResourceType::OPT) {
+        auto rdata_length_bytes = TRY(buffer.get_bytes_for_writing(2));
+        auto net_rdata_length = static_cast<NetworkOrdered<u16>>(rdata.size());
+        memcpy(rdata_length_bytes.data(), &net_rdata_length, 2);
+    }
+
+    TRY(buffer.try_append(rdata));
+
+    return {};
+}
+
+ErrorOr<String> ResourceRecord::to_string() const
+{
+    StringBuilder builder;
+    record.visit(
+        [&](auto const& record) { builder.appendff("{}", MUST(record.to_string())); },
+        [&](ByteBuffer const& raw) { builder.appendff("{:hex-dump}", raw.bytes()); });
+    return builder.to_string();
+}
+
+ErrorOr<Records::A> Records::A::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.4.1. A RDATA format.
+    // | ADDRESS | a 32 bit Internet address.
+
+    u32 const address = TRY(ctx.stream.read_value<LittleEndian<u32>>());
+    return Records::A { IPv4Address { address } };
+}
+
+ErrorOr<Records::AAAA> Records::AAAA::from_raw(ParseContext& ctx)
+{
+    // RFC 3596, 2.2. AAAA RDATA format.
+    // | ADDRESS | a 128 bit Internet address.
+
+    u128 const address = TRY(ctx.stream.read_value<LittleEndian<u128>>());
+    return Records::AAAA { IPv6Address { bit_cast<Array<u8, 16>>(address) } };
+}
+
+ErrorOr<Records::TXT> Records::TXT::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.3.14. TXT RDATA format.
+    // | TXT-DATA | a <character-string> which is used for human readability.
+
+    auto length = TRY(ctx.stream.read_value<u8>());
+    ByteBuffer content;
+    TRY(ctx.stream.read_until_filled(TRY(content.get_bytes_for_writing(length))));
+    return Records::TXT { ByteString::copy(content) };
+}
+
+ErrorOr<Records::CNAME> Records::CNAME::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.3.1. CNAME RDATA format.
+    // | CNAME | a <domain-name> which specifies the canonical or primary name for the owner.
+
+    auto name = TRY(DomainName::from_raw(ctx));
+    return Records::CNAME { move(name) };
+}
+
+ErrorOr<Records::NS> Records::NS::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.3.11. NS RDATA format.
+    // | NSDNAME | a <domain-name> which specifies a host which should be authoritative for the specified class and domain.
+
+    auto name = TRY(DomainName::from_raw(ctx));
+    return Records::NS { move(name) };
+}
+
+ErrorOr<Records::SOA> Records::SOA::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.3.13. SOA RDATA format.
+    // | MNAME   | <domain-name> which specifies the name of the host where the master file for the zone is maintained.
+    // | RNAME   | <domain-name> which specifies the mailbox of the person responsible for this zone.
+    // | SERIAL  | a 32-bit unsigned integer that specifies the version number of the original copy of the zone.
+    // | REFRESH | a 32-bit unsigned integer that specifies the time interval before the zone should be refreshed.
+    // | RETRY   | a 32-bit unsigned integer that specifies the time interval that should elapse before a failed refresh should be retried.
+    // | EXPIRE  | a 32-bit unsigned integer that specifies the time value that specifies the upper limit on the time interval that can elapse before the zone is no longer authoritative.
+    // | MINIMUM | a 32-bit unsigned integer that specifies the minimum TTL field that should be exported with any RR from this zone.
+
+    auto mname = TRY(DomainName::from_raw(ctx));
+    auto rname = TRY(DomainName::from_raw(ctx));
+    auto serial = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto refresh = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto retry = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto expire = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto minimum = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+
+    return Records::SOA { move(mname), move(rname), serial, refresh, retry, expire, minimum };
+}
+
+ErrorOr<Records::MX> Records::MX::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.3.9. MX RDATA format.
+    // | PREFERENCE | a 16 bit integer which specifies the preference given to this RR among others at the same owner.
+    // | EXCHANGE   | a <domain-name> which specifies a host willing to act as a mail exchange for the owner name.
+
+    auto preference = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto exchange = TRY(DomainName::from_raw(ctx));
+    return Records::MX { preference, move(exchange) };
+}
+
+ErrorOr<Records::PTR> Records::PTR::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.3.12. PTR RDATA format.
+    // | PTRDNAME | a <domain-name> which points to some location in the domain name space.
+
+    auto name = TRY(DomainName::from_raw(ctx));
+    return Records::PTR { move(name) };
+}
+
+ErrorOr<Records::SRV> Records::SRV::from_raw(ParseContext& ctx)
+{
+    // RFC 2782, 2. Service location and priority.
+    // | PRIORITY | a 16 bit integer that specifies the priority of this target host.
+    // | WEIGHT   | a 16 bit integer that specifies a weight for this target host.
+    // | PORT     | a 16 bit integer that specifies the port on this target host.
+    // | TARGET   | a <domain-name> which specifies the target host.
+
+    auto priority = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto weight = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto port = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto target = TRY(DomainName::from_raw(ctx));
+    return Records::SRV { priority, weight, port, move(target) };
+}
+
+ErrorOr<Records::DNSKEY> Records::DNSKEY::from_raw(ParseContext& ctx)
+{
+    // RFC 4034, 2.1. The DNSKEY Resource Record.
+    // | FLAGS    | a 16-bit value that flags the key.
+    // | PROTOCOL | an 8-bit value that specifies the protocol for this key.
+    // | ALGORITHM| an 8-bit value that identifies the public key's cryptographic algorithm.
+    // | PUBLICKEY| the public key material.
+
+    auto flags = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto protocol = TRY(ctx.stream.read_value<u8>());
+    auto algorithm = static_cast<DNSSEC::Algorithm>(static_cast<u8>(TRY(ctx.stream.read_value<u8>())));
+    auto public_key = TRY(ctx.stream.read_until_eof());
+    return Records::DNSKEY { flags, protocol, algorithm, move(public_key) };
+}
+
+ErrorOr<Records::DS> Records::DS::from_raw(ParseContext& ctx)
+{
+    // RFC 4034, 5.1. The DS Resource Record.
+    // | KEYTAG      | a 16-bit value that identifies the DNSKEY RR.
+    // | ALGORITHM   | an 8-bit value that identifies the DS's hash algorithm.
+    // | DIGEST TYPE | an 8-bit value that identifies the DS's digest algorithm.
+    // | DIGEST      | Digest of the DNSKEY RDATA (Flags | Protocol | Algorithm | Pubkey).
+
+    auto key_tag = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto algorithm = static_cast<DNSSEC::Algorithm>(static_cast<u8>(TRY(ctx.stream.read_value<u8>())));
+    auto digest_type = static_cast<DNSSEC::DigestType>(static_cast<u8>(TRY(ctx.stream.read_value<u8>())));
+    size_t digest_size;
+    switch (digest_type) {
+    case DNSSEC::DigestType::SHA1:
+        digest_size = 20;
+        break;
+    case DNSSEC::DigestType::SHA256:
+    case DNSSEC::DigestType::GOST3411:
+        digest_size = 32;
+        break;
+    case DNSSEC::DigestType::SHA384:
+        digest_size = 48;
+        break;
+    case DNSSEC::DigestType::SHA512:
+        digest_size = 64;
+        break;
+    case DNSSEC::DigestType::SHA224:
+        digest_size = 28;
+        break;
+    case DNSSEC::DigestType::Unknown:
+    default:
+        return Error::from_string_literal("Unknown digest type in DS record");
+    }
+
+    ByteBuffer digest;
+    TRY(ctx.stream.read_until_filled(TRY(digest.get_bytes_for_writing(digest_size))));
+    return Records::DS { key_tag, algorithm, digest_type, move(digest) };
+}
+
+ErrorOr<Records::SIG> Records::SIG::from_raw(ParseContext& ctx)
+{
+    // RFC 4034, 2.2. The SIG Resource Record.
+    // | TYPE-COVERED | a 16-bit value that specifies the type of the RRset that is covered by this SIG.
+    // | ALGORITHM    | an 8-bit value that specifies the algorithm used to create the signature.
+    // | LABELS       | an 8-bit value that specifies the number of labels in the original SIG RR owner name.
+    // | ORIGINAL TTL | a 32-bit value that specifies the TTL of the covered RRset as it appears in the authoritative zone.
+    // | SIGNATURE EXPIRATION | a 32-bit value that specifies the expiration date of the signature.
+    // | SIGNATURE INCEPTION  | a 32-bit value that specifies the inception date of the signature.
+    // | KEY TAG      | a 16-bit value that contains the key tag value of the DNSKEY RR that was used to create the signature.
+    // | SIGNER'S NAME| a <domain-name> which specifies the domain name of the signer generating the SIG RR.
+    // | SIGNATURE    | a <signature> that authenticates the RRs.
+
+    auto type_covered = static_cast<ResourceType>(static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>())));
+    auto algorithm = static_cast<DNSSEC::Algorithm>(static_cast<u8>(TRY(ctx.stream.read_value<u8>())));
+    auto labels = TRY(ctx.stream.read_value<u8>());
+    auto original_ttl = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto signature_expiration = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto signature_inception = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto key_tag = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto signer_name = TRY(DomainName::from_raw(ctx));
+    auto signature = TRY(ctx.stream.read_until_eof());
+
+    return Records::SIG { type_covered, algorithm, labels, original_ttl, UnixDateTime::from_seconds_since_epoch(signature_expiration), UnixDateTime::from_seconds_since_epoch(signature_inception), key_tag, move(signer_name), move(signature) };
+}
+
+ErrorOr<String> Records::SIG::to_string() const
+{
+    // Single line:
+    // SIG Type covered: <type>, Algorithm: <algorithm>, Labels: <labels>, Original TTL: <ttl>, Signature expiration: <expiration>, Signature inception: <inception>, Key tag: <key tag>, Signer's name: <signer>, Signature: <signature>
+    StringBuilder builder;
+    builder.append("SIG "sv);
+    builder.appendff("Type covered: {}, ", Messages::to_string(type_covered));
+    builder.appendff("Algorithm: {}, ", DNSSEC::to_string(algorithm));
+    builder.appendff("Labels: {}, ", label_count);
+    builder.appendff("Original TTL: {}, ", original_ttl);
+    builder.appendff("Signature expiration: {}, ", Core::DateTime::from_timestamp(expiration.truncated_seconds_since_epoch()));
+    builder.appendff("Signature inception: {}, ", Core::DateTime::from_timestamp(inception.truncated_seconds_since_epoch()));
+    builder.appendff("Key tag: {}, ", key_tag);
+    builder.appendff("Signer's name: '{}', ", signers_name.to_string());
+    builder.appendff("Signature: {}", TRY(encode_base64(signature)));
+    return builder.to_string();
+}
+
+ErrorOr<Records::HINFO> Records::HINFO::from_raw(ParseContext& ctx)
+{
+    // RFC 1035, 3.3.2. HINFO RDATA format.
+    // | CPU    | a <character-string> which specifies the CPU type.
+    // | OS     | a <character-string> which specifies the operating system type.
+
+    auto cpu_length = TRY(ctx.stream.read_value<u8>());
+    ByteBuffer cpu;
+    TRY(ctx.stream.read_until_filled(TRY(cpu.get_bytes_for_writing(cpu_length))));
+    auto os_length = TRY(ctx.stream.read_value<u8>());
+    ByteBuffer os;
+    TRY(ctx.stream.read_until_filled(TRY(os.get_bytes_for_writing(os_length))));
+    return Records::HINFO { ByteString::copy(cpu), ByteString::copy(os) };
+}
+
+ErrorOr<Records::OPT> Records::OPT::from_raw(ParseContext& ctx)
+{
+    // RFC 6891, 6.1. The OPT pseudo-RR.
+    // This RR does *not* use the standard RDATA format, `ctx` starts right after 'TYPE'.
+    // | NAME       | empty (root domain)
+    // | TYPE       | OPT (41)
+    // - we are here -
+    // | UDP SIZE   | 16-bit max UDP payload size
+    // | RCODE_AND_FLAGS | 32-bit flags and response code
+    // | RDLENGTH   | 16-bit length of the RDATA field
+    // | RDATA      | variable length, pairs of OPTION-CODE and OPTION-DATA { length(16), data(length) }
+
+    auto udp_size = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    auto rcode_and_flags = static_cast<u32>(TRY(ctx.stream.read_value<NetworkOrdered<u32>>()));
+    auto rd_length = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+    Vector<OPT::Option> options;
+    while (rd_length > 0 && !ctx.stream.is_eof()) {
+        auto option_code = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+        auto option_length = static_cast<u16>(TRY(ctx.stream.read_value<NetworkOrdered<u16>>()));
+        ByteBuffer option_data;
+        TRY(ctx.stream.read_until_filled(TRY(option_data.get_bytes_for_writing(option_length))));
+        rd_length -= 4 + option_length;
+        options.append(OPT::Option { option_code, move(option_data) });
+    }
+
+    if (rd_length != 0)
+        return Error::from_string_literal("Invalid OPT record");
+
+    return Records::OPT { udp_size, rcode_and_flags, move(options) };
+}
+
+ErrorOr<void> Records::OPT::to_raw(ByteBuffer& buffer) const
+{
+    auto udp_size_bytes = TRY(buffer.get_bytes_for_writing(sizeof(udp_payload_size)));
+    auto net_udp_size = static_cast<NetworkOrdered<u16>>(udp_payload_size);
+    memcpy(udp_size_bytes.data(), &net_udp_size, 2);
+
+    auto rcode_and_flags_bytes = TRY(buffer.get_bytes_for_writing(sizeof(extended_rcode_and_flags)));
+    auto net_rcode_and_flags = static_cast<NetworkOrdered<u32>>(extended_rcode_and_flags);
+    memcpy(rcode_and_flags_bytes.data(), &net_rcode_and_flags, 4);
+
+    auto rd_length_bytes = TRY(buffer.get_bytes_for_writing(2));
+    u16 rd_length = 0;
+    for (auto const& option : options) {
+        rd_length += 4 + option.data.size();
+    }
+    auto net_rd_length = static_cast<NetworkOrdered<u16>>(rd_length);
+    memcpy(rd_length_bytes.data(), &net_rd_length, 2);
+
+    for (auto& option : options) {
+        auto option_code_bytes = TRY(buffer.get_bytes_for_writing(sizeof(option.code)));
+        auto net_option_code = static_cast<NetworkOrdered<u16>>(option.code);
+        memcpy(option_code_bytes.data(), &net_option_code, 2);
+
+        auto option_length_bytes = TRY(buffer.get_bytes_for_writing(2));
+        auto net_option_length = static_cast<NetworkOrdered<u16>>(option.data.size());
+        memcpy(option_length_bytes.data(), &net_option_length, 2);
+
+        TRY(buffer.try_append(option.data));
+    }
+
+    return {};
+}
+
+}

+ 704 - 0
Libraries/LibDNS/Message.h

@@ -0,0 +1,704 @@
+/*
+ * Copyright (c) 2024, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Base64.h>
+#include <AK/MaybeOwned.h>
+#include <AK/RedBlackTree.h>
+#include <AK/Time.h>
+#include <LibCore/Promise.h>
+#include <LibCore/SocketAddress.h>
+#include <LibURL/URL.h>
+
+namespace DNS {
+namespace Messages {
+
+struct DomainName;
+struct ParseContext {
+    CountingStream& stream;
+    NonnullOwnPtr<RedBlackTree<u16, DomainName>> pointers;
+};
+
+enum class OpCode : u8;
+
+struct Options {
+    //                                  1  1  1  1  1  1
+    //    0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+    //    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+    //    |                      ID                       |
+    //    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+    //    |QR| Opcode |AA|TC|RD|RA|   Z |AD|CD|   RCODE   |
+    constexpr inline static u16 QRMask = 0b1000000000000000;
+    constexpr inline static u16 OpCodeMask = 0b0111100000000000;
+    constexpr inline static u16 AuthoritativeAnswerMask = 0b0000010000000000;
+    constexpr inline static u16 TruncatedMask = 0b0000001000000000;
+    constexpr inline static u16 RecursionDesiredMask = 0b0000000100000000;
+    constexpr inline static u16 RecursionAvailableMask = 0b0000000010000000;
+    constexpr inline static u16 AuthenticatedDataMask = 0b0000000000100000;
+    constexpr inline static u16 CheckingDisabledMask = 0b0000000000010000;
+    constexpr inline static u16 ResponseCodeMask = 0b0000000000001111;
+
+    enum class ResponseCode : u16 {
+        NoError = 0,
+        FormatError = 1,
+        ServerFailure = 2,
+        NameError = 3,
+        NotImplemented = 4,
+        Refused = 5,
+    };
+
+    void set_is_question(bool value) { raw = (raw & ~QRMask) | (value ? QRMask : 0); }
+    void set_is_authoritative_answer(bool value) { raw = (raw & ~AuthoritativeAnswerMask) | (value ? AuthoritativeAnswerMask : 0); }
+    void set_is_truncated(bool value) { raw = (raw & ~TruncatedMask) | (value ? TruncatedMask : 0); }
+    void set_recursion_desired(bool value) { raw = (raw & ~RecursionDesiredMask) | (value ? RecursionDesiredMask : 0); }
+    void set_recursion_available(bool value) { raw = (raw & ~RecursionAvailableMask) | (value ? RecursionAvailableMask : 0); }
+    void set_response_code(ResponseCode code) { raw = (raw & ~ResponseCodeMask) | static_cast<u16>(code); }
+    void set_checking_disabled(bool value) { raw = (raw & ~CheckingDisabledMask) | (value ? CheckingDisabledMask : 0); }
+    void set_authenticated_data(bool value) { raw = (raw & ~AuthenticatedDataMask) | (value ? AuthenticatedDataMask : 0); }
+    void set_op_code(OpCode code) { raw = (raw & ~OpCodeMask) | (static_cast<u16>(code) << 11); }
+
+    bool is_question() const { return (raw & QRMask) == 0; }
+    bool is_authoritative_answer() const { return (raw & AuthoritativeAnswerMask) != 0; }
+    bool is_truncated() const { return (raw & TruncatedMask) != 0; }
+    bool recursion_desired() const { return (raw & RecursionDesiredMask) != 0; }
+    bool recursion_available() const { return (raw & RecursionAvailableMask) != 0; }
+    bool checking_disabled() const { return (raw & CheckingDisabledMask) != 0; }
+    bool authenticated_data() const { return (raw & AuthenticatedDataMask) != 0; }
+    ResponseCode response_code() const { return static_cast<ResponseCode>(raw & ResponseCodeMask); }
+    OpCode op_code() const { return static_cast<OpCode>((raw & OpCodeMask) >> 11); }
+
+    String to_string() const;
+
+    NetworkOrdered<u16> raw { 0 };
+};
+StringView to_string(Options::ResponseCode);
+
+struct Header {
+    NetworkOrdered<u16> id;
+    Options options;
+    NetworkOrdered<u16> question_count;
+    NetworkOrdered<u16> answer_count;
+    NetworkOrdered<u16> authority_count;
+    NetworkOrdered<u16> additional_count;
+};
+
+struct DomainName {
+    Vector<ByteString> labels;
+
+    static DomainName from_string(StringView);
+    static ErrorOr<DomainName> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const;
+    String to_string() const;
+};
+
+// Listing from IANA https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-4.
+enum class ResourceType : u16 {
+    Reserved = 0,    // [RFC6895]
+    A = 1,           // a host address [RFC1035]
+    NS = 2,          // an authoritative name server [RFC1035]
+    MD = 3,          // a mail destination (OBSOLETE - use MX) [RFC1035]
+    MF = 4,          // a mail forwarder (OBSOLETE - use MX) [RFC1035]
+    CNAME = 5,       // the canonical name for an alias [RFC1035]
+    SOA = 6,         // marks the start of a zone of authority [RFC1035]
+    MB = 7,          // a mailbox domain name (EXPERIMENTAL) [RFC1035]
+    MG = 8,          // a mail group member (EXPERIMENTAL) [RFC1035]
+    MR = 9,          // a mail rename domain name (EXPERIMENTAL) [RFC1035]
+    NULL_ = 10,      // a null RR (EXPERIMENTAL) [RFC1035]
+    WKS = 11,        // a well known service description [RFC1035]
+    PTR = 12,        // a domain name pointer [RFC1035]
+    HINFO = 13,      // host information [RFC1035]
+    MINFO = 14,      // mailbox or mail list information [RFC1035]
+    MX = 15,         // mail exchange [RFC1035]
+    TXT = 16,        // text strings [RFC1035]
+    RP = 17,         // for Responsible Person [RFC1183]
+    AFSDB = 18,      // for AFS Data Base location [RFC1183][RFC5864]
+    X25 = 19,        // for X.25 PSDN address [RFC1183]
+    ISDN = 20,       // for ISDN address [RFC1183]
+    RT = 21,         // for Route Through [RFC1183]
+    NSAP = 22,       // for NSAP address, NSAP style A record (DEPRECATED) [RFC1706][Moving TPC.INT and NSAP.INT infrastructure domains to historic]
+    NSAP_PTR = 23,   // for domain name pointer, NSAP style (DEPRECATED) [RFC1706][Moving TPC.INT and NSAP.INT infrastructure domains to historic]
+    SIG = 24,        // for security signature [RFC2536][RFC2931][RFC3110][RFC4034]
+    KEY = 25,        // for security key [RFC2536][RFC2539][RFC3110][RFC4034]
+    PX = 26,         // X.400 mail mapping information [RFC2163]
+    GPOS = 27,       // Geographical Position [RFC1712]
+    AAAA = 28,       // IP6 Address [RFC3596]
+    LOC = 29,        // Location Information [RFC1876]
+    NXT = 30,        // Next Domain (OBSOLETE) [RFC2535][RFC3755]
+    EID = 31,        // Endpoint Identifier [Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt]
+    NIMLOC = 32,     // Nimrod Locator [1][Michael_Patton][http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt]
+    SRV = 33,        // Server Selection [1][RFC2782]
+    ATMA = 34,       // ATM Address "[ ATM Forum Technical Committee, "ATM Name System, V2.0", Doc ID: AF-DANS-0152.000, July 2000. Available from and held in escrow by IANA.]"
+    NAPTR = 35,      // Naming Authority Pointer [RFC3403]
+    KX = 36,         // Key Exchanger [RFC2230]
+    CERT = 37,       // CERT [RFC4398]
+    A6 = 38,         // A6 (OBSOLETE - use AAAA) [RFC2874][RFC3226][RFC6563]
+    DNAME = 39,      // DNAME [RFC6672]
+    SINK = 40,       // SINK [Donald_E_Eastlake][draft-eastlake-kitchen-sink]
+    OPT = 41,        // OPT [RFC3225][RFC6891]
+    APL = 42,        // APL [RFC3123]
+    DS = 43,         // Delegation Signer [RFC4034]
+    SSHFP = 44,      // SSH Key Fingerprint [RFC4255]
+    IPSECKEY = 45,   // IPSECKEY [RFC4025]
+    RRSIG = 46,      // RRSIG [RFC4034]
+    NSEC = 47,       // NSEC [RFC4034][RFC9077]
+    DNSKEY = 48,     // DNSKEY [RFC4034]
+    DHCID = 49,      // DHCID [RFC4701]
+    NSEC3 = 50,      // NSEC3 [RFC5155][RFC9077]
+    NSEC3PARAM = 51, // NSEC3PARAM [RFC5155]
+    TLSA = 52,       // TLSA [RFC6698]
+    SMIMEA = 53,     // S/MIME cert association [RFC8162]
+    HIP = 55,        // Host Identity Protocol [RFC8005]
+    NINFO = 56,      // NINFO [Jim_Reid]
+    RKEY = 57,       // RKEY [Jim_Reid]
+    TALINK = 58,     // Trust Anchor LINK [Wouter_Wijngaards]
+    CDS = 59,        // Child DS [RFC7344]
+    CDNSKEY = 60,    // DNSKEY(s) the Child wants reflected in DS [RFC7344]
+    OPENPGPKEY = 61, // OpenPGP Key [RFC7929]
+    CSYNC = 62,      // Child-To-Parent Synchronization [RFC7477]
+    ZONEMD = 63,     // Message Digest Over Zone Data [RFC8976]
+    SVCB = 64,       // General-purpose service binding [RFC9460]
+    HTTPS = 65,      // SVCB-compatible type for use with HTTP [RFC9460]
+    SPF = 99,        // [RFC7208]
+    UINFO = 100,     // [IANA-Reserved]
+    UID = 101,       // [IANA-Reserved]
+    GID = 102,       // [IANA-Reserved]
+    UNSPEC = 103,    // [IANA-Reserved]
+    NID = 104,       // [RFC6742]
+    L32 = 105,       // [RFC6742]
+    L64 = 106,       // [RFC6742]
+    LP = 107,        // [RFC6742]
+    EUI48 = 108,     // an EUI-48 address [RFC7043]
+    EUI64 = 109,     // an EUI-64 address [RFC7043]
+    NXNAME = 128,    // NXDOMAIN indicator for Compact Denial of Existence [draft-ietf-dnsop-compact-denial-of-existence-04]
+    TKEY = 249,      // Transaction Key [RFC2930]
+    TSIG = 250,      // Transaction Signature [RFC8945]
+    IXFR = 251,      // incremental transfer [RFC1995]
+    AXFR = 252,      // transfer of an entire zone [RFC1035][RFC5936]
+    MAILB = 253,     // mailbox-related RRs (MB, MG or MR) [RFC1035]
+    MAILA = 254,     // mail agent RRs (OBSOLETE - see MX) [RFC1035]
+    ANY = 255,       // A request for some or all records the server has available [RFC1035][RFC6895][RFC8482]
+    URI = 256,       // URI [RFC7553]
+    CAA = 257,       // Certification Authority Restriction [RFC8659]
+    AVC = 258,       // Application Visibility and Control [Wolfgang_Riedel]
+    DOA = 259,       // Digital Object Architecture [draft-durand-doa-over-dns]
+    AMTRELAY = 260,  // Automatic Multicast Tunneling Relay [RFC8777]
+    RESINFO = 261,   // Resolver Information as Key/Value Pairs [RFC9606]
+    WALLET = 262,    // Public wallet address [Paul_Hoffman]
+    CLA = 263,       // BP Convergence Layer Adapter [draft-johnson-dns-ipn-cla-07]
+    IPN = 264,       // BP Node Number [draft-johnson-dns-ipn-cla-07]
+    TA = 32768,      // DNSSEC Trust Authorities "[Sam_Weiler][Deploying DNSSEC Without a Signed Root.  Technical Report 1999-19, Information Networking Institute, Carnegie Mellon University, April 2004.]"
+    DLV = 32769,     // DNSSEC Lookaside Validation (OBSOLETE) [RFC8749][RFC4431]
+};
+StringView to_string(ResourceType);
+Optional<ResourceType> resource_type_from_string(StringView);
+
+// Listing from IANA https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-2.
+enum class Class : u16 {
+    IN = 1, // the Internet [RFC1035]
+    CH = 3, // the CHAOS class [Moon1981]
+    HS = 4, // Hesiod [Dyer1987]
+};
+StringView to_string(Class);
+
+// Listing from IANA https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-3.
+enum class OpCode : u8 {
+    Query = 0,        // a standard query (QUERY)
+    IQuery = 1,       // an inverse query (IQUERY)
+    Status = 2,       // a server status request (STATUS)
+    Notify = 4,       // NOTIFY
+    Update = 5,       // dynamic update (RFC 2136)
+    DSO = 6,          // DNS Stateful Operations (DSO) [RFC8490]
+    Reserved = 7,     // [RFC6895]
+    ReservedMask = 15 // [RFC6895]
+};
+StringView to_string(OpCode);
+
+namespace TLSA {
+
+// Listings from IANA https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml.
+enum class CertUsage : u8 {
+    CAConstraint = 0,
+    ServiceCertificateConstraint = 1,
+    TrustAnchorAssertion = 2,
+    DomainIssuedCertificate = 3,
+    Private = 255
+};
+enum class Selector : u8 {
+    FullCertificate = 0,
+    SubjectPublicKeyInfo = 1,
+    Private = 255
+};
+enum class MatchingType : u8 {
+    Full = 0,
+    SHA256 = 1,
+    SHA512 = 2,
+    Private = 255
+};
+
+}
+
+namespace DNSSEC {
+
+// Listing from IANA https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml.
+enum class Algorithm : u8 {
+    RSAMD5 = 1,           // RSA/MD5 [RFC4034][RFC3110]
+    DSA = 3,              // DSA/SHA-1 [RFC3755][RFC2536]
+    RSASHA1 = 5,          // RSA/SHA-1 [RFC3110]
+    RSASHA1NSEC3SHA1 = 7, // [RFC5155]
+    RSASHA256 = 8,        // RSA/SHA-256 [RFC5702]
+    RSASHA512 = 10,       // RSA/SHA-512 [RFC5702]
+    ECDSAP256SHA256 = 13, // ECDSA Curve P-256 with SHA-256 [RFC6605]
+    ECDSAP384SHA384 = 14, // ECDSA Curve P-384 with SHA-384 [RFC6605]
+    ED25519 = 15,         // Ed25519 [RFC8080]
+    Unknown = 255         // Reserved for Private Use
+};
+
+static inline StringView to_string(Algorithm algorithm)
+{
+    switch (algorithm) {
+    case Algorithm::RSAMD5:
+        return "RSAMD5"sv;
+    case Algorithm::DSA:
+        return "DSA"sv;
+    case Algorithm::RSASHA1:
+        return "RSASHA1"sv;
+    case Algorithm::RSASHA1NSEC3SHA1:
+        return "RSASHA1NSEC3SHA1"sv;
+    case Algorithm::RSASHA256:
+        return "RSASHA256"sv;
+    case Algorithm::RSASHA512:
+        return "RSASHA512"sv;
+    case Algorithm::ECDSAP256SHA256:
+        return "ECDSAP256SHA256"sv;
+    case Algorithm::ECDSAP384SHA384:
+        return "ECDSAP384SHA384"sv;
+    case Algorithm::ED25519:
+        return "ED25519"sv;
+    case Algorithm::Unknown:
+        return "Unknown"sv;
+    }
+    VERIFY_NOT_REACHED();
+}
+
+// Listing from IANA https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml.
+enum class DigestType : u8 {
+    SHA1 = 1,     // SHA-1 [RFC3658]
+    SHA256 = 2,   // SHA-256 [RFC4509]
+    GOST3411 = 3, // GOST R 34.11-94 [RFC5933]
+    SHA384 = 4,   // SHA-384 [RFC6605]
+    SHA512 = 5,   // SHA-512 [RFC6605]
+    SHA224 = 6,   // SHA-224 [RFC6605]
+    Unknown = 255 // Reserved for Private Use
+};
+
+static inline StringView to_string(DigestType digest_type)
+{
+    switch (digest_type) {
+    case DigestType::SHA1:
+        return "SHA1"sv;
+    case DigestType::SHA256:
+        return "SHA256"sv;
+    case DigestType::GOST3411:
+        return "GOST3411"sv;
+    case DigestType::SHA384:
+        return "SHA384"sv;
+    case DigestType::SHA512:
+        return "SHA512"sv;
+    case DigestType::SHA224:
+        return "SHA224"sv;
+    case DigestType::Unknown:
+        return "Unknown"sv;
+    }
+    VERIFY_NOT_REACHED();
+}
+
+// Listing from IANA https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml.
+enum class NSEC3HashAlgorithm : u8 {
+    SHA1 = 1,     // [RFC5155]
+    SHA256 = 2,   // [RFC6605]
+    GOST3411 = 3, // [RFC5933]
+    SHA384 = 4,   // [RFC6605]
+    SHA512 = 5,   // [RFC6605]
+    SHA224 = 6,   // [RFC6605]
+    Unknown = 255 // Reserved for Private Use
+};
+
+static inline StringView to_string(NSEC3HashAlgorithm hash_algorithm)
+{
+    switch (hash_algorithm) {
+    case NSEC3HashAlgorithm::SHA1:
+        return "SHA1"sv;
+    case NSEC3HashAlgorithm::SHA256:
+        return "SHA256"sv;
+    case NSEC3HashAlgorithm::GOST3411:
+        return "GOST3411"sv;
+    case NSEC3HashAlgorithm::SHA384:
+        return "SHA384"sv;
+    case NSEC3HashAlgorithm::SHA512:
+        return "SHA512"sv;
+    case NSEC3HashAlgorithm::SHA224:
+        return "SHA224"sv;
+    case NSEC3HashAlgorithm::Unknown:
+        return "Unknown"sv;
+    }
+    VERIFY_NOT_REACHED();
+}
+
+}
+
+struct Question {
+    DomainName name;
+    ResourceType type;
+    Class class_;
+
+    static ErrorOr<Question> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const;
+};
+
+namespace Records {
+
+struct A {
+    IPv4Address address;
+
+    static constexpr ResourceType type = ResourceType::A;
+    static ErrorOr<A> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return address.to_string(); }
+};
+struct AAAA {
+    IPv6Address address;
+
+    static constexpr ResourceType type = ResourceType::AAAA;
+    static ErrorOr<AAAA> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return address.to_string(); }
+};
+struct TXT {
+    ByteString content;
+
+    static constexpr ResourceType type = ResourceType::TXT;
+    static ErrorOr<TXT> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return String::formatted("Text: '{}'", StringView { content }); }
+};
+struct CNAME {
+    DomainName names;
+
+    static constexpr ResourceType type = ResourceType::CNAME;
+    static ErrorOr<CNAME> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return names.to_string(); }
+};
+struct NS {
+    DomainName name;
+
+    static constexpr ResourceType type = ResourceType::NS;
+    static ErrorOr<NS> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return name.to_string(); }
+};
+struct SOA {
+    DomainName mname;
+    DomainName rname;
+    u32 serial;
+    u32 refresh;
+    u32 retry;
+    u32 expire;
+    u32 minimum;
+
+    static constexpr ResourceType type = ResourceType::SOA;
+    static ErrorOr<SOA> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const
+    {
+        return String::formatted("SOA MName: '{}', RName: '{}', Serial: {}, Refresh: {}, Retry: {}, Expire: {}, Minimum: {}", mname.to_string(), rname.to_string(), serial, refresh, retry, expire, minimum);
+    }
+};
+struct MX {
+    u16 preference;
+    DomainName exchange;
+
+    static constexpr ResourceType type = ResourceType::MX;
+    static ErrorOr<MX> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return String::formatted("MX Preference: {}, Exchange: '{}'", preference, exchange.to_string()); }
+};
+struct PTR {
+    DomainName name;
+
+    static constexpr ResourceType type = ResourceType::PTR;
+    static ErrorOr<PTR> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return name.to_string(); }
+};
+struct SRV {
+    u16 priority;
+    u16 weight;
+    u16 port;
+    DomainName target;
+
+    static constexpr ResourceType type = ResourceType::SRV;
+    static ErrorOr<SRV> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return String::formatted("SRV Priority: {}, Weight: {}, Port: {}, Target: '{}'", priority, weight, port, target.to_string()); }
+};
+struct DNSKEY {
+    u16 flags;
+    u8 protocol;
+    DNSSEC::Algorithm algorithm;
+    ByteBuffer public_key;
+
+    constexpr static inline u16 FlagSecureEntryPoint = 0b1000000000000000;
+    constexpr static inline u16 FlagZoneKey = 0b0100000000000000;
+    constexpr static inline u16 FlagRevoked = 0b0010000000000000;
+
+    constexpr bool is_secure_entry_point() const { return flags & FlagSecureEntryPoint; }
+    constexpr bool is_zone_key() const { return flags & FlagZoneKey; }
+    constexpr bool is_revoked() const { return flags & FlagRevoked; }
+    constexpr bool is_key_signing_key() const { return is_secure_entry_point() && is_zone_key() && !is_revoked(); }
+
+    static constexpr ResourceType type = ResourceType::DNSKEY;
+    static ErrorOr<DNSKEY> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const
+    {
+        return String::formatted("DNSKEY Flags: {}{}{}{}({}), Protocol: {}, Algorithm: {}, Public Key: {}",
+            is_secure_entry_point() ? "sep "sv : ""sv,
+            is_zone_key() ? "zone "sv : ""sv,
+            is_revoked() ? "revoked "sv : ""sv,
+            is_key_signing_key() ? "ksk "sv : ""sv,
+            flags,
+            protocol,
+            DNSSEC::to_string(algorithm),
+            TRY(encode_base64(public_key)));
+    }
+};
+struct CDNSKEY : public DNSKEY {
+    template<typename... Ts>
+    CDNSKEY(Ts&&... args)
+        : DNSKEY(forward<Ts>(args)...)
+    {
+    }
+
+    static constexpr ResourceType type = ResourceType::CDNSKEY;
+    static ErrorOr<CDNSKEY> from_raw(ParseContext& raw) { return DNSKEY::from_raw(raw); }
+};
+struct DS {
+    u16 key_tag;
+    DNSSEC::Algorithm algorithm;
+    DNSSEC::DigestType digest_type;
+    ByteBuffer digest;
+
+    static constexpr ResourceType type = ResourceType::DS;
+    static ErrorOr<DS> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return "DS"_string; }
+};
+struct CDS : public DS {
+    template<typename... Ts>
+    CDS(Ts&&... args)
+        : DS(forward<Ts>(args)...)
+    {
+    }
+    static constexpr ResourceType type = ResourceType::CDS;
+    static ErrorOr<CDS> from_raw(ParseContext& raw) { return DS::from_raw(raw); }
+};
+struct SIG {
+    ResourceType type_covered;
+    DNSSEC::Algorithm algorithm;
+    u8 label_count;
+    u32 original_ttl;
+    UnixDateTime expiration;
+    UnixDateTime inception;
+    u16 key_tag;
+    DomainName signers_name;
+    ByteBuffer signature;
+
+    static constexpr ResourceType type = ResourceType::SIG;
+    static ErrorOr<SIG> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const;
+};
+struct RRSIG : public SIG {
+    template<typename... Ts>
+    RRSIG(Ts&&... args)
+        : SIG(forward<Ts>(args)...)
+    {
+    }
+
+    static constexpr ResourceType type = ResourceType::RRSIG;
+    static ErrorOr<RRSIG> from_raw(ParseContext& raw) { return SIG::from_raw(raw); }
+};
+struct NSEC {
+    DomainName next_domain_name;
+    Vector<ResourceType> types;
+
+    static constexpr ResourceType type = ResourceType::NSEC;
+    static ErrorOr<NSEC> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return "NSEC"_string; }
+};
+struct NSEC3 {
+    DNSSEC::NSEC3HashAlgorithm hash_algorithm;
+    u8 flags;
+    u16 iterations;
+    ByteBuffer salt;
+    DomainName next_hashed_owner_name;
+    Vector<ResourceType> types;
+
+    static constexpr ResourceType type = ResourceType::NSEC3;
+    static ErrorOr<NSEC3> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return "NSEC3"_string; }
+};
+struct NSEC3PARAM {
+    DNSSEC::NSEC3HashAlgorithm hash_algorithm;
+    u8 flags;
+    u16 iterations;
+    ByteBuffer salt;
+
+    constexpr static inline u8 FlagOptOut = 0b10000000;
+
+    constexpr bool is_opt_out() const { return flags & FlagOptOut; }
+
+    static constexpr ResourceType type = ResourceType::NSEC3PARAM;
+    static ErrorOr<NSEC3PARAM> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return "NSEC3PARAM"_string; }
+};
+struct TLSA {
+    Messages::TLSA::CertUsage cert_usage;
+    Messages::TLSA::Selector selector;
+    Messages::TLSA::MatchingType matching_type;
+    ByteBuffer certificate_association_data;
+
+    static ErrorOr<TLSA> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return "TLSA"_string; }
+};
+struct HINFO {
+    ByteString cpu;
+    ByteString os;
+
+    static constexpr ResourceType type = ResourceType::HINFO;
+    static ErrorOr<HINFO> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); }
+    ErrorOr<String> to_string() const { return String::formatted("HINFO CPU: '{}', OS: '{}'", StringView { cpu }, StringView { os }); }
+};
+struct OPT {
+    struct Option {
+        u16 code;
+        ByteBuffer data;
+    };
+
+    //                                   1  1  1  1  1  1
+    //     0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
+    //    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+    //    |                UDP Payload Size               |
+    //    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+    //    |     Extended RCode    |    VER    |     ZZ    |
+    //    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+    //    |DO|                  Z                         |
+    //    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
+    //    |  OPT-LEN  / OPT-DATA...
+
+    NetworkOrdered<u16> udp_payload_size { 0 };
+    NetworkOrdered<u32> extended_rcode_and_flags { 0 };
+    Vector<Option> options;
+    static constexpr u32 MaskExtendedRCode = 0b11111111000000000000000000000000;
+    static constexpr u32 MaskVersion = 0b00000000111100000000000000000000;
+    static constexpr u32 MaskDO = 0b00000000000000001000000000000000;
+
+    static constexpr ResourceType type = ResourceType::OPT;
+
+    constexpr u8 extended_rcode() const { return (extended_rcode_and_flags & MaskExtendedRCode) >> 24; }
+    constexpr u8 version() const { return (extended_rcode_and_flags & MaskVersion) >> 20; }
+    constexpr bool dnssec_ok() const { return extended_rcode_and_flags & MaskDO; }
+
+    void set_extended_rcode(u8 value) { extended_rcode_and_flags = (extended_rcode_and_flags & ~MaskExtendedRCode) | (value << 24); }
+    void set_version(u8 value) { extended_rcode_and_flags = (extended_rcode_and_flags & ~MaskVersion) | (value << 20); }
+    void set_dnssec_ok(bool value) { extended_rcode_and_flags = (extended_rcode_and_flags & ~MaskDO) | (value ? MaskDO : 0); }
+
+    static ErrorOr<OPT> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const;
+    ErrorOr<String> to_string() const
+    {
+        StringBuilder builder;
+        builder.appendff("OPT UDP Payload Size: {}, Extended RCode: {}, Version: {}, DNSSEC OK: {}", udp_payload_size, extended_rcode(), version(), dnssec_ok());
+        for (auto& option : options)
+            builder.appendff(", opt[{} = '{:hex-dump}']", option.code, option.data.bytes());
+
+        return builder.to_string();
+    }
+};
+
+}
+
+using Record = Variant<
+    Records::A,
+    Records::AAAA,
+    Records::TXT,
+    Records::CNAME,
+    Records::NS,
+    Records::SOA,
+    Records::MX,
+    Records::PTR,
+    Records::SRV,
+    Records::DNSKEY,
+    Records::CDNSKEY,
+    Records::DS,
+    Records::CDS,
+    Records::RRSIG,
+    Records::NSEC,
+    Records::NSEC3,
+    Records::NSEC3PARAM,
+    Records::TLSA,
+    Records::HINFO,
+    Records::OPT,
+    // TODO: Add more records.
+    ByteBuffer>; // Fallback for unknown records.
+
+struct ResourceRecord {
+    DomainName name;
+    ResourceType type;
+    Class class_;
+    u32 ttl;
+    Record record;
+    Optional<ByteBuffer> raw;
+
+    static ErrorOr<ResourceRecord> from_raw(ParseContext&);
+    ErrorOr<void> to_raw(ByteBuffer&) const;
+    ErrorOr<String> to_string() const;
+};
+
+struct ZoneAuthority {
+    DomainName name;
+    ByteString admin_mailbox;
+    u32 serial;
+    u32 refresh;
+    u32 retry;
+    u32 expire;
+    u32 minimum_ttl;
+};
+
+struct Message {
+    Header header;
+    Vector<Question> questions;
+    Vector<ResourceRecord> answers;
+    Vector<ResourceRecord> authorities;
+    Vector<ResourceRecord> additional_records;
+
+    static ErrorOr<Message> from_raw(ParseContext&);
+    static ErrorOr<Message> from_raw(Stream&);
+    ErrorOr<size_t> to_raw(ByteBuffer&) const;
+
+    ErrorOr<String> format_for_log() const;
+};
+
+}
+
+}

+ 404 - 0
Libraries/LibDNS/Resolver.h

@@ -0,0 +1,404 @@
+/*
+ * Copyright (c) 2024, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/AtomicRefCounted.h>
+#include <AK/HashTable.h>
+#include <AK/MemoryStream.h>
+#include <AK/Random.h>
+#include <AK/StringView.h>
+#include <AK/TemporaryChange.h>
+#include <LibCore/Promise.h>
+#include <LibCore/SocketAddress.h>
+#include <LibDNS/Message.h>
+#include <LibThreading/MutexProtected.h>
+#include <LibThreading/RWLockProtected.h>
+
+namespace DNS {
+class Resolver;
+
+class LookupResult : public AtomicRefCounted<LookupResult>
+    , public Weakable<LookupResult> {
+public:
+    explicit LookupResult(Messages::DomainName name)
+        : m_name(move(name))
+    {
+    }
+
+    Vector<Variant<IPv4Address, IPv6Address>> cached_addresses() const
+    {
+        Vector<Variant<IPv4Address, IPv6Address>> result;
+        for (auto& record : m_cached_records) {
+            record.record.visit(
+                [&](Messages::Records::A const& a) { result.append(a.address); },
+                [&](Messages::Records::AAAA const& aaaa) { result.append(aaaa.address); },
+                [](auto&) {});
+        }
+        return result;
+    }
+
+    void add_record(Messages::ResourceRecord record)
+    {
+        m_valid = true;
+        m_cached_records.append(move(record));
+    }
+
+    Vector<Messages::ResourceRecord> const& records() const { return m_cached_records; }
+
+    bool has_record_of_type(Messages::ResourceType type, bool later = false) const
+    {
+        if (later && m_desired_types.contains(type))
+            return true;
+
+        for (auto const& record : m_cached_records) {
+            if (record.type == type)
+                return true;
+        }
+        return false;
+    }
+
+    void will_add_record_of_type(Messages::ResourceType type) { m_desired_types.set(type); }
+
+    void set_id(u16 id) { m_id = id; }
+    u16 id() { return m_id; }
+
+    bool is_valid() const { return m_valid; }
+    Messages::DomainName const& name() const { return m_name; }
+
+private:
+    bool m_valid { false };
+    Messages::DomainName m_name;
+    Vector<Messages::ResourceRecord> m_cached_records;
+    HashTable<Messages::ResourceType> m_desired_types;
+    u16 m_id { 0 };
+};
+
+class Resolver {
+public:
+    enum class ConnectionMode {
+        TCP,
+        UDP,
+    };
+
+    struct SocketResult {
+        MaybeOwned<Core::Socket> socket;
+        ConnectionMode mode;
+    };
+
+    Resolver(Function<ErrorOr<SocketResult>()> create_socket)
+        : m_pending_lookups(make<RedBlackTree<u16, PendingLookup>>())
+        , m_create_socket(move(create_socket))
+    {
+    }
+
+    NonnullRefPtr<Core::Promise<Empty>> when_socket_ready()
+    {
+        auto promise = Core::Promise<Empty>::construct();
+        m_socket_ready_promises.append(promise);
+        if (has_connection(false)) {
+            promise->resolve({});
+            return promise;
+        }
+
+        if (!has_connection())
+            promise->reject(Error::from_string_literal("Failed to create socket"));
+
+        return promise;
+    }
+
+    void reset_connection()
+    {
+        m_socket.with_write_locked([&](auto& socket) { socket = {}; });
+    }
+
+    NonnullRefPtr<LookupResult const> expect_cached(StringView name, Messages::Class class_ = Messages::Class::IN)
+    {
+        return expect_cached(name, class_, Array { Messages::ResourceType::A, Messages::ResourceType::AAAA });
+    }
+
+    NonnullRefPtr<LookupResult const> expect_cached(StringView name, Messages::Class class_, Span<Messages::ResourceType const> desired_types)
+    {
+        auto result = lookup_in_cache(name, class_, desired_types);
+        VERIFY(!result.is_null());
+        dbgln("DNS::expect({}) -> OK", name);
+        return *result;
+    }
+
+    RefPtr<LookupResult const> lookup_in_cache(StringView name, Messages::Class class_ = Messages::Class::IN)
+    {
+        return lookup_in_cache(name, class_, Array { Messages::ResourceType::A, Messages::ResourceType::AAAA });
+    }
+
+    RefPtr<LookupResult const> lookup_in_cache(StringView name, Messages::Class, Span<Messages::ResourceType const> desired_types)
+    {
+        return m_cache.with_read_locked([&](auto& cache) -> RefPtr<LookupResult const> {
+            auto it = cache.find(name);
+            if (it == cache.end())
+                return {};
+
+            auto& result = *it->value;
+            for (auto const& type : desired_types) {
+                if (!result.has_record_of_type(type))
+                    return {};
+            }
+
+            return result;
+        });
+    }
+
+    NonnullRefPtr<Core::Promise<NonnullRefPtr<LookupResult const>>> lookup(ByteString name, Messages::Class class_ = Messages::Class::IN)
+    {
+        return lookup(move(name), class_, Array { Messages::ResourceType::A, Messages::ResourceType::AAAA });
+    }
+
+    NonnullRefPtr<Core::Promise<NonnullRefPtr<LookupResult const>>> lookup(ByteString name, Messages::Class class_, Span<Messages::ResourceType const> desired_types)
+    {
+        auto promise = Core::Promise<NonnullRefPtr<LookupResult const>>::construct();
+
+        if (auto result = lookup_in_cache(name, class_, desired_types)) {
+            promise->resolve(result.release_nonnull());
+            return promise;
+        }
+
+        auto domain_name = Messages::DomainName::from_string(name);
+
+        if (!has_connection()) {
+            // Use system resolver
+            // FIXME: Use an underlying resolver instead.
+            dbgln("Not ready to resolve, using system resolver and skipping cache for {}", name);
+            auto record_or_error = Core::Socket::resolve_host(name, Core::Socket::SocketType::Stream);
+            if (record_or_error.is_error()) {
+                promise->reject(record_or_error.release_error());
+                return promise;
+            }
+            auto result = make_ref_counted<LookupResult>(domain_name);
+            auto record = record_or_error.release_value();
+            record.visit(
+                [&](IPv4Address const& address) {
+                    result->add_record({ .name = {}, .type = Messages::ResourceType::A, .class_ = Messages::Class::IN, .ttl = 0, .record = Messages::Records::A { address }, .raw = {} });
+                },
+                [&](IPv6Address const& address) {
+                    result->add_record({ .name = {}, .type = Messages::ResourceType::AAAA, .class_ = Messages::Class::IN, .ttl = 0, .record = Messages::Records::AAAA { address }, .raw = {} });
+                });
+            promise->resolve(result);
+            return promise;
+        }
+
+        auto already_in_cache = false;
+        auto result = m_cache.with_write_locked([&](auto& cache) -> NonnullRefPtr<LookupResult> {
+            auto existing = [&] -> RefPtr<LookupResult> {
+                if (cache.contains(name)) {
+                    auto ptr = *cache.get(name);
+
+                    already_in_cache = true;
+                    for (auto const& type : desired_types) {
+                        if (!ptr->has_record_of_type(type, true)) {
+                            already_in_cache = false;
+                            break;
+                        }
+                    }
+
+                    return ptr;
+                }
+                return nullptr;
+            }();
+
+            if (existing)
+                return *existing;
+
+            auto ptr = make_ref_counted<LookupResult>(domain_name);
+            for (auto const& type : desired_types)
+                ptr->will_add_record_of_type(type);
+            cache.set(name, ptr);
+            return ptr;
+        });
+
+        Optional<u16> cached_result_id;
+        if (already_in_cache) {
+            auto id = result->id();
+            cached_result_id = id;
+            auto existing_promise = m_pending_lookups.with_write_locked([&](auto& lookups) -> RefPtr<Core::Promise<NonnullRefPtr<LookupResult const>>> {
+                if (auto* lookup = lookups->find(id))
+                    return lookup->promise;
+                return nullptr;
+            });
+            if (existing_promise)
+                return existing_promise.release_nonnull();
+
+            promise->resolve(*result);
+            return promise;
+        }
+
+        Messages::Message query;
+        m_pending_lookups.with_read_locked([&](auto& lookups) {
+            do
+                fill_with_random({ &query.header.id, sizeof(query.header.id) });
+            while (lookups->find(query.header.id) != nullptr);
+        });
+        query.header.question_count = max(1u, desired_types.size());
+        query.header.options.set_response_code(Messages::Options::ResponseCode::NoError);
+        query.header.options.set_recursion_desired(true);
+        query.header.options.set_op_code(Messages::OpCode::Query);
+        for (auto const& type : desired_types) {
+            query.questions.append(Messages::Question {
+                .name = domain_name,
+                .type = type,
+                .class_ = class_,
+            });
+        }
+
+        if (query.questions.is_empty()) {
+            query.questions.append(Messages::Question {
+                .name = Messages::DomainName::from_string(name),
+                .type = Messages::ResourceType::A,
+                .class_ = class_,
+            });
+        }
+
+        auto cached_entry = m_pending_lookups.with_write_locked([&](auto& pending_lookups) -> RefPtr<Core::Promise<NonnullRefPtr<LookupResult const>>> {
+            // One more try to make sure we're not overwriting an existing lookup
+            if (cached_result_id.has_value()) {
+                if (auto* lookup = pending_lookups->find(*cached_result_id))
+                    return lookup->promise;
+            }
+
+            pending_lookups->insert(query.header.id, { query.header.id, name, result->make_weak_ptr(), promise });
+            return nullptr;
+        });
+        if (cached_entry) {
+            dbgln("DNS::lookup({}) -> Already in cache", name);
+            return cached_entry.release_nonnull();
+        }
+
+        ByteBuffer query_bytes;
+        MUST(query.to_raw(query_bytes));
+
+        if (m_mode == ConnectionMode::TCP) {
+            auto original_query_bytes = query_bytes;
+            query_bytes = MUST(ByteBuffer::create_uninitialized(query_bytes.size() + sizeof(u16)));
+            NetworkOrdered<u16> size = original_query_bytes.size();
+            query_bytes.overwrite(0, &size, sizeof(size));
+            query_bytes.overwrite(sizeof(size), original_query_bytes.data(), original_query_bytes.size());
+        }
+
+        auto write_result = m_socket.with_write_locked([&](auto& socket) {
+            return (*socket)->write_until_depleted(query_bytes.bytes());
+        });
+        if (write_result.is_error()) {
+            promise->reject(write_result.release_error());
+            return promise;
+        }
+
+        return promise;
+    }
+
+private:
+    struct PendingLookup {
+        u16 id { 0 };
+        ByteString name;
+        WeakPtr<LookupResult> result;
+        NonnullRefPtr<Core::Promise<NonnullRefPtr<LookupResult const>>> promise;
+    };
+
+    ErrorOr<Messages::Message> parse_one_message()
+    {
+        if (m_mode == ConnectionMode::UDP)
+            return m_socket.with_write_locked([&](auto& socket) { return Messages::Message::from_raw(**socket); });
+
+        return m_socket.with_write_locked([&](auto& socket) -> ErrorOr<Messages::Message> {
+            if (!TRY((*socket)->can_read_without_blocking()))
+                return Error::from_errno(EAGAIN);
+
+            auto size = TRY((*socket)->template read_value<NetworkOrdered<u16>>());
+            auto buffer = TRY(ByteBuffer::create_uninitialized(size));
+            TRY((*socket)->read_until_filled(buffer));
+            FixedMemoryStream stream { static_cast<ReadonlyBytes>(buffer) };
+            return Messages::Message::from_raw(stream);
+        });
+    }
+
+    void process_incoming_messages()
+    {
+        while (true) {
+            if (auto result = m_socket.with_read_locked([](auto& socket) { return (*socket)->can_read_without_blocking(); }); result.is_error() || !result.value())
+                break;
+            auto message_or_err = parse_one_message();
+            if (message_or_err.is_error()) {
+                if (!message_or_err.error().is_errno() || message_or_err.error().code() != EAGAIN)
+                    dbgln("Failed to receive message: {}", message_or_err.error());
+                break;
+            }
+
+            auto message = message_or_err.release_value();
+            auto result = m_pending_lookups.with_write_locked([&](auto& lookups) -> ErrorOr<void> {
+                auto* lookup = lookups->find(message.header.id);
+                if (!lookup)
+                    return Error::from_string_literal("No pending lookup found for this message");
+
+                auto result = lookup->result.strong_ref();
+                for (auto& record : message.answers)
+                    result->add_record(move(record));
+
+                lookup->promise->resolve(*result);
+                lookups->remove(message.header.id);
+                return {};
+            });
+            if (result.is_error()) {
+                dbgln("Received a message with no pending lookup: {}", result.error());
+                continue;
+            }
+        }
+    }
+
+    bool has_connection(bool attempt_restart = true)
+    {
+        auto result = m_socket.with_read_locked(
+            [&](auto& socket) { return socket.has_value() && (*socket)->is_open(); });
+
+        if (attempt_restart && !result && !m_attempting_restart) {
+            TemporaryChange change(m_attempting_restart, true);
+            auto create_result = m_create_socket();
+            if (create_result.is_error()) {
+                dbgln("Failed to create socket: {}", create_result.error());
+                return false;
+            }
+
+            auto [socket, mode] = MUST(move(create_result));
+            set_socket(move(socket), mode);
+            result = true;
+        }
+
+        return result;
+    }
+
+    void set_socket(MaybeOwned<Core::Socket> socket, ConnectionMode mode = ConnectionMode::UDP)
+    {
+        m_mode = mode;
+        m_socket.with_write_locked([&](auto& s) {
+            s = move(socket);
+            (*s)->on_ready_to_read = [this] {
+                process_incoming_messages();
+            };
+            (*s)->set_notifications_enabled(true);
+        });
+
+        for (auto& promise : m_socket_ready_promises)
+            promise->resolve({});
+
+        m_socket_ready_promises.clear();
+    }
+
+    Threading::RWLockProtected<HashMap<ByteString, NonnullRefPtr<LookupResult>>> m_cache;
+    Threading::RWLockProtected<NonnullOwnPtr<RedBlackTree<u16, PendingLookup>>> m_pending_lookups;
+    Threading::RWLockProtected<Optional<MaybeOwned<Core::Socket>>> m_socket;
+    Function<ErrorOr<SocketResult>()> m_create_socket;
+    bool m_attempting_restart { false };
+    ConnectionMode m_mode { ConnectionMode::UDP };
+    Vector<NonnullRefPtr<Core::Promise<Empty>>> m_socket_ready_promises;
+};
+
+}

+ 14 - 0
Libraries/LibWebView/Application.cpp

@@ -72,6 +72,9 @@ void Application::initialize(Main::Arguments const& arguments, URL::URL new_tab_
     Optional<StringView> profile_process;
     Optional<StringView> webdriver_content_ipc_path;
     Optional<StringView> user_agent_preset;
+    Optional<StringView> dns_server_address;
+    Optional<u16> dns_server_port;
+    bool use_dns_over_tls = false;
     bool log_all_js_exceptions = false;
     bool enable_idl_tracing = false;
     bool enable_http_cache = false;
@@ -101,6 +104,9 @@ void Application::initialize(Main::Arguments const& arguments, URL::URL new_tab_
     args_parser.add_option(force_cpu_painting, "Force CPU painting", "force-cpu-painting");
     args_parser.add_option(force_fontconfig, "Force using fontconfig for font loading", "force-fontconfig");
     args_parser.add_option(collect_garbage_on_every_allocation, "Collect garbage after every JS heap allocation", "collect-garbage-on-every-allocation", 'g');
+    args_parser.add_option(dns_server_address, "Set the DNS server address", "dns-server", 0, "host|address");
+    args_parser.add_option(dns_server_port, "Set the DNS server port", "dns-port", 0, "port (default: 53 or 853 if --dot)");
+    args_parser.add_option(use_dns_over_tls, "Use DNS over TLS", "dot");
     args_parser.add_option(Core::ArgsParser::Option {
         .argument_mode = Core::ArgsParser::OptionArgumentMode::Required,
         .help_string = "Name of the User-Agent preset to use in place of the default User-Agent",
@@ -120,6 +126,9 @@ void Application::initialize(Main::Arguments const& arguments, URL::URL new_tab_
     if (force_new_process)
         disable_sql_database = true;
 
+    if (!dns_server_port.has_value())
+        dns_server_port = use_dns_over_tls ? 853 : 53;
+
     Optional<ProcessType> debug_process_type;
     Optional<ProcessType> profile_process_type;
 
@@ -140,6 +149,11 @@ void Application::initialize(Main::Arguments const& arguments, URL::URL new_tab_
         .disable_sql_database = disable_sql_database ? DisableSQLDatabase::Yes : DisableSQLDatabase::No,
         .debug_helper_process = move(debug_process_type),
         .profile_helper_process = move(profile_process_type),
+        .dns_settings = (dns_server_address.has_value()
+                ? (use_dns_over_tls
+                          ? DNSSettings(DNSOverTLS(dns_server_address.release_value(), *dns_server_port))
+                          : DNSSettings(DNSOverUDP(dns_server_address.release_value(), *dns_server_port)))
+                : SystemDNS {}),
     };
 
     if (webdriver_content_ipc_path.has_value())

+ 13 - 1
Libraries/LibWebView/HelperProcess.cpp

@@ -163,7 +163,19 @@ ErrorOr<NonnullRefPtr<Requests::RequestClient>> launch_request_server_process()
         arguments.append(server.value());
     }
 
-    return launch_server_process<Requests::RequestClient>("RequestServer"sv, move(arguments));
+    auto client = TRY(launch_server_process<Requests::RequestClient>("RequestServer"sv, move(arguments)));
+    WebView::Application::chrome_options().dns_settings.visit(
+        [](WebView::SystemDNS) {},
+        [&](WebView::DNSOverTLS const& dns_over_tls) {
+            dbgln("Setting DNS server to {}:{} with TLS", dns_over_tls.server_address, dns_over_tls.port);
+            client->async_set_dns_server(dns_over_tls.server_address, dns_over_tls.port, true);
+        },
+        [&](WebView::DNSOverUDP const& dns_over_udp) {
+            dbgln("Setting DNS server to {}:{}", dns_over_udp.server_address, dns_over_udp.port);
+            client->async_set_dns_server(dns_over_udp.server_address, dns_over_udp.port, false);
+        });
+
+    return client;
 }
 
 ErrorOr<IPC::File> connect_new_request_server_client()

+ 13 - 0
Libraries/LibWebView/Options.h

@@ -45,6 +45,18 @@ enum class EnableAutoplay {
     Yes,
 };
 
+struct SystemDNS { };
+struct DNSOverTLS {
+    ByteString server_address;
+    u16 port;
+};
+struct DNSOverUDP {
+    ByteString server_address;
+    u16 port;
+};
+
+using DNSSettings = Variant<SystemDNS, DNSOverTLS, DNSOverUDP>;
+
 struct ChromeOptions {
     Vector<URL::URL> urls;
     Vector<ByteString> raw_urls;
@@ -58,6 +70,7 @@ struct ChromeOptions {
     Optional<ProcessType> debug_helper_process {};
     Optional<ProcessType> profile_helper_process {};
     Optional<ByteString> webdriver_content_ipc_path {};
+    DNSSettings dns_settings { SystemDNS {} };
 };
 
 enum class IsLayoutTestMode {

+ 3 - 0
Meta/Lagom/CMakeLists.txt

@@ -382,6 +382,7 @@ set(lagom_standard_libraries
     Compress
     Crypto
     Diff
+    DNS
     GC
     HTTP
     IPC
@@ -448,6 +449,8 @@ endif()
 # Lagom Utilities
 lagom_utility(abench SOURCES ../../Utilities/abench.cpp LIBS LibMain LibFileSystem LibMedia)
 
+lagom_utility(dns SOURCES ../../Utilities/dns.cpp LIBS LibDNS LibMain LibTLS LibCrypto)
+
 if (ENABLE_GUI_TARGETS)
     lagom_utility(animation SOURCES ../../Utilities/animation.cpp LIBS LibGfx LibMain)
     lagom_utility(icc SOURCES ../../Utilities/icc.cpp LIBS LibGfx LibMain LibURL)

+ 1 - 1
Services/RequestServer/CMakeLists.txt

@@ -25,7 +25,7 @@ target_include_directories(requestserverservice PRIVATE ${CMAKE_CURRENT_BINARY_D
 target_include_directories(requestserverservice PRIVATE ${LADYBIRD_SOURCE_DIR}/Services/)
 
 target_link_libraries(RequestServer PRIVATE requestserverservice)
-target_link_libraries(requestserverservice PUBLIC LibCore LibMain LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebView LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl)
+target_link_libraries(requestserverservice PUBLIC LibCore LibDNS LibMain LibCrypto LibFileSystem LibIPC LibMain LibTLS LibWebView LibWebSocket LibURL LibTextCodec LibThreading CURL::libcurl)
 
 if (${CMAKE_SYSTEM_NAME} MATCHES "SunOS")
     # Solaris has socket and networking related functions in two extra libraries

+ 103 - 0
Services/RequestServer/ConnectionFromClient.cpp

@@ -24,6 +24,47 @@ ByteString g_default_certificate_path;
 static HashMap<int, RefPtr<ConnectionFromClient>> s_connections;
 static IDAllocator s_client_ids;
 static long s_connect_timeout_seconds = 90L;
+static HashMap<ByteString, ByteString> g_dns_cache; // host -> curl "resolve" string
+static struct {
+    Optional<Core::SocketAddress> server_address;
+    Optional<ByteString> server_hostname;
+    u16 port;
+    bool use_dns_over_tls = true;
+} g_dns_info;
+
+static WeakPtr<Resolver> s_resolver {};
+static NonnullRefPtr<Resolver> default_resolver()
+{
+    if (auto resolver = s_resolver.strong_ref())
+        return *resolver;
+    auto resolver = make_ref_counted<Resolver>([] -> ErrorOr<DNS::Resolver::SocketResult> {
+        if (!g_dns_info.server_address.has_value()) {
+            if (!g_dns_info.server_hostname.has_value())
+                return Error::from_string_literal("No DNS server configured");
+
+            auto resolved = TRY(default_resolver()->dns.lookup(*g_dns_info.server_hostname)->await());
+            if (resolved->cached_addresses().is_empty())
+                return Error::from_string_literal("Failed to resolve DNS server hostname");
+            auto address = resolved->cached_addresses().first().visit([](auto& addr) -> Core::SocketAddress { return { addr, g_dns_info.port }; });
+            g_dns_info.server_address = address;
+        }
+
+        if (g_dns_info.use_dns_over_tls) {
+            return DNS::Resolver::SocketResult {
+                MaybeOwned<Core::Socket>(TRY(TLS::TLSv12::connect(*g_dns_info.server_address, *g_dns_info.server_hostname))),
+                DNS::Resolver::ConnectionMode::TCP,
+            };
+        }
+
+        return DNS::Resolver::SocketResult {
+            MaybeOwned<Core::Socket>(TRY(Core::BufferedSocket<Core::UDPSocket>::create(TRY(Core::UDPSocket::connect(*g_dns_info.server_address))))),
+            DNS::Resolver::ConnectionMode::UDP,
+        };
+    });
+
+    s_resolver = resolver;
+    return resolver;
+}
 
 struct ConnectionFromClient::ActiveRequest {
     CURLM* multi { nullptr };
@@ -199,6 +240,7 @@ int ConnectionFromClient::on_timeout_callback(void*, long timeout_ms, void* user
 
 ConnectionFromClient::ConnectionFromClient(IPC::Transport transport)
     : IPC::ConnectionFromClient<RequestClientEndpoint, RequestServerEndpoint>(*this, move(transport), s_client_ids.allocate())
+    , m_resolver(default_resolver())
 {
     s_connections.set(client_id(), *this);
 
@@ -264,6 +306,33 @@ Messages::RequestServer::IsSupportedProtocolResponse ConnectionFromClient::is_su
     return protocol == "http"sv || protocol == "https"sv;
 }
 
+void ConnectionFromClient::set_dns_server(ByteString const& host_or_address, u16 port, bool use_tls)
+{
+    if (host_or_address == g_dns_info.server_hostname && port == g_dns_info.port && use_tls == g_dns_info.use_dns_over_tls)
+        return;
+
+    auto result = [&] -> ErrorOr<void> {
+        Core::SocketAddress addr;
+        if (auto v4 = IPv4Address::from_string(host_or_address); v4.has_value())
+            addr = { v4.value(), port };
+        else if (auto v6 = IPv6Address::from_string(host_or_address); v6.has_value())
+            addr = { v6.value(), port };
+        else
+            TRY(default_resolver()->dns.lookup(host_or_address)->await())->cached_addresses().first().visit([&](auto& address) { addr = { address, port }; });
+
+        g_dns_info.server_address = addr;
+        g_dns_info.server_hostname = host_or_address;
+        g_dns_info.port = port;
+        g_dns_info.use_dns_over_tls = use_tls;
+        return {};
+    }();
+
+    if (result.is_error())
+        dbgln("Failed to set DNS server: {}", result.error());
+    else
+        default_resolver()->dns.reset_connection();
+}
+
 void ConnectionFromClient::start_request(i32 request_id, ByteString const& method, URL::URL const& url, HTTP::HeaderMap const& request_headers, ByteBuffer const& request_body, Core::ProxyData const& proxy_data)
 {
     if (!url.is_valid()) {
@@ -272,6 +341,22 @@ void ConnectionFromClient::start_request(i32 request_id, ByteString const& metho
         return;
     }
 
+    auto host = url.serialized_host().value().to_byte_string();
+    auto dns_promise = m_resolver->dns.lookup(host, DNS::Messages::Class::IN, Array { DNS::Messages::ResourceType::A, DNS::Messages::ResourceType::AAAA }.span());
+    auto resolve_result = dns_promise->await();
+    if (resolve_result.is_error()) {
+        dbgln("StartRequest: DNS lookup failed for '{}': {}", host, resolve_result.error());
+        async_request_finished(request_id, 0, Requests::NetworkError::UnableToResolveHost);
+        return;
+    }
+
+    auto dns_result = resolve_result.release_value();
+    if (dns_result->records().is_empty()) {
+        dbgln("StartRequest: DNS lookup failed for '{}'", host);
+        async_request_finished(request_id, 0, Requests::NetworkError::UnableToResolveHost);
+        return;
+    }
+
     auto* easy = curl_easy_init();
     if (!easy) {
         dbgln("StartRequest: Failed to initialize curl easy handle");
@@ -349,6 +434,24 @@ void ConnectionFromClient::start_request(i32 request_id, ByteString const& metho
     set_option(CURLOPT_HEADERFUNCTION, &on_header_received);
     set_option(CURLOPT_HEADERDATA, reinterpret_cast<void*>(request.ptr()));
 
+    StringBuilder resolve_opt_builder;
+    resolve_opt_builder.appendff("{}:{}:", host, url.port_or_default());
+    auto first = true;
+    for (auto& addr : dns_result->cached_addresses()) {
+        auto formatted_address = addr.visit(
+            [&](IPv4Address const& ipv4) { return ipv4.to_byte_string(); },
+            [&](IPv6Address const& ipv6) { return MUST(ipv6.to_string()).to_byte_string(); });
+        if (!first)
+            resolve_opt_builder.append(',');
+        first = false;
+        resolve_opt_builder.append(formatted_address);
+    }
+
+    auto formatted_address = resolve_opt_builder.to_byte_string();
+    g_dns_cache.set(host, formatted_address);
+    curl_slist* resolve_list = curl_slist_append(nullptr, formatted_address.characters());
+    curl_easy_setopt(easy, CURLOPT_RESOLVE, resolve_list);
+
     auto result = curl_multi_add_handle(m_curl_multi, easy);
     VERIFY(result == CURLM_OK);
 

+ 13 - 0
Services/RequestServer/ConnectionFromClient.h

@@ -7,6 +7,7 @@
 #pragma once
 
 #include <AK/HashMap.h>
+#include <LibDNS/Resolver.h>
 #include <LibIPC/ConnectionFromClient.h>
 #include <LibWebSocket/WebSocket.h>
 #include <RequestServer/Forward.h>
@@ -15,6 +16,16 @@
 
 namespace RequestServer {
 
+struct Resolver : public RefCounted<Resolver>
+    , Weakable<Resolver> {
+    Resolver(Function<ErrorOr<DNS::Resolver::SocketResult>()> create_socket)
+        : dns(move(create_socket))
+    {
+    }
+
+    DNS::Resolver dns;
+};
+
 class ConnectionFromClient final
     : public IPC::ConnectionFromClient<RequestClientEndpoint, RequestServerEndpoint> {
     C_OBJECT(ConnectionFromClient);
@@ -34,6 +45,7 @@ private:
 
     virtual Messages::RequestServer::ConnectNewClientResponse connect_new_client() override;
     virtual Messages::RequestServer::IsSupportedProtocolResponse is_supported_protocol(ByteString const&) override;
+    virtual void set_dns_server(ByteString const& host_or_address, u16 port, bool use_tls) override;
     virtual void start_request(i32 request_id, ByteString const&, URL::URL const&, HTTP::HeaderMap const&, ByteBuffer const&, Core::ProxyData const&) override;
     virtual Messages::RequestServer::StopRequestResponse stop_request(i32) override;
     virtual Messages::RequestServer::SetCertificateResponse set_certificate(i32, ByteString const&, ByteString const&) override;
@@ -61,6 +73,7 @@ private:
     RefPtr<Core::Timer> m_timer;
     HashMap<int, NonnullRefPtr<Core::Notifier>> m_read_notifiers;
     HashMap<int, NonnullRefPtr<Core::Notifier>> m_write_notifiers;
+    NonnullRefPtr<Resolver> m_resolver;
 };
 
 }

+ 3 - 0
Services/RequestServer/RequestServer.ipc

@@ -7,6 +7,9 @@ endpoint RequestServer
 {
     connect_new_client() => (IPC::File client_socket)
 
+    // use_tls: enable DNS over TLS
+    set_dns_server(ByteString host_or_address, u16 port, bool use_tls) =|
+
     // Test if a specific protocol is supported, e.g "http"
     is_supported_protocol(ByteString protocol) => (bool supported)
 

+ 132 - 0
Utilities/dns.cpp

@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2024, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibCore/ArgsParser.h>
+#include <LibCore/EventLoop.h>
+#include <LibCore/Socket.h>
+#include <LibDNS/Resolver.h>
+#include <LibMain/Main.h>
+#include <LibTLS/TLSv12.h>
+
+ErrorOr<int> serenity_main(Main::Arguments arguments)
+{
+    struct Request {
+        Vector<DNS::Messages::ResourceType> types;
+        ByteString name;
+    };
+    Vector<Request> requests;
+    Request current_request;
+    StringView server_address;
+    StringView cert_path;
+    bool use_tls = false;
+
+    Core::ArgsParser args_parser;
+    args_parser.add_option(cert_path, "Path to the CA certificate file", "ca-certs", 'C', "file");
+    args_parser.add_option(server_address, "The address of the DNS server to query", "server", 's', "addr");
+    args_parser.add_option(use_tls, "Use TLS to connect to the server", "tls", 0);
+    args_parser.add_positional_argument(Core::ArgsParser::Arg {
+        .help_string = "The resource types and name of the DNS record to query",
+        .name = "rr,rr@name",
+        .min_values = 1,
+        .max_values = 99999,
+        .accept_value = [&](StringView name) -> ErrorOr<bool> {
+            auto parts = name.split_view('@');
+            if (parts.size() > 2 || parts.is_empty())
+                return Error::from_string_literal("Invalid record/name format");
+
+            if (parts.size() == 1) {
+                current_request.types.append(DNS::Messages::ResourceType::ANY);
+                current_request.name = parts[0];
+            } else {
+                auto rr_parts = parts[0].split_view(',');
+                for (auto& rr : rr_parts) {
+                    ByteString rr_name = rr;
+                    auto type = DNS::Messages::resource_type_from_string(rr_name.to_uppercase());
+                    if (!type.has_value())
+                        return Error::from_string_literal("Invalid resource type");
+                    current_request.types.append(type.value());
+                }
+                current_request.name = parts[1];
+            }
+            requests.append(move(current_request));
+            current_request = {};
+            return true;
+        },
+    });
+
+    args_parser.parse(arguments);
+
+    if (server_address.is_empty()) {
+        outln("You must specify a server address to query");
+        return 1;
+    }
+
+    if (current_request.name.is_empty() && !current_request.types.is_empty()) {
+        outln("You must specify at least one DNS record to query");
+        return 1;
+    }
+
+    Core::EventLoop loop;
+
+    DNS::Resolver resolver {
+        [&] -> ErrorOr<DNS::Resolver::SocketResult> {
+            Core::SocketAddress addr;
+            if (auto v4 = IPv4Address::from_string(server_address); v4.has_value()) {
+                addr = { v4.value(), static_cast<u16>(use_tls ? 853 : 53) };
+            } else if (auto v6 = IPv6Address::from_string(server_address); v6.has_value()) {
+                addr = { v6.value(), static_cast<u16>(use_tls ? 853 : 53) };
+            } else {
+                return MUST(resolver.lookup(server_address)->await())->cached_addresses().first().visit([&](auto& address) -> DNS::Resolver::SocketResult {
+                    if (use_tls) {
+                        auto tls = MUST(TLS::TLSv12::connect({ address, 853 }, server_address));
+                        return { move(tls), DNS::Resolver::ConnectionMode::TCP };
+                    }
+                    return { MUST(Core::BufferedSocket<Core::UDPSocket>::create(MUST(Core::UDPSocket::connect({ address, 53 })))), DNS::Resolver::ConnectionMode::UDP };
+                });
+            }
+
+            if (use_tls)
+                return DNS::Resolver::SocketResult { MUST(TLS::TLSv12::connect(addr, server_address)), DNS::Resolver::ConnectionMode::TCP };
+            return DNS::Resolver::SocketResult { MUST(Core::BufferedSocket<Core::UDPSocket>::create(MUST(Core::UDPSocket::connect(addr)))), DNS::Resolver::ConnectionMode::UDP };
+        }
+    };
+
+    DefaultRootCACertificates::set_default_certificate_paths(Array<ByteString, 1> { cert_path.is_empty() ? "/etc/ssl/cert.pem"sv : cert_path });
+
+    MUST(resolver.when_socket_ready()->await());
+
+    size_t pending_requests = requests.size();
+    for (auto& request : requests) {
+        resolver.lookup(request.name, DNS::Messages::Class::IN, request.types)
+            ->when_resolved([&](auto& result) {
+                outln("Resolved {}:", request.name);
+                HashTable<DNS::Messages::ResourceType> types;
+                auto& recs = result->records();
+                for (auto& record : recs)
+                    types.set(record.type);
+
+                for (auto& type : types) {
+                    outln("  - {} IN {}:", request.name, DNS::Messages::to_string(type));
+                    for (auto& record : recs) {
+                        if (type != record.type)
+                            continue;
+
+                        outln("    - {}", record.to_string());
+                    }
+                }
+
+                if (--pending_requests == 0)
+                    loop.quit(0);
+            })
+            .when_rejected([&](auto& error) {
+                outln("Failed to resolve {} IN {}: {}", request.name, DNS::Messages::to_string(request.types.first()), error);
+                if (--pending_requests == 0)
+                    loop.quit(1);
+            });
+    }
+
+    return loop.exec();
+}