From 7e20f4726fe64d2417a32ff032da9fdd66e03b7b Mon Sep 17 00:00:00 2001 From: Ali Mohammad Pur Date: Fri, 1 Nov 2024 23:53:43 +0100 Subject: [PATCH] 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.) --- Libraries/LibDNS/CMakeLists.txt | 6 + Libraries/LibDNS/Message.cpp | 1177 +++++++++++++++++ Libraries/LibDNS/Message.h | 704 ++++++++++ Libraries/LibDNS/Resolver.h | 404 ++++++ Libraries/LibWebView/Application.cpp | 14 + Libraries/LibWebView/HelperProcess.cpp | 14 +- Libraries/LibWebView/Options.h | 13 + Meta/Lagom/CMakeLists.txt | 3 + Services/RequestServer/CMakeLists.txt | 2 +- .../RequestServer/ConnectionFromClient.cpp | 103 ++ Services/RequestServer/ConnectionFromClient.h | 13 + Services/RequestServer/RequestServer.ipc | 3 + Utilities/dns.cpp | 132 ++ 13 files changed, 2586 insertions(+), 2 deletions(-) create mode 100644 Libraries/LibDNS/CMakeLists.txt create mode 100644 Libraries/LibDNS/Message.cpp create mode 100644 Libraries/LibDNS/Message.h create mode 100644 Libraries/LibDNS/Resolver.h create mode 100644 Utilities/dns.cpp diff --git a/Libraries/LibDNS/CMakeLists.txt b/Libraries/LibDNS/CMakeLists.txt new file mode 100644 index 00000000000..ccb572a8e7b --- /dev/null +++ b/Libraries/LibDNS/CMakeLists.txt @@ -0,0 +1,6 @@ +set(SOURCES + Message.cpp +) + +serenity_lib(LibDNS dns) +target_link_libraries(LibDNS PRIVATE LibCore) diff --git a/Libraries/LibDNS/Message.cpp b/Libraries/LibDNS/Message.cpp new file mode 100644 index 00000000000..d581bee1184 --- /dev/null +++ b/Libraries/LibDNS/Message.cpp @@ -0,0 +1,1177 @@ +/* + * Copyright (c) 2024, Ali Mohammad Pur + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +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::from_raw(AK::Stream& stream) +{ + CountingStream counting_stream { MaybeOwned(stream) }; + auto context = ParseContext { counting_stream, make>() }; + return from_raw(context); +} + +ErrorOr 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 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 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::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(static_cast(TRY(ctx.stream.read_value>()))); + auto class_ = static_cast(static_cast(TRY(ctx.stream.read_value>()))); + + return Question { move(name), type, class_ }; +} + +ErrorOr 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>(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>(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 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::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()); + 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(length & ~OffsetMarkerMask) << 8 | TRY(ctx.stream.read_value()); + 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 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(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 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 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 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 m_stream; + ByteBuffer m_recorded_data; +}; + +ErrorOr 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(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(static_cast(TRY(rr_ctx.stream.read_value>()))); + 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(static_cast(TRY(rr_ctx.stream.read_value>()))); + ttl = static_cast(TRY(rr_ctx.stream.read_value>())); + auto rd_length = static_cast(TRY(rr_ctx.stream.read_value>())); + 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) }; + 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 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>(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>(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>(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>(rdata.size()); + memcpy(rdata_length_bytes.data(), &net_rdata_length, 2); + } + + TRY(buffer.try_append(rdata)); + + return {}; +} + +ErrorOr 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::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>()); + return Records::A { IPv4Address { address } }; +} + +ErrorOr 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>()); + return Records::AAAA { IPv6Address { bit_cast>(address) } }; +} + +ErrorOr Records::TXT::from_raw(ParseContext& ctx) +{ + // RFC 1035, 3.3.14. TXT RDATA format. + // | TXT-DATA | a which is used for human readability. + + auto length = TRY(ctx.stream.read_value()); + ByteBuffer content; + TRY(ctx.stream.read_until_filled(TRY(content.get_bytes_for_writing(length)))); + return Records::TXT { ByteString::copy(content) }; +} + +ErrorOr Records::CNAME::from_raw(ParseContext& ctx) +{ + // RFC 1035, 3.3.1. CNAME RDATA format. + // | CNAME | a 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::from_raw(ParseContext& ctx) +{ + // RFC 1035, 3.3.11. NS RDATA format. + // | NSDNAME | a 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::from_raw(ParseContext& ctx) +{ + // RFC 1035, 3.3.13. SOA RDATA format. + // | MNAME | which specifies the name of the host where the master file for the zone is maintained. + // | RNAME | 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(TRY(ctx.stream.read_value>())); + auto refresh = static_cast(TRY(ctx.stream.read_value>())); + auto retry = static_cast(TRY(ctx.stream.read_value>())); + auto expire = static_cast(TRY(ctx.stream.read_value>())); + auto minimum = static_cast(TRY(ctx.stream.read_value>())); + + return Records::SOA { move(mname), move(rname), serial, refresh, retry, expire, minimum }; +} + +ErrorOr 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 which specifies a host willing to act as a mail exchange for the owner name. + + auto preference = static_cast(TRY(ctx.stream.read_value>())); + auto exchange = TRY(DomainName::from_raw(ctx)); + return Records::MX { preference, move(exchange) }; +} + +ErrorOr Records::PTR::from_raw(ParseContext& ctx) +{ + // RFC 1035, 3.3.12. PTR RDATA format. + // | PTRDNAME | a 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::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 which specifies the target host. + + auto priority = static_cast(TRY(ctx.stream.read_value>())); + auto weight = static_cast(TRY(ctx.stream.read_value>())); + auto port = static_cast(TRY(ctx.stream.read_value>())); + auto target = TRY(DomainName::from_raw(ctx)); + return Records::SRV { priority, weight, port, move(target) }; +} + +ErrorOr 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(TRY(ctx.stream.read_value>())); + auto protocol = TRY(ctx.stream.read_value()); + auto algorithm = static_cast(static_cast(TRY(ctx.stream.read_value()))); + auto public_key = TRY(ctx.stream.read_until_eof()); + return Records::DNSKEY { flags, protocol, algorithm, move(public_key) }; +} + +ErrorOr 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(TRY(ctx.stream.read_value>())); + auto algorithm = static_cast(static_cast(TRY(ctx.stream.read_value()))); + auto digest_type = static_cast(static_cast(TRY(ctx.stream.read_value()))); + 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::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 which specifies the domain name of the signer generating the SIG RR. + // | SIGNATURE | a that authenticates the RRs. + + auto type_covered = static_cast(static_cast(TRY(ctx.stream.read_value>()))); + auto algorithm = static_cast(static_cast(TRY(ctx.stream.read_value()))); + auto labels = TRY(ctx.stream.read_value()); + auto original_ttl = static_cast(TRY(ctx.stream.read_value>())); + auto signature_expiration = static_cast(TRY(ctx.stream.read_value>())); + auto signature_inception = static_cast(TRY(ctx.stream.read_value>())); + auto key_tag = static_cast(TRY(ctx.stream.read_value>())); + 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 Records::SIG::to_string() const +{ + // Single line: + // SIG Type covered: , Algorithm: , Labels: , Original TTL: , Signature expiration: , Signature inception: , Key tag: , Signer's name: , 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::from_raw(ParseContext& ctx) +{ + // RFC 1035, 3.3.2. HINFO RDATA format. + // | CPU | a which specifies the CPU type. + // | OS | a which specifies the operating system type. + + auto cpu_length = TRY(ctx.stream.read_value()); + ByteBuffer cpu; + TRY(ctx.stream.read_until_filled(TRY(cpu.get_bytes_for_writing(cpu_length)))); + auto os_length = TRY(ctx.stream.read_value()); + 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::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(TRY(ctx.stream.read_value>())); + auto rcode_and_flags = static_cast(TRY(ctx.stream.read_value>())); + auto rd_length = static_cast(TRY(ctx.stream.read_value>())); + Vector options; + while (rd_length > 0 && !ctx.stream.is_eof()) { + auto option_code = static_cast(TRY(ctx.stream.read_value>())); + auto option_length = static_cast(TRY(ctx.stream.read_value>())); + 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 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>(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>(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>(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>(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>(option.data.size()); + memcpy(option_length_bytes.data(), &net_option_length, 2); + + TRY(buffer.try_append(option.data)); + } + + return {}; +} + +} diff --git a/Libraries/LibDNS/Message.h b/Libraries/LibDNS/Message.h new file mode 100644 index 00000000000..faea780e7f8 --- /dev/null +++ b/Libraries/LibDNS/Message.h @@ -0,0 +1,704 @@ +/* + * Copyright (c) 2024, Ali Mohammad Pur + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace DNS { +namespace Messages { + +struct DomainName; +struct ParseContext { + CountingStream& stream; + NonnullOwnPtr> 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(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(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(raw & ResponseCodeMask); } + OpCode op_code() const { return static_cast((raw & OpCodeMask) >> 11); } + + String to_string() const; + + NetworkOrdered raw { 0 }; +}; +StringView to_string(Options::ResponseCode); + +struct Header { + NetworkOrdered id; + Options options; + NetworkOrdered question_count; + NetworkOrdered answer_count; + NetworkOrdered authority_count; + NetworkOrdered additional_count; +}; + +struct DomainName { + Vector labels; + + static DomainName from_string(StringView); + static ErrorOr from_raw(ParseContext&); + ErrorOr 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 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const; +}; + +namespace Records { + +struct A { + IPv4Address address; + + static constexpr ResourceType type = ResourceType::A; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const { return address.to_string(); } +}; +struct AAAA { + IPv6Address address; + + static constexpr ResourceType type = ResourceType::AAAA; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const { return address.to_string(); } +}; +struct TXT { + ByteString content; + + static constexpr ResourceType type = ResourceType::TXT; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const { return String::formatted("Text: '{}'", StringView { content }); } +}; +struct CNAME { + DomainName names; + + static constexpr ResourceType type = ResourceType::CNAME; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const { return names.to_string(); } +}; +struct NS { + DomainName name; + + static constexpr ResourceType type = ResourceType::NS; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 + CDNSKEY(Ts&&... args) + : DNSKEY(forward(args)...) + { + } + + static constexpr ResourceType type = ResourceType::CDNSKEY; + static ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const { return "DS"_string; } +}; +struct CDS : public DS { + template + CDS(Ts&&... args) + : DS(forward(args)...) + { + } + static constexpr ResourceType type = ResourceType::CDS; + static ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const; +}; +struct RRSIG : public SIG { + template + RRSIG(Ts&&... args) + : SIG(forward(args)...) + { + } + + static constexpr ResourceType type = ResourceType::RRSIG; + static ErrorOr from_raw(ParseContext& raw) { return SIG::from_raw(raw); } +}; +struct NSEC { + DomainName next_domain_name; + Vector types; + + static constexpr ResourceType type = ResourceType::NSEC; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const { return "NSEC"_string; } +}; +struct NSEC3 { + DNSSEC::NSEC3HashAlgorithm hash_algorithm; + u8 flags; + u16 iterations; + ByteBuffer salt; + DomainName next_hashed_owner_name; + Vector types; + + static constexpr ResourceType type = ResourceType::NSEC3; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr to_string() const { return "TLSA"_string; } +}; +struct HINFO { + ByteString cpu; + ByteString os; + + static constexpr ResourceType type = ResourceType::HINFO; + static ErrorOr from_raw(ParseContext&); + ErrorOr to_raw(ByteBuffer&) const { return Error::from_string_literal("Not implemented"); } + ErrorOr 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 udp_payload_size { 0 }; + NetworkOrdered extended_rcode_and_flags { 0 }; + Vector