Skip to content

Commit

Permalink
Merge pull request #16 from hadriansecurity/add_caa_record
Browse files Browse the repository at this point in the history
src/dns_packet: Add CAA record
  • Loading branch information
kalmjasper authored Aug 13, 2024
2 parents 1a27e14 + 63df549 commit 31e7af4
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 43 deletions.
8 changes: 7 additions & 1 deletion include/dns_struct_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,16 @@ struct SOARdata {
} interval_settings;
};

struct CAARdata {
uint8_t flags;
CAATag tag;
TxtString value;
};

struct OPTRdata { };

using Rdata = std::variant<ARdata, AAAARdata, NSRdata, MXRdata, CNAMERdata, DNAMERdata, PTRRdata,
TXTRdata, SOARdata, OPTRdata, std::monostate>;
TXTRdata, SOARdata, CAARdata, OPTRdata, std::monostate>;

struct ResourceRecord {
DnsName name;
Expand Down
80 changes: 62 additions & 18 deletions src/dns_packet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <exception>

#include "dns_format.h"
#include "dns_struct_defs.h"
#include "dpdk_wrappers.h"
#include "network_types.h"
#include "spdlog/fmt/bundled/core.h"
Expand Down Expand Up @@ -133,7 +134,7 @@ tl::expected<DnsName, DNSParseError> ReadFromDNSNameFormat(std::span<const std::
name_parsed.buf[name_parsed.len] = static_cast<uint16_t>(*(reader));
if (++reader >= bytes.end()) [[unlikely]]
return tl::unexpected(DNSParseError::OutOfBounds);
if (unlikely(++name_parsed.len >= DOMAIN_NAME_MAX_SIZE))
if (++name_parsed.len >= DOMAIN_NAME_MAX_SIZE) [[unlikely]]
return tl::unexpected(DNSParseError::NameTooLong);

// Increment count if we haven't jumped yet
Expand All @@ -157,6 +158,26 @@ tl::expected<DnsName, DNSParseError> ReadFromDNSNameFormat(std::span<const std::
return name_parsed;
}

template <size_t N>
tl::expected<FixedName<N>, DNSParseError> ParseFixedName(std::span<const std::byte> bytes,
std::span<const std::byte>::iterator &reader, uint16_t length) {
FixedName<N> result;

// Tag field name length does not take \0 terminator into account
if (length >= N) [[unlikely]]
return tl::unexpected(DNSParseError::NameTooLong);
if (reader + length > bytes.end()) [[unlikely]]
return tl::unexpected(DNSParseError::OutOfBounds);

// Copy tag into r_data
std::copy(reader, reader + length, reinterpret_cast<std::byte *>(result.buf.begin()));
result.buf[length] = '\0';
result.len = length;
reader += length;

return result;
}

tl::expected<ResourceRecord, DNSParseError> ParseResourceRecord(std::span<const std::byte> bytes,
std::span<const std::byte>::iterator &reader) {
ResourceRecord parsed_record;
Expand All @@ -165,30 +186,41 @@ tl::expected<ResourceRecord, DNSParseError> ParseResourceRecord(std::span<const
const RData *response = UNWRAP_OR_RETURN(AdvanceReader<RData>(bytes, reader));
parsed_record.q_type = static_cast<DnsQType>(rte_be_to_cpu_16(response->type));
parsed_record.ttl = rte_be_to_cpu_32(response->ttl);

auto rdata_bytes = std::span(reader, rte_be_to_cpu_16(response->data_len));

// Make sure that the rdata bytes area doesn't go outside the packet byte area
if (rdata_bytes.end() > bytes.end()) [[unlikely]]
return tl::unexpected(DNSParseError::OutOfBounds);

auto begin = reader;
switch (parsed_record.q_type) {
case DnsQType::A: {
ARdata r_data;
r_data.ipv4_addr = *UNWRAP_OR_RETURN(AdvanceReader<InAddr>(bytes, reader));
r_data.ipv4_addr =
*UNWRAP_OR_RETURN(AdvanceReader<InAddr>(rdata_bytes, reader));
parsed_record.r_data = r_data;
break;
}
case DnsQType::AAAA: {
AAAARdata r_data;
r_data.ipv6_addr = *UNWRAP_OR_RETURN(AdvanceReader<In6Addr>(bytes, reader));
r_data.ipv6_addr =
*UNWRAP_OR_RETURN(AdvanceReader<In6Addr>(rdata_bytes, reader));
parsed_record.r_data = r_data;
break;
}
case DnsQType::NS: {
NSRdata r_data;
// Always pass in the full DNS packet as valid area for the
// ReadFromDNSNameFormat since the reader might jump
r_data.nameserver = UNWRAP_OR_RETURN(ReadFromDNSNameFormat(bytes, reader));
parsed_record.r_data = r_data;
break;
}
case DnsQType::MX: {
MXRdata r_data;
r_data.preference = rte_be_to_cpu_16(
*UNWRAP_OR_RETURN(AdvanceReader<uint16_t>(bytes, reader)));
*UNWRAP_OR_RETURN(AdvanceReader<uint16_t>(rdata_bytes, reader)));
r_data.mailserver = UNWRAP_OR_RETURN(ReadFromDNSNameFormat(bytes, reader));
parsed_record.r_data = r_data;
break;
Expand All @@ -215,18 +247,13 @@ tl::expected<ResourceRecord, DNSParseError> ParseResourceRecord(std::span<const
TXTRdata r_data;
r_data.txt.len = 0;

auto txt_bytes = std::span(reader, rte_be_to_cpu_16(response->data_len));
auto txt_writer = r_data.txt.buf.begin();

// Make sure that the txt bytes area doesn't go outside the packet byte area
if (txt_bytes.end() > bytes.end())
return tl::unexpected(DNSParseError::OutOfBounds);

while (reader < txt_bytes.end()) {
while (reader < rdata_bytes.end()) {
uint8_t next_string_size =
*UNWRAP_OR_RETURN(AdvanceReader<uint8_t>(txt_bytes, reader));
*UNWRAP_OR_RETURN(AdvanceReader<uint8_t>(rdata_bytes, reader));

if (reader + next_string_size > bytes.end()) [[unlikely]]
if (reader + next_string_size > rdata_bytes.end()) [[unlikely]]
return tl::unexpected(DNSParseError::OutOfBounds);

// Also take \0 terminator into account
Expand Down Expand Up @@ -268,24 +295,41 @@ tl::expected<ResourceRecord, DNSParseError> ParseResourceRecord(std::span<const
parsed_record.r_data = r_data;
break;
}
case DnsQType::OPT: {
if (reader + rte_be_to_cpu_16(response->data_len) > bytes.end())
return tl::unexpected(DNSParseError::OutOfBounds);
case DnsQType::CAA: {
CAARdata r_data;
r_data.flags =
*UNWRAP_OR_RETURN(AdvanceReader<uint8_t>(rdata_bytes, reader));

uint8_t tag_length =
*UNWRAP_OR_RETURN(AdvanceReader<uint8_t>(rdata_bytes, reader));

r_data.tag = UNWRAP_OR_RETURN(
ParseFixedName<CAA_TAG_MAX_SIZE>(rdata_bytes, reader, tag_length));

uint16_t value_len = rdata_bytes.end() - reader;

// Max length of the value is not specified in the RFC,
// character string should be sufficient
r_data.value = UNWRAP_OR_RETURN(ParseFixedName<CHARACTER_STRING_MAX_SIZE>(
rdata_bytes, reader, value_len));

parsed_record.r_data = r_data;
break;
}
case DnsQType::OPT: {
reader += rte_be_to_cpu_16(response->data_len);

parsed_record.r_data = OPTRdata{};
break;
}
default:
if (reader + rte_be_to_cpu_16(response->data_len) > bytes.end())
return tl::unexpected(DNSParseError::OutOfBounds);

reader += rte_be_to_cpu_16(response->data_len);
parsed_record.r_data = std::monostate();
break;
}

// Check that the actual read bytes are equal to the number of bytes indicated in
// the RData length section
if (reader != begin + rte_be_to_cpu_16(response->data_len)) [[unlikely]]
return tl::unexpected(DNSParseError::MalformedPacket);

Expand Down
4 changes: 2 additions & 2 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ std::optional<UserConfig> InitConfigFromArgs(int argc, char** argv) {
auto& q_type = parser["q-type"]
.abbreviation('q')
.description("Question type\n (A, NS, CNAME, DNAME, SOA, "
"PTR, MX, TXT, AAAA, OPT)")
"PTR, MX, TXT, AAAA, CAA, OPT)")
.type(po::string)
.fallback("A");

Expand Down Expand Up @@ -362,7 +362,7 @@ std::optional<UserConfig> InitConfigFromArgs(int argc, char** argv) {
std::optional res = GetQTypeFromString(q_type.get().string);
if (!res) {
fmt::print("{} invalid question type {}, choose from A, NS, CNAME, "
"DNAME, SOA, PTR, MX, TXT, AAAA, OPT\n",
"DNAME, SOA, PTR, MX, TXT, AAAA, CAA, OPT\n",
error_str, q_type.get().string);
return std::nullopt;
}
Expand Down
8 changes: 5 additions & 3 deletions src/worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ void HandleParsedPacket(WorkerContext &ctx, WorkerParams &param, const DNSPacket
return;
}

// fmt::print("{:02x}\n", fmt::join(reinterpret_cast<unsigned char
// *>(raw_pkt.data().padding.data()),
// reinterpret_cast<unsigned char *>(raw_pkt.data().padding.data() +
// raw_pkt.data_len), ""));

if (!param.output_raw) {
std::string out = glz::write_json(pkt);

Expand Down Expand Up @@ -463,9 +468,6 @@ int Worker(std::stop_token stop_token, uint16_t worker_id, WorkerParams param) {
// rte_memdump(stdout, "PACKET", data, packet.data_len);
// }

// if (prepared_packet_buf.size() > 1)
// std::ignore = prepared_packet_buf.pop();

auto [num_sent, unsent_packets] =
param.rxtx_if.SendPackets(ctx.queue_id, std::move(prepared_packet_buf));
prepared_packet_buf = std::move(unsent_packets);
Expand Down
61 changes: 60 additions & 1 deletion test/dns_packet_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ struct ReferenceRecord {
uint32_t minimum;
} soa_record;

struct TypeCAA {
uint8_t flags;
std::string tag;
std::string value;
} caa_record;

struct TypeTxt {
std::string name;
} txt_record;
Expand Down Expand Up @@ -159,6 +165,11 @@ void CheckRecord(const ReferenceRecord& reference, const ResourceRecord& test_re
EXPECT_EQ(reference.soa_record.expire, r_data.interval_settings.expire);
EXPECT_EQ(reference.soa_record.minimum, r_data.interval_settings.minimum);
},
[&](CAARdata r_data) {
EXPECT_EQ(reference.caa_record.flags, r_data.flags);
CheckFixedName(reference.caa_record.tag, r_data.tag);
CheckFixedName(reference.caa_record.value, r_data.value);
},
[&]([[maybe_unused]] OPTRdata r_data) {}, [&](std::monostate) {}},
test_record.r_data);
}
Expand Down Expand Up @@ -198,7 +209,7 @@ auto ExtractPacket(const TestPacket& packet) {
void CheckPacket(const TestPacket& packet) {
auto [mbuf_pool_out, parsed_packet] = ExtractPacket(packet);

EXPECT_EQ(parsed_packet.has_value(), true);
ASSERT_EQ(parsed_packet.has_value(), true);

EXPECT_EQ(parsed_packet->ip_data.dst_port, packet.dst_port);
EXPECT_EQ(parsed_packet->dns_id, packet.id);
Expand Down Expand Up @@ -687,6 +698,54 @@ TEST(DnsPacketParserTest, MX_record) {
CheckPacket(test_packet);
}

TEST(DnsPacketParserTest, CAA_RECORD) {
auto test_packet = TestPacket{
// Checks CAA record
.raw = "\xbc\xd0\x74\x17\xbe\x44\x1a\xfa\xb7\x3a\x46\x64\x08\x00\x45\x00"
"\x00\xaf\x66\x6f\x00\x00\x40\x11\xa7\xa0\xac\x14\x0a\x01\xac\x14"
"\x0a\x05\x00\x35\xf9\xb4\x00\x9b\xb2\x33\x0f\x8a\x81\x80\x00\x01"
"\x00\x03\x00\x00\x00\x01\x05\x61\x70\x70\x6c\x65\x03\x63\x6f\x6d"
"\x00\x01\x01\x00\x01\xc0\x0c\x01\x01\x00\x01\x00\x00\x01\x70\x00"
"\x23\x00\x05\x69\x6f\x64\x65\x66\x6d\x61\x69\x6c\x74\x6f\x3a\x63"
"\x6f\x6e\x74\x61\x63\x74\x5f\x70\x6b\x69\x40\x61\x70\x70\x6c\x65"
"\x2e\x63\x6f\x6d\xc0\x0c\x01\x01\x00\x01\x00\x00\x01\x70\x00\x12"
"\x00\x05\x69\x73\x73\x75\x65\x65\x6e\x74\x72\x75\x73\x74\x2e\x6e"
"\x65\x74\xc0\x0c\x01\x01\x00\x01\x00\x00\x01\x70\x00\x14\x00\x05"
"\x69\x73\x73\x75\x65\x70\x6b\x69\x2e\x61\x70\x70\x6c\x65\x2e\x63"
"\x6f\x6d\x00\x00\x29\x10\x00\x00\x00\x00\x00\x00\x00"s,

.ip_type = PacketIpType::Ipv4,
.qname = "apple.com.",
.q_type = DnsQType::CAA,
.error_code = DnsRCode::NOERROR,
.id = 0x0f8a,
.dst_port = 63924,
.src_ip = "172.20.10.1",
.dst_ip = "172.20.10.5",

.answer_ref_records = {ReferenceRecord{.q_type = DnsQType::CAA,
.ttl = 368,
.name = "apple.com.",
.caa_record = {.flags = 0,
.tag = "iodef",
.value = "mailto:[email protected]"}},
ReferenceRecord{.q_type = DnsQType::CAA,
.ttl = 368,
.name = "apple.com.",
.caa_record = {.flags = 0, .tag = "issue", .value = "entrust.net"}},
ReferenceRecord{.q_type = DnsQType::CAA,
.ttl = 368,
.name = "apple.com.",
.caa_record = {.flags = 0, .tag = "issue", .value = "pki.apple.com"}}},
.additional_ref_records =
{
ReferenceRecord{.q_type = DnsQType::OPT, .ttl = 0, .name = ""},
},
};

CheckPacket(test_packet);
}

TEST(DnsPacketParserTest, PTR_record) {
auto test_packet = TestPacket{// Checks PTR record
.raw = "\xbc\xd0\x74\x17\xbe\x44\x26\x5a\x4c\x53\x36\x7e\x08\x00\x45\x00"
Expand Down
Loading

0 comments on commit 31e7af4

Please sign in to comment.