From ced645fb025012f802c0ca4d8e85bbc46d23d70e Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Thu, 14 Jan 2016 12:18:43 -0800 Subject: [PATCH] Add DNS SOA record parsing and serialization Fixes #48 --- include/tins/dns.h | 195 +++++++++++++++++++++++++++++++++++++- include/tins/exceptions.h | 10 ++ src/dns.cpp | 152 ++++++++++++++++++++++++++--- tests/src/dns.cpp | 80 ++++++++++++++++ 4 files changed, 423 insertions(+), 14 deletions(-) diff --git a/include/tins/dns.h b/include/tins/dns.h index 7b52553..83f1279 100644 --- a/include/tins/dns.h +++ b/include/tins/dns.h @@ -221,8 +221,176 @@ public: QueryClass qclass_; }; + class Resource; + /** - * \brief Struct that represent DNS resource records. + * \brief Class that represents a Start Of Authority record + */ + class soa_record { + public: + + friend class DNSTest; + /** + * \brief Default constructor + */ + soa_record(); + + /** + * \brief Constructs a SOA record from a DNS::resource + * \param resource The resource from which to construct this record + */ + soa_record(const DNS::Resource& resource); + + /** + * \brief Constructs a SOA record from a buffer + * \param buffer The buffer from which to construct this SOA record + * \param total_sz The size of the buffer + */ + soa_record(const uint8_t* buffer, uint32_t total_sz); + + /** + * \brief Constructs a SOA record + * + * \param mname The primary source name + * \param rname The responsible person name + * \param serial The serial number + * \param refresh The refresh value + * \param retry The retry value + * \param expire The expire value + * \param minimum_ttl The minimum TTL value + */ + soa_record(const std::string& mname, + const std::string& rname, + uint32_t serial, + uint32_t refresh, + uint32_t retry, + uint32_t expire, + uint32_t minimum_ttl); + + /** + * \brief Getter for the primary source name field + * + * The returned domain name is already decoded. + * + * \return mname The primary source name field + */ + const std::string& mname() const { + return mname_; + } + + /** + * \brief Getter for the responsible person name field + * + * The returned domain name is already decoded. + * + * \return mname The responsible person name field + */ + const std::string& rname() const { + return rname_; + } + + /** + * \brief Getter for the serial number field + * \return The serial number field + */ + uint32_t serial() const { + return serial_; + } + + /** + * \brief Getter for the refresh field + * \return The refresh field + */ + uint32_t refresh() const { + return refresh_; + } + + /** + * \brief Getter for the retry field + * \return The retry field + */ + uint32_t retry() const { + return retry_; + } + + /** + * \brief Getter for the expire field + * \return The expire field + */ + uint32_t expire() const { + return expire_; + } + + /** + * \brief Getter for the minimum TTL field + * \return The minimum TTL field + */ + uint32_t minimum_ttl() const { + return minimum_ttl_; + } + + /** + * \brief Getter for the primary source name field + * \param value The new primary source name field value + */ + void mname(const std::string& value); + + /** + * \brief Getter for the responsible person name field + * \param value The new responsible person name field value + */ + void rname(const std::string& value); + + /** + * \brief Getter for the serial number field + * \param value The new serial number field value + */ + void serial(uint32_t value); + + /** + * \brief Getter for the refresh field + * \param value The new refresh field value + */ + void refresh(uint32_t value); + + /** + * \brief Getter for the retry field + * \param value The new retry field value + */ + void retry(uint32_t value); + + /** + * \brief Getter for the expire field + * \param value The new expire field value + */ + void expire(uint32_t value); + + /** + * \brief Getter for the minimum TTL field + * \param value The new minimum TTL field value + */ + void minimum_ttl(uint32_t value); + + /** + * \brief Serialize this SOA record + * \return The serialized SOA record + */ + PDU::serialization_type serialize() const; + private: + friend class DNS; + void init(const uint8_t* buffer, uint32_t total_sz); + + std::string mname_; + std::string rname_; + uint32_t serial_; + uint32_t refresh_; + uint32_t retry_; + uint32_t expire_; + uint32_t minimum_ttl_; + }; + + /** + * \brief Class that represent DNS resource records. */ class Resource { public: @@ -317,6 +485,15 @@ public: data_ = data; } + /** + * \brief Sets the contents of this resource to the provided SOA record + * \param data The SOA record that will be stored in this resource + */ + void data(const soa_record& data) { + serialization_type buffer = data.serialize(); + data_.assign(buffer.begin(), buffer.end()); + } + /** * Setter for the type field. */ @@ -693,6 +870,20 @@ public: */ static std::string encode_domain_name(const std::string& domain_name); + /** + * \brief Decodes a domain name + * + * This method processes an encoded domain name and returns the decoded + * version. This can't handle offset labels. + * + * For example, given the input "\x03www\x07example\x03com\x00", + * the output would be www.example.com". + * + * \param domain_name The domain name to decode. + * \return The decoded domain name. + */ + static std::string decode_domain_name(const std::string& domain_name); + /** * \brief Check wether ptr points to a valid response for this PDU. * @@ -709,6 +900,8 @@ public: return new DNS(*this); } private: + friend class soa_record; + TINS_BEGIN_PACK struct dns_header { uint16_t id; diff --git a/include/tins/exceptions.h b/include/tins/exceptions.h index 318fe8f..cc0c9cb 100644 --- a/include/tins/exceptions.h +++ b/include/tins/exceptions.h @@ -263,6 +263,16 @@ public: } }; +/** + * \brief Exception thrown when an invalid domain name is parsed + */ +class invalid_domain_name : public exception_base { +public: + const char* what() const throw() { + return "Invalid domain name"; + } +}; + namespace Crypto { namespace WPA2 { /** diff --git a/src/dns.cpp b/src/dns.cpp index 95e539a..5a66ac2 100644 --- a/src/dns.cpp +++ b/src/dns.cpp @@ -294,6 +294,39 @@ string DNS::encode_domain_name(const string& dn) { return output; } +string DNS::decode_domain_name(const string& domain_name) { + string output; + if (domain_name.empty()) { + return output; + } + const uint8_t* ptr = (const uint8_t*)&domain_name[0]; + const uint8_t* end = ptr + domain_name.size(); + while (*ptr) { + // We can't handle offsets + if ((*ptr & 0xc0)) { + throw invalid_domain_name(); + } + else { + // It's a label, grab its size. + uint8_t size = *ptr; + ptr++; + if (ptr + size > end) { + throw malformed_packet(); + } + // Append a dot if it's not the first one. + if (!output.empty()) { + output.push_back('.'); + } + output.insert(output.end(), ptr, ptr + size); + ptr += size; + } + if (output.size() > 256) { + throw invalid_domain_name(); + } + } + return output; +} + // The output buffer should be at least 256 bytes long. This used to use // a std::string but it worked about 50% slower, so this is somehow // unsafe but a lot faster. @@ -380,7 +413,7 @@ void DNS::convert_records(const uint8_t* ptr, InputMemoryStream stream(ptr, end - ptr); char dname[256], small_addr_buf[256]; while (stream) { - string addr; + string data; bool used_small_buffer = false; // Retrieve the record's domain name. stream.skip(compose_name(stream.pointer(), dname)); @@ -402,7 +435,7 @@ void DNS::convert_records(const uint8_t* ptr, switch (type) { case AAAA: - addr = stream.read().to_string(); + data = stream.read().to_string(); break; case A: inline_convert_v4(stream.read(), small_addr_buf); @@ -417,23 +450,29 @@ void DNS::convert_records(const uint8_t* ptr, stream.skip(data_size); used_small_buffer = true; break; + case SOA: + { + stream.skip(compose_name(stream.pointer(), small_addr_buf)); + data = encode_domain_name(small_addr_buf); + stream.skip(compose_name(stream.pointer(), small_addr_buf)); + data += encode_domain_name(small_addr_buf); + const uint32_t size_left = sizeof(uint32_t) * 5; + if (!stream.can_read(size_left)) { + throw malformed_packet(); + } + data.insert(data.end(), stream.pointer(), stream.pointer() + size_left); + stream.skip(size_left); + } + break; default: - if (data_size < sizeof(small_addr_buf) - 1) { - stream.read(small_addr_buf, data_size); - // null terminator - small_addr_buf[data_size] = 0; - used_small_buffer = true; - } - else { - addr.assign(stream.pointer(), stream.pointer() + data_size); - stream.skip(data_size); - } + data.assign(stream.pointer(), stream.pointer() + data_size); + stream.skip(data_size); break; } res.push_back( Resource( dname, - (used_small_buffer) ? small_addr_buf : addr, + (used_small_buffer) ? small_addr_buf : data, type, qclass, ttl, @@ -556,4 +595,91 @@ bool DNS::matches_response(const uint8_t* ptr, uint32_t total_sz) const { return hdr->id == header_.id; } +// SOA record + +DNS::soa_record::soa_record() +: serial_(0), refresh_(0), retry_(0), expire_(0) { + +} + +DNS::soa_record::soa_record(const string& mname, + const string& rname, + uint32_t serial, + uint32_t refresh, + uint32_t retry, + uint32_t expire, + uint32_t minimum_ttl) +: mname_(mname), rname_(rname), serial_(serial), refresh_(refresh), retry_(retry), + expire_(expire), minimum_ttl_(minimum_ttl) { + +} + +DNS::soa_record::soa_record(const uint8_t* buffer, uint32_t total_sz) { + init(buffer, total_sz); +} + +DNS::soa_record::soa_record(const DNS::Resource& resource) { + init((const uint8_t*)&resource.data()[0], resource.data().size()); +} + +void DNS::soa_record::mname(const string& value) { + mname_ = value; +} + +void DNS::soa_record::rname(const string& value) { + rname_ = value; +} + +void DNS::soa_record::serial(uint32_t value) { + serial_ = value; +} + +void DNS::soa_record::refresh(uint32_t value) { + refresh_ = value; +} + +void DNS::soa_record::retry(uint32_t value) { + retry_ = value; +} + +void DNS::soa_record::expire(uint32_t value) { + expire_ = value; +} + +void DNS::soa_record::minimum_ttl(uint32_t value) { + minimum_ttl_ = value; +} + +PDU::serialization_type DNS::soa_record::serialize() const { + string encoded_mname = DNS::encode_domain_name(mname_); + string encoded_rname = DNS::encode_domain_name(rname_); + PDU::serialization_type output( + encoded_mname.size() + encoded_rname.size() + sizeof(uint32_t) * 5 + ); + OutputMemoryStream stream(output); + stream.write(encoded_mname.begin(), encoded_mname.end()); + stream.write(encoded_rname.begin(), encoded_rname.end()); + stream.write_be(serial_); + stream.write_be(refresh_); + stream.write_be(retry_); + stream.write_be(expire_); + stream.write_be(minimum_ttl_); + return output; +} + +void DNS::soa_record::init(const uint8_t* buffer, uint32_t total_sz) { + InputMemoryStream stream(buffer, total_sz); + string domain = (const char*)stream.pointer(); + mname_ = DNS::decode_domain_name(domain); + stream.skip(domain.size() + 1); + domain = (const char*)stream.pointer(); + stream.skip(domain.size() + 1); + rname_ = DNS::decode_domain_name(domain); + serial_ = stream.read_be(); + refresh_ = stream.read_be(); + retry_ = stream.read_be(); + expire_ = stream.read_be(); + minimum_ttl_ = stream.read_be(); +} + } // Tins diff --git a/tests/src/dns.cpp b/tests/src/dns.cpp index 22c0527..fbe4840 100644 --- a/tests/src/dns.cpp +++ b/tests/src/dns.cpp @@ -473,3 +473,83 @@ TEST_F(DNSTest, MXPreferenceField) { EXPECT_EQ(42, resource.preference()); EXPECT_EQ("example.com", resource.dname()); } + +TEST_F(DNSTest, SOARecordConstructor) { + DNS::soa_record r( + "hehehehe.example.com", + "john.example.com", + 0x9823ade9, + 0x918273aa, + 0x827361ad, + 0x8ad71928, + 0x1ad92871 + ); + EXPECT_EQ("hehehehe.example.com", r.mname()); + EXPECT_EQ("john.example.com", r.rname()); + EXPECT_EQ(0x9823ade9, r.serial()); + EXPECT_EQ(0x918273aa, r.refresh()); + EXPECT_EQ(0x827361ad, r.retry()); + EXPECT_EQ(0x8ad71928, r.expire()); + EXPECT_EQ(0x1ad92871, r.minimum_ttl()); +} + +TEST_F(DNSTest, SOARecordGettersAndSetters) { + DNS::soa_record r; + r.mname("hehehehe.example.com"); + r.rname("john.example.com"); + r.serial(0x9823ade9); + r.refresh(0x918273aa); + r.retry(0x827361ad); + r.expire(0x8ad71928); + r.minimum_ttl(0x1ad92871); + EXPECT_EQ("hehehehe.example.com", r.mname()); + EXPECT_EQ("john.example.com", r.rname()); + EXPECT_EQ(0x9823ade9, r.serial()); + EXPECT_EQ(0x918273aa, r.refresh()); + EXPECT_EQ(0x827361ad, r.retry()); + EXPECT_EQ(0x8ad71928, r.expire()); + EXPECT_EQ(0x1ad92871, r.minimum_ttl()); +} + +TEST_F(DNSTest, SOARecordFromBuffer) { + const uint8_t raw[] = { + 232, 101, 129, 128, 0, 1, 0, 1, 0, 0, 0, 0, 6, 103, 111, 111, 103, 108, + 101, 3, 99, 111, 109, 0, 0, 6, 0, 1, 192, 12, 0, 6, 0, 1, 0, 0, 0, 59, + 0, 38, 3, 110, 115, 50, 192, 12, 9, 100, 110, 115, 45, 97, 100, 109, 105, + 110, 192, 12, 6, 174, 163, 84, 0, 0, 3, 132, 0, 0, 3, 132, 0, 0, 7, 8, 0, + 0, 0, 60 + }; + + DNS dns(raw, sizeof(raw)); + ASSERT_EQ(1, dns.answers().size()); + DNS::Resource r(dns.answers().front()); + DNS::soa_record soa(r); + EXPECT_EQ("ns2.google.com", soa.mname()); + EXPECT_EQ("dns-admin.google.com", soa.rname()); + EXPECT_EQ(112108372, soa.serial()); + EXPECT_EQ(900, soa.refresh()); + EXPECT_EQ(900, soa.retry()); + EXPECT_EQ(1800, soa.expire()); + EXPECT_EQ(60, soa.minimum_ttl()); +} + +TEST_F(DNSTest, SOARecordSerialize) { + DNS::soa_record r1; + r1.mname("hehehehe.example.com"); + r1.rname("john.example.com"); + r1.serial(0x9823ade9); + r1.refresh(0x918273aa); + r1.retry(0x827361ad); + r1.expire(0x8ad71928); + r1.minimum_ttl(0x1ad92871); + + DNS::serialization_type buffer = r1.serialize(); + DNS::soa_record r2(&buffer[0], buffer.size()); + EXPECT_EQ("hehehehe.example.com", r2.mname()); + EXPECT_EQ("john.example.com", r2.rname()); + EXPECT_EQ(0x9823ade9, r2.serial()); + EXPECT_EQ(0x918273aa, r2.refresh()); + EXPECT_EQ(0x827361ad, r2.retry()); + EXPECT_EQ(0x8ad71928, r2.expire()); + EXPECT_EQ(0x1ad92871, r2.minimum_ttl()); +}