From: Piotrek Zadroga Date: Fri, 7 Apr 2023 14:03:50 +0000 (+0200) Subject: [#2536] Implementing DNRv6 Option with TDD X-Git-Tag: Kea-2.3.8~184 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1b255a15548b89608f0ac49aab0e36fd9a8a0b3e;p=thirdparty%2Fkea.git [#2536] Implementing DNRv6 Option with TDD --- diff --git a/src/lib/dhcp/option_dnr.cc b/src/lib/dhcp/option_dnr.cc index 427bf0feef..83fe76fef1 100644 --- a/src/lib/dhcp/option_dnr.cc +++ b/src/lib/dhcp/option_dnr.cc @@ -6,16 +6,19 @@ #include +#include #include #include #include #include +using namespace isc::asiolink; + namespace isc { namespace dhcp { OptionDnr6::OptionDnr6(OptionBufferConstIter begin, OptionBufferConstIter end) - : Option(V6, D6O_V6_DNR), addr_length_(0) { + : Option(V6, D6O_V6_DNR) { unpack(begin, end); } @@ -35,42 +38,67 @@ OptionDnr6::pack(util::OutputBuffer& buf, bool check) const { void OptionDnr6::unpack(OptionBufferConstIter begin, OptionBufferConstIter end) { if (std::distance(begin, end) < getMinimalLength()) { - isc_throw(OutOfRange, "parsed DHCPv6 Encrypted DNS Option (" - << D6O_V6_DNR << ") data truncated to" + isc_throw(OutOfRange, "DHCPv6 Encrypted DNS Option (" << type_ << ") malformed: " + "data truncated to" " size " << std::distance(begin, end)); } setData(begin, end); // First two octets of Option data is Service Priority - this is mandatory field. service_priority_ = isc::util::readUint16((&*begin), SERVICE_PRIORITY_SIZE); - begin += sizeof(service_priority_); + begin += SERVICE_PRIORITY_SIZE; - // Next comes two octets of ADN Length plus the ADN data itself (variable length). + // Next come two octets of ADN Length plus the ADN data itself (variable length). // This is Opaque Data Tuple so let's use this class to retrieve the ADN data. OpaqueDataTuple adn_tuple(OpaqueDataTuple::LENGTH_2_BYTES, begin, end); adn_length_ = adn_tuple.getLength(); - if (adn_length_ > 0) { - // Let's try to extract ADN FQDN data - isc::util::InputBuffer name_buf(&adn_tuple.getData()[0], adn_length_); - try { - adn_.reset(new isc::dns::Name(name_buf, true)); - } catch (const Exception& ex) { - isc_throw(InvalidOptionDnr6DomainName, "failed to parse " - "fully qualified domain-name from wire format " - "- " << ex.what()); - } + + // Encrypted DNS options are designed to always include an authentication domain name, + // so when there is no FQDN included, we shall throw an exception. + if (adn_length_ == 0) { + isc_throw(InvalidOptionDnr6DomainName, "Mandatory Authentication Domain Name fully " + "qualified domain-name is missing"); } + + // Let's try to extract ADN FQDN data. + isc::util::InputBuffer name_buf(&adn_tuple.getData()[0], adn_length_); + try { + adn_.reset(new isc::dns::Name(name_buf, true)); + } catch (const Exception& ex) { + isc_throw(InvalidOptionDnr6DomainName, "failed to parse " + "fully qualified domain-name from wire format " + "- " << ex.what()); + } + begin += adn_tuple.getTotalLength(); if (begin == end) { - // ADN only mode, other fields are not included + // ADN only mode, other fields are not included. return; } if (std::distance(begin, end) < ADDR_LENGTH_SIZE) { - isc_throw(OutOfRange, "truncated DHCPv6 Encrypted DNS Option (" << D6O_V6_DNR << ") - after" - " ADN field, there should be at least 2 bytes long Addr Length field"); + isc_throw(OutOfRange, "DHCPv6 Encrypted DNS Option (" << type_ << ") malformed: after" + " ADN field, there should be at least " + "2 bytes long Addr Length field"); } + // Next come two octets of Addr Length. addr_length_ = isc::util::readUint16((&*begin), ADDR_LENGTH_SIZE); - // TBD + begin += ADDR_LENGTH_SIZE; + // It MUST be a multiple of 16. + if ((addr_length_ % V6ADDRESS_LEN) != 0) { + isc_throw(OutOfRange, "DHCPv6 Encrypted DNS Option (" << type_ << ")" + << " malformed: Addr Len=" << addr_length_ + << " is not divisible by 16."); + } + + // Let's unpack the ipv6-address(es). + auto addr_end = begin + addr_length_; + while (begin != addr_end) { + ipv6_addresses_.push_back(IOAddress::fromBytes(AF_INET6, &(*begin))); + begin += V6ADDRESS_LEN; + } + + // SvcParams (variable length) field is last + svc_params_length_ = std::distance(begin, end); } std::string diff --git a/src/lib/dhcp/option_dnr.h b/src/lib/dhcp/option_dnr.h index 45f3807c42..a1b3e2896b 100644 --- a/src/lib/dhcp/option_dnr.h +++ b/src/lib/dhcp/option_dnr.h @@ -7,6 +7,7 @@ #ifndef OPTION_DNR_H #define OPTION_DNR_H +#include #include #include @@ -23,6 +24,9 @@ public: class OptionDnr6 : public Option { public: + /// a container for (IPv6) addresses + typedef std::vector AddressContainer; + /// @brief Size in octets of Service Priority field static const uint8_t SERVICE_PRIORITY_SIZE = 2; @@ -67,6 +71,25 @@ public: return addr_length_; } + /// @brief Getter of the @c svc_params_length_ + /// + /// @return Length of Service Parameters field in octets. + uint16_t getSvcParamsLength() const { + return svc_params_length_; + } + + /// @brief Returns vector with addresses. + /// + /// We return a copy of our list. Although this includes overhead, + /// it also makes this list safe to use after this option object + /// is no longer available. As options are expected to hold only + /// a few (1-3) addresses, the overhead is not that big. + /// + /// @return address container with addresses + AddressContainer getAddresses() const { + return ipv6_addresses_; + } + private: /// @brief The priority of this OPTION_V6_DNR instance compared to other instances. uint16_t service_priority_; @@ -82,7 +105,16 @@ private: boost::shared_ptr adn_; /// @brief Length of enclosed IPv6 addresses in octets. - uint16_t addr_length_; + uint16_t addr_length_ = 0; + + /// @brief Vector container holding one or more IPv6 addresses + /// + /// One or more IPv6 addresses to reach the encrypted DNS resolver. + /// An address can be link-local, ULA, or GUA. + AddressContainer ipv6_addresses_; + + /// @brief Length of Service Parameters field in octets. + uint16_t svc_params_length_ = 0; /// @brief Returns minimal length of the option data (without headers) in octets. /// diff --git a/src/lib/dhcp/tests/option_dnr_unittest.cc b/src/lib/dhcp/tests/option_dnr_unittest.cc index 5ac3eb63f8..e8d69cd1c5 100644 --- a/src/lib/dhcp/tests/option_dnr_unittest.cc +++ b/src/lib/dhcp/tests/option_dnr_unittest.cc @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -47,6 +48,7 @@ TEST(OptionDnr6Test, constructorAdnOnlyMode) { EXPECT_EQ(20, option->getAdnLength()); EXPECT_EQ("myhost.example.com.", option->getAdn()); EXPECT_EQ(0, option->getAddrLength()); + EXPECT_EQ(0, option->getSvcParamsLength()); } TEST(OptionDnr6Test, constructorDataTruncated) { @@ -77,4 +79,141 @@ TEST(OptionDnr6Test, onlyWhitespaceFqdn) { ASSERT_FALSE(option); } +TEST(OptionDnr6Test, noAdnFqdn) { + // Prepare data to decode - ADN only mode. + const uint8_t buf_data[] = { + 0x00, 0x01, // Service priority is 1 dec + 0x00, 0x00 // ADN Length is 0 dec + }; + + OptionBuffer buf(buf_data, buf_data + sizeof(buf_data)); + // Create option instance. Encrypted DNS options are designed to ALWAYS include + // an authentication domain name, so check that constructor throws + // InvalidOptionDnr6DomainName exception. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(buf.begin(), buf.end())), InvalidOptionDnr6DomainName); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, truncatedFqdn) { + // Prepare data to decode - ADN only mode. + const uint8_t buf_data[] = { + 0x80, 0x01, // Service priority is 32769 dec + 0x00, 0x14, // ADN Length is 20 dec + 0x06, 0x4D, 0x79, 0x68, 0x6F, 0x73, 0x74 // FQDN data is truncated + }; + + OptionBuffer buf(buf_data, buf_data + sizeof(buf_data)); + // Create option instance. Check that constructor throws OpaqueDataTupleError exception. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(buf.begin(), buf.end())), OpaqueDataTupleError); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, addrLenTruncated) { + // Prepare data to decode + const uint8_t buf_data[] = { + 0x80, 0x01, // Service priority is 32769 dec + 0x00, 0x14, // ADN Length is 20 dec + 0x06, 0x4D, 0x79, 0x68, 0x6F, 0x73, 0x74, // FQDN: Myhost. + 0x07, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // Example. + 0x03, 0x43, 0x6F, 0x6D, 0x00, // Com. + 0x10 // Truncated Addr Len field + }; + + OptionBuffer buf(buf_data, buf_data + sizeof(buf_data)); + // Create option instance. Check that constructor throws OutOfRange exception. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(buf.begin(), buf.end())), OutOfRange); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, addrLenZero) { + // Prepare data to decode + const uint8_t buf_data[] = { + 0x80, 0x01, // Service priority is 32769 dec + 0x00, 0x14, // ADN Length is 20 dec + 0x06, 0x4D, 0x79, 0x68, 0x6F, 0x73, 0x74, // FQDN: Myhost. + 0x07, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // Example. + 0x03, 0x43, 0x6F, 0x6D, 0x00, // Com. + 0x00, 0x00 // Addr Len field value = 0 + }; + + OptionBuffer buf(buf_data, buf_data + sizeof(buf_data)); + // Create option instance. Check that constructor doesn't throw. + scoped_ptr option; + EXPECT_NO_THROW(option.reset(new OptionDnr6(buf.begin(), buf.end()))); + ASSERT_TRUE(option); + + // Check if member variables were correctly set by ctor. + EXPECT_EQ(Option::V6, option->getUniverse()); + EXPECT_EQ(D6O_V6_DNR, option->getType()); + + // Check if data was unpacked correctly from wire data. + EXPECT_EQ(0x8001, option->getServicePriority()); + EXPECT_EQ(20, option->getAdnLength()); + EXPECT_EQ("myhost.example.com.", option->getAdn()); + EXPECT_EQ(0, option->getAddrLength()); + EXPECT_EQ(0, option->getSvcParamsLength()); +} + +TEST(OptionDnr6Test, addrLenNot16Modulo) { + // Prepare data to decode + const uint8_t buf_data[] = { + 0x80, 0x01, // Service priority is 32769 dec + 0x00, 0x14, // ADN Length is 20 dec + 0x06, 0x4D, 0x79, 0x68, 0x6F, 0x73, 0x74, // FQDN: Myhost. + 0x07, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // Example. + 0x03, 0x43, 0x6F, 0x6D, 0x00, // Com. + 0xFF, 0xFE // Addr Len is not a multiple of 16 + }; + + OptionBuffer buf(buf_data, buf_data + sizeof(buf_data)); + // Create option instance. Check that constructor throws OutOfRange exception. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(buf.begin(), buf.end())), OutOfRange); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, validIpV6Addresses) { + // Prepare data to decode + const uint8_t buf_data[] = { + 0x80, 0x01, // Service priority is 32769 dec + 0x00, 0x14, // ADN Length is 20 dec + 0x06, 0x4D, 0x79, 0x68, 0x6F, 0x73, 0x74, // FQDN: Myhost. + 0x07, 0x45, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, // Example. + 0x03, 0x43, 0x6F, 0x6D, 0x00, // Com. + 0x00, 0x30, // Addr Len field value = 48 dec + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00, // 2001:db8:1::dead:beef + 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, + 0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // ff02::face:b00c + 0x00, 0x00, 0x00, 0x00, 0xfa, 0xce, 0xb0, 0x0c, + // ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff + }; + + OptionBuffer buf(buf_data, buf_data + sizeof(buf_data)); + // Create option instance. Check that constructor doesn't throw. + scoped_ptr option; + EXPECT_NO_THROW(option.reset(new OptionDnr6(buf.begin(), buf.end()))); + ASSERT_TRUE(option); + + // Check if member variables were correctly set by ctor. + EXPECT_EQ(Option::V6, option->getUniverse()); + EXPECT_EQ(D6O_V6_DNR, option->getType()); + + // Check if data was unpacked correctly from wire data. + EXPECT_EQ(0x8001, option->getServicePriority()); + EXPECT_EQ(20, option->getAdnLength()); + EXPECT_EQ("myhost.example.com.", option->getAdn()); + EXPECT_EQ(48, option->getAddrLength()); + const OptionDnr6::AddressContainer& addresses = option->getAddresses(); + EXPECT_EQ(3, addresses.size()); + EXPECT_EQ("2001:db8:1::dead:beef", addresses[0].toText()); + EXPECT_EQ("ff02::face:b00c", addresses[1].toText()); + EXPECT_EQ("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", addresses[2].toText()); + EXPECT_EQ(0, option->getSvcParamsLength()); +} + } // namespace \ No newline at end of file