Skip to content

Commit

Permalink
Merge pull request #18 from hadriansecurity/format_caa_mx_soa
Browse files Browse the repository at this point in the history
Format CAA, MX, SOA
  • Loading branch information
kalmjasper authored Aug 13, 2024
2 parents 7c11cca + 9f4f6ad commit 4eb6dec
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 7 deletions.
31 changes: 28 additions & 3 deletions include/dns_json_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "dns_format.h"
#include "dns_packet_constructor.h"
#include "dns_struct_defs.h"
#include "encode_dns_char_string.h"

/**
* Definitions for resource records
Expand All @@ -29,6 +30,13 @@ struct glz::meta<NSRdata> {
static constexpr auto value{&NSRdata::nameserver};
};

template <>
struct glz::meta<MXRdata> {
static constexpr auto value = [](const auto &self) -> auto {
return fmt::format("{} {}", self.preference, self.mailserver);
};
};

template <>
struct glz::meta<CNAMERdata> {
static constexpr auto value{&CNAMERdata::cname};
Expand All @@ -46,7 +54,24 @@ struct glz::meta<PTRRdata> {

template <>
struct glz::meta<struct TXTRdata> {
static constexpr auto value{&TXTRdata::txt};
static constexpr auto value = [](const auto &self) -> auto {
return encode_dns_char_string(self.txt);
};
};

template <>
struct glz::meta<SOARdata> {
static constexpr auto value = [](const auto &self) -> auto {
return fmt::format("{} {} {} {} {} {} {}", self.m_name, self.r_name, self.interval_settings.serial, self.interval_settings.refresh, self.interval_settings.retry, self.interval_settings.expire, self.interval_settings.minimum);
};
};

template <>
struct glz::meta<struct CAARdata> {
static constexpr auto value = [](const auto &self) -> auto {
auto value_encoded = encode_dns_char_string(self.value, true);
return fmt::format("{} {} {}", self.flags, self.tag, value_encoded);
};
};

template <>
Expand All @@ -62,7 +87,7 @@ struct glz::meta<ResourceRecord> {

template <>
struct glz::meta<in_addr> {
static constexpr auto value = [](auto &self) -> auto {
static constexpr auto value = [](const auto &self) -> auto {
FixedName<INET_ADDRSTRLEN> val;

inet_ntop(AF_INET, &self, val.buf.data(), INET_ADDRSTRLEN);
Expand All @@ -73,7 +98,7 @@ struct glz::meta<in_addr> {

template <>
struct glz::meta<in6_addr> {
static constexpr auto value = [](auto &self) -> auto {
static constexpr auto value = [](const auto &self) -> auto {
FixedName<INET6_ADDRSTRLEN> val;

inet_ntop(AF_INET6, &self, val.buf.data(), INET6_ADDRSTRLEN);
Expand Down
1 change: 1 addition & 0 deletions include/dns_struct_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ enum class DNSParseError {
SrcPortErr,
MalformedPacket,
TxtTooLong,
InvalidChar,
};

// spdlog formatting for DNSParseError
Expand Down
17 changes: 17 additions & 0 deletions src/dns_packet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ auto fmt::formatter<DNSParseError>::format(DNSParseError e, format_context &ctx)
break;
case DNSParseError::EtherHdrProtoErr:
error = "ethernet header error";
case DNSParseError::InvalidChar:
error = "invalid character detected in packet";
break;
}
return formatter<string_view>::format(error, ctx);
Expand All @@ -75,6 +77,15 @@ inline tl::expected<const T *, DNSParseError> AdvanceReader(std::span<const std:
return ptr;
}

bool contains_unprintable_chars(std::string_view sv) {
bool result = false;
for (const auto &c : sv) {
result |= c < ' ' || c > '~';
}

return result;
}

tl::expected<DnsName, DNSParseError> ReadFromDNSNameFormat(std::span<const std::byte> bytes,
std::span<const std::byte>::iterator &reader) {
DnsName name_parsed;
Expand Down Expand Up @@ -149,6 +160,9 @@ tl::expected<DnsName, DNSParseError> ReadFromDNSNameFormat(std::span<const std::
// Add NULL terminator
name_parsed.buf[name_parsed.len] = '\0';

if (contains_unprintable_chars(std::string_view(name_parsed))) [[unlikely]]
return tl::unexpected(DNSParseError::InvalidChar);

// Add one to count if jumped to account for 2 byte offset field instead of 1 byte NULL
// terminator
count += (bool) jmp_cnt;
Expand Down Expand Up @@ -306,6 +320,9 @@ tl::expected<ResourceRecord, DNSParseError> ParseResourceRecord(std::span<const
r_data.tag = UNWRAP_OR_RETURN(
ParseFixedName<CAA_TAG_MAX_SIZE>(rdata_bytes, reader, tag_length));

if (contains_unprintable_chars(std::string_view(r_data.tag))) [[unlikely]]
return tl::unexpected(DNSParseError::InvalidChar);

uint16_t value_len = rdata_bytes.end() - reader;

// Max length of the value is not specified in the RFC,
Expand Down
38 changes: 35 additions & 3 deletions test/utils_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,9 @@ TYPED_TEST(FixedNameTests, FixedNameConcatenation) {

{
// Test concatenation within bounds
auto hello_opt = FixedNameType::init("Hello");
ASSERT_TRUE(hello_opt.has_value());
auto hello = FixedNameType("Hello");

auto combined_opt = *hello_opt + "World";
auto combined_opt = hello + "World";
if (ArraySize >= 11) { // "HelloWorld" + null terminator fits within 11 chars
ASSERT_TRUE(combined_opt.has_value());
EXPECT_EQ(combined_opt->len, 10);
Expand All @@ -267,6 +266,39 @@ TYPED_TEST(FixedNameTests, FixedNameConcatenation) {
}
}

TYPED_TEST(FixedNameTests, FixedNameAppend) {
constexpr auto ArraySize = TestFixture::ArraySize;
using FixedNameType = FixedName<ArraySize>;

{
// Test append within bounds
auto hello = FixedNameType("Hello");

auto success = hello += "World";
if (ArraySize >= 11) { // "HelloWorld" + null terminator fits within 11 chars
ASSERT_TRUE(success);
EXPECT_EQ(hello.len, 10);
EXPECT_EQ(std::string_view(hello), "HelloWorld");
} else {
ASSERT_FALSE(success);
}
}
{
// Test append that exceeds buffer size by one character
std::string almost_full(ArraySize - 2,
'a'); // leaving 1 space for 'b' and 1 for null terminator
auto almost_full_opt = FixedNameType::init(almost_full);

ASSERT_TRUE(almost_full_opt.has_value());

bool success = *almost_full_opt += "b";
ASSERT_TRUE(success);

success = *almost_full_opt += "c";
ASSERT_FALSE(success);
}
}

std::optional<std::vector<InAddr>> GetRefResolverList(std::vector<std::string> input) {
std::vector<InAddr> to_ret;
for (auto& resolver_str : input) {
Expand Down
64 changes: 64 additions & 0 deletions utils/encode_dns_char_string.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#pragma once

#include <expected.h>
#include <expected_helpers.h>

#include <array>
#include <cstdint>
#include <fixed_name.hpp>
#include <string_view>

namespace _detail {
static constexpr size_t char_encoding_max_size = 4;

struct CharEncoding {
char encoded[char_encoding_max_size];
uint8_t len;

operator std::string_view() const {
return std::string_view(encoded, len);
}
};

static constexpr char to_decimal_char(int n) {
return static_cast<char>('0' + n);
}

static constexpr std::array<char, 3> to_decimal(int n) {
return {to_decimal_char(n / 100), to_decimal_char((n / 10) % 10), to_decimal_char(n % 10)};
}

static constexpr std::array<CharEncoding, 256> char_map = []() {
std::array<CharEncoding, 256> map{};
for (int i = 0; i < 256; ++i) {
if (i >= 32 && i <= 126 && i != '"' && i != '\\') {
map[i] = {{static_cast<char>(i)}, 1};
} else {
auto decimal = to_decimal(i);
map[i] = {{'\\', decimal[0], decimal[1], decimal[2]}, 4};
}
}
map['"'] = {{'\\', '"'}, 2};
map['\\'] = {{'\\', '\\'}, 2};
return map;
}();
} // namespace _detail

template <size_t N>
static FixedName<N * _detail::char_encoding_max_size> encode_dns_char_string(FixedName<N> in,
bool add_quotes = false) {
FixedName<N * _detail::char_encoding_max_size> out;

if (add_quotes)
out += "\"";

for (unsigned char c : std::string_view(in)) {
const auto& encoding = _detail::char_map[c];
out += encoding;
}

if (add_quotes)
out += "\"";

return out;
}
18 changes: 17 additions & 1 deletion utils/fixed_name.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ struct FixedName {
* @return std::optional<FixedName> containing the concatenated result, or std::nullopt if the result exceeds the buffer size.
*/
std::optional<FixedName> operator+(std::string_view other) const {
if (len + other.size() + 1 > N) { // +1 for the null terminator
if (len + other.size() >= N) { // >= N because we need space for null terminato
return std::nullopt;
}

Expand All @@ -98,6 +98,22 @@ struct FixedName {
return result;
}

/**
* @brief Appends a string_view to the current FixedName.
*
* @param other The string_view to append.
* @return bool True if the operation was successful, false otherwise.
*/
bool operator+=(std::string_view other) {
if (len + other.size() >= N) { // >= N because we need space for null terminator
return false;
}
std::copy(other.begin(), other.end(), buf.begin() + len);
len += other.size();
buf[len] = '\0'; // Ensure null-termination
return true;
}

/**
* @brief Initializes a FixedName from a string view.
*
Expand Down

0 comments on commit 4eb6dec

Please sign in to comment.