From: Piotrek Zadroga Date: Fri, 14 Apr 2023 15:38:12 +0000 (+0200) Subject: [#2536] Implementing DNRv6 Option with TDD X-Git-Tag: Kea-2.3.8~179 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=340c1e3ea6a4750d1ff86ffd886caa24b394542d;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 314327a32c..6a1d6a95a3 100644 --- a/src/lib/dhcp/option_dnr.cc +++ b/src/lib/dhcp/option_dnr.cc @@ -12,6 +12,8 @@ #include #include #include +#include +#include using namespace isc::asiolink; @@ -23,6 +25,21 @@ OptionDnr6::OptionDnr6(OptionBufferConstIter begin, OptionBufferConstIter end) unpack(begin, end); } +OptionDnr6::OptionDnr6(const uint16_t service_priority, + const std::string& adn, + const OptionDnr6::AddressContainer& ipv6_addresses, + const std::string& svc_params) + : Option(V6, D6O_V6_DNR), service_priority_(service_priority), + ipv6_addresses_(ipv6_addresses), svc_params_(svc_params) { + setAdn(adn); + checkFields(); +} + +OptionDnr6::OptionDnr6(const uint16_t service_priority, const std::string& adn) + : Option(V6, D6O_V6_DNR), service_priority_(service_priority) { + setAdn(adn); +} + OptionPtr OptionDnr6::clone() const { return (cloneInternal()); @@ -205,8 +222,137 @@ OptionDnr6::getAdn() const { } void -OptionDnr6::checkSvcParams() const { - // TODO: check SvcParams and throw in case something is wrong +OptionDnr6::checkSvcParams(bool from_wire_data) { + std::string svc_params = isc::util::str::trim(svc_params_); + if (svc_params.empty()) { + isc_throw(InvalidOptionDnrSvcParams, "Provided Svc Params field is empty"); + } + if (!from_wire_data) { + // If Service Params field was not parsed from on-wire data, + // but actually was provided with ctor, let's calculate svc_params_length_. + auto svc_params_len = svc_params.length(); + if (svc_params_len > std::numeric_limits::max()) { + isc_throw(OutOfRange, "Given Svc Params length " + << svc_params_len << " is bigger than uint_16 MAX"); + } + svc_params_length_ = svc_params_len; + // If Service Params field was not parsed from on-wire data, + // but actually was provided with ctor, let's replace it with trimmed value. + svc_params_ = svc_params; + } + + // SvcParams are a whitespace-separated list, with each SvcParam + // consisting of a SvcParamKey=SvcParamValue pair or a standalone SvcParamKey. + // SvcParams in presentation format MAY appear in any order, but keys MUST NOT be repeated. + + // Let's put all elements of a whitespace-separated list into a vector. + std::vector tokens = isc::util::str::tokens(svc_params, " "); + + // Set of keys used to check if a key is not repeated. + std::set keys; + // String sanitizer is used to check keys syntax. + util::str::StringSanitizerPtr sanitizer; + // SvcParamKeys are lower-case alphanumeric strings. Key names + // contain 1-63 characters from the ranges "a"-"z", "0"-"9", and "-". + std::string regex = "[^a-z0-9-]"; + sanitizer.reset(new util::str::StringSanitizer(regex, "")); + // The service parameters MUST NOT include + // "ipv4hint" or "ipv6hint" SvcParams as they are superseded by the + // included IP addresses. + std::set forbidden_keys = {"ipv4hint", "ipv6hint"}; + + // Now let's check each SvcParamKey=SvcParamValue pair. + for (const std::string& token : tokens) { + std::vector key_val = isc::util::str::tokens(token, "="); + if (key_val.size() > 2) { + isc_throw(InvalidOptionDnrSvcParams, "Wrong Svc Params syntax - more than one " + "equals sign found in SvcParamKey=SvcParamValue " + "pair"); + } + + // SvcParam Key related checks come below. + std::string key = key_val[0]; + if (forbidden_keys.find(key) != forbidden_keys.end()) { + isc_throw(InvalidOptionDnrSvcParams, "Wrong Svc Params syntax - key " + << key << " must not be used"); + } + + auto insert_res = keys.insert(key); + if (!insert_res.second) { + isc_throw(InvalidOptionDnrSvcParams, "Wrong Svc Params syntax - key " + << key << " was duplicated"); + } + + if (key.length() > 63) { + isc_throw(InvalidOptionDnrSvcParams, "Wrong Svc Params syntax - key had more than 63 " + "characters - " << key); + } + + std::string sanitized_key = sanitizer->scrub(key); + if (sanitized_key.size() < key.size()) { + isc_throw(InvalidOptionDnrSvcParams, "Wrong Svc Params syntax - invalid character " + "used in key - " << key); + } + + if (key_val.size() == 2) { + // tbd Check value syntax + std::string value = key_val[1]; + } + } + +} + +void +OptionDnr6::setAdn(const std::string& adn) { + std::string trimmed_adn = isc::util::str::trim(adn); + if (trimmed_adn.empty()) { + isc_throw(InvalidOptionDnrDomainName, "Mandatory Authentication Domain Name fully " + "qualified domain-name must not be empty"); + } + try { + adn_.reset(new isc::dns::Name(trimmed_adn, true)); + } catch (const Exception& ex) { + isc_throw(InvalidOptionDnrDomainName, "Failed to parse " + "fully qualified domain-name from string " + "- " << ex.what()); + } + size_t adn_len = 0; + isc::dns::LabelSequence label_sequence(*adn_); + label_sequence.getData(&adn_len); + if (adn_len > std::numeric_limits::max()) { + isc_throw(InvalidOptionDnrDomainName, "Given ADN FQDN length " + << adn_len << " is bigger than uint_16 MAX"); + } + + adn_length_ = adn_len; +} + +void +OptionDnr6::checkFields() { + if (svc_params_.empty() && ipv6_addresses_.empty()) { + // ADN only mode, nothing more to do. + return; + } + if(!svc_params_.empty() && ipv6_addresses_.empty()) { + // As per draft-ietf-add-dnr 3.1.8: + // If additional data is supplied (i.e. not ADN only mode), + // the option includes at least one valid IP address. + isc_throw(OutOfRange, "DHCPv6 Encrypted DNS Option (" + << type_ << ")" + << " malformed: No IPv6 address given. Since this is not " + "ADN only mode, at least one valid IP address must be included"); + + } + if(!svc_params_.empty()) { + checkSvcParams(false); + } + adn_only_mode_ = false; + auto addr_len = ipv6_addresses_.size() * V6ADDRESS_LEN; + if (addr_len > std::numeric_limits::max()) { + isc_throw(OutOfRange, "Given IPv6 addresses length " + << addr_len << " is bigger than uint_16 MAX"); + } + addr_length_ = addr_len; } OptionDnr4::OptionDnr4() : Option(V4, DHO_V4_DNR) { diff --git a/src/lib/dhcp/option_dnr.h b/src/lib/dhcp/option_dnr.h index efc1c40c5b..537548508a 100644 --- a/src/lib/dhcp/option_dnr.h +++ b/src/lib/dhcp/option_dnr.h @@ -22,6 +22,14 @@ public: } }; +/// @brief Exception thrown when Service parameters have wrong format. +class InvalidOptionDnrSvcParams : public Exception { +public: + InvalidOptionDnrSvcParams(const char* file, size_t line, const char* what) + : isc::Exception(file, line, what) { + } +}; + /// @brief Represents DHCPv6 Encrypted DNS %Option (code 144). /// /// This option has been defined in the draft-ietf-add-dnr-15 (to be replaced @@ -58,8 +66,12 @@ public: /// @param end Iterator pointing to the end of the buffer holding an option. OptionDnr6(OptionBufferConstIter begin, OptionBufferConstIter end); + OptionDnr6(const uint16_t service_priority, const std::string& adn, const AddressContainer& ipv6_addresses, const std::string& svc_params); + + OptionDnr6(const uint16_t service_priority, const std::string& adn); + virtual OptionPtr clone() const; - virtual void pack(util::OutputBuffer& buf, bool check) const; + virtual void pack(util::OutputBuffer& buf, bool check = false) const; virtual void unpack(OptionBufferConstIter begin, OptionBufferConstIter end); virtual std::string toText(int indent = 0) const; virtual uint16_t len() const; @@ -190,7 +202,18 @@ private: /// /// The field should be encoded following the rules in /// Section 2.1 of [I-D.ietf-dnsop-svcb-https]. - void checkSvcParams() const; + void checkSvcParams(bool from_wire_data = true); + + /// @brief Sets Authentication domain name from given string. + /// + /// Sets FQDN of the encrypted DNS resolver from given string. + /// It may throw an exception if parsing of the FQDN fails or if + /// provided FQDN length is bigger than uint16_t Max. + /// It also calculates and sets value of Addr length field. + /// + /// @param adn string representation of ADN FQDN + void setAdn(const std::string& adn); + void checkFields(); }; class OptionDnr4 : public Option { diff --git a/src/lib/dhcp/tests/option_dnr_unittest.cc b/src/lib/dhcp/tests/option_dnr_unittest.cc index 75ff6556e9..37894ea273 100644 --- a/src/lib/dhcp/tests/option_dnr_unittest.cc +++ b/src/lib/dhcp/tests/option_dnr_unittest.cc @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -23,7 +24,7 @@ namespace { // Provided wire data is in the ADN only mode i.e. only // Service priority and Authentication domain name FQDN // fields are present. -TEST(OptionDnr6Test, constructorAdnOnlyMode) { +TEST(OptionDnr6Test, onWireCtorAdnOnlyMode) { // Prepare data to decode - ADN only mode. const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -64,7 +65,7 @@ TEST(OptionDnr6Test, constructorAdnOnlyMode) { "adn='myhost.example.com.'", option->toText()); } -TEST(OptionDnr6Test, constructorDataTruncated) { +TEST(OptionDnr6Test, onWireCtorDataTruncated) { // Prepare data to decode - data too short. const uint8_t buf_data[] = { 0x80, 0x01 // Service priority is 32769 dec, other data is missing @@ -77,7 +78,7 @@ TEST(OptionDnr6Test, constructorDataTruncated) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, onlyWhitespaceFqdn) { +TEST(OptionDnr6Test, onWireCtorOnlyWhitespaceFqdn) { // Prepare data to decode - ADN only mode. const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -92,7 +93,7 @@ TEST(OptionDnr6Test, onlyWhitespaceFqdn) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, noAdnFqdn) { +TEST(OptionDnr6Test, onWireCtorNoAdnFqdn) { // Prepare data to decode - ADN only mode. const uint8_t buf_data[] = { 0x00, 0x01, // Service priority is 1 dec @@ -108,7 +109,7 @@ TEST(OptionDnr6Test, noAdnFqdn) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, truncatedFqdn) { +TEST(OptionDnr6Test, onWireCtorTruncatedFqdn) { // Prepare data to decode - ADN only mode. const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -123,7 +124,7 @@ TEST(OptionDnr6Test, truncatedFqdn) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, addrLenTruncated) { +TEST(OptionDnr6Test, onWireCtorAddrLenTruncated) { // Prepare data to decode const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -141,7 +142,7 @@ TEST(OptionDnr6Test, addrLenTruncated) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, addrLenZero) { +TEST(OptionDnr6Test, onWireCtorAddrLenZero) { // Prepare data to decode const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -159,7 +160,7 @@ TEST(OptionDnr6Test, addrLenZero) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, addrLenNot16Modulo) { +TEST(OptionDnr6Test, onWireCtorAddrLenNot16Modulo) { // Prepare data to decode const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -177,7 +178,7 @@ TEST(OptionDnr6Test, addrLenNot16Modulo) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, validIpV6Addresses) { +TEST(OptionDnr6Test, onWireCtorValidIpV6Addresses) { // Prepare data to decode const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -232,7 +233,7 @@ TEST(OptionDnr6Test, validIpV6Addresses) { "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", option->toText()); } -TEST(OptionDnr6Test, truncatedIpV6Addresses) { +TEST(OptionDnr6Test, onWireCtorTruncatedIpV6Addresses) { // Prepare data to decode const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -253,7 +254,7 @@ TEST(OptionDnr6Test, truncatedIpV6Addresses) { ASSERT_FALSE(option); } -TEST(OptionDnr6Test, svcParamsIncluded) { +TEST(OptionDnr6Test, onWireCtorSvcParamsIncluded) { // Prepare data to decode const uint8_t buf_data[] = { 0x80, 0x01, // Service priority is 32769 dec @@ -261,7 +262,7 @@ TEST(OptionDnr6Test, svcParamsIncluded) { 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, 0x10, // Addr Len field value = 48 dec + 0x00, 0x10, // Addr Len field value = 16 dec 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01, 0x00, 0x00, // 2001:db8:1::dead:beef 0x00, 0x00, 0x00, 0x00, 0xde, 0xad, 0xbe, 0xef, 'a', 'b', 'c' // example SvcParams data @@ -302,4 +303,181 @@ TEST(OptionDnr6Test, svcParamsIncluded) { "svc_params='abc'", option->toText()); } +TEST(OptionDnr6Test, onWireCtorSvcParamsInvalidCharKey) { + // Prepare data to decode with invalid SvcParams + 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, 0x10, // 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, + 'a', '+', 'c' // Allowed "a"-"z", "0"-"9", and "-". + }; + + OptionBuffer buf(buf_data, buf_data + sizeof(buf_data)); + // Create option instance. Check that constructor throws InvalidOptionDnrSvcParams exception. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(buf.begin(), buf.end())), InvalidOptionDnrSvcParams); + ASSERT_FALSE(option); +} + +// This test verifies option constructor in ADN only mode. +// Service priority and ADN are provided via ctor. +TEST(OptionDnr6Test, adnOnlyModeCtor) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = "myhost.example.com."; + + // Create option instance. Check that constructor doesn't throw. + scoped_ptr option; + EXPECT_NO_THROW(option.reset(new OptionDnr6(service_priority, adn))); + 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()); + EXPECT_EQ(service_priority, option->getServicePriority()); + EXPECT_EQ(20, option->getAdnLength()); + EXPECT_EQ(adn, option->getAdn()); + + // This is ADN only mode, so Addr Length and SvcParams Length + // are both expected to be zero. + EXPECT_EQ(0, option->getAddrLength()); + EXPECT_EQ(0, option->getSvcParamsLength()); + + // BTW let's check if len() works ok. + // expected len: 20 (FQDN) + 2 (ADN Len) + 2 (Service priority) + 4 (headers) = 28. + EXPECT_EQ(28, option->len()); + + // BTW let's check if toText() works ok. + // toText() len does not count in headers len. + EXPECT_EQ("type=144(V6_DNR), len=24, " + "service_priority=9, adn_length=20, " + "adn='myhost.example.com.'", option->toText()); +} + +TEST(OptionDnr6Test, adnOnlyModeCtorNoFqdn) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = ""; // invalid empty ADN + + // Create option instance. Check that constructor throws. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(service_priority, adn)), InvalidOptionDnrDomainName); + ASSERT_FALSE(option); +} + +// This test verifies option constructor where all fields +// i.e. Service priority, ADN, IP address(es) and Service params +// are provided as ctor parameters. +TEST(OptionDnr6Test, allFieldsCtor) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = "myhost.example.com."; + OptionDnr6::AddressContainer addresses; + addresses.push_back(isc::asiolink::IOAddress("2001:db8:1::baca")); + const std::string svc_params = "alpn"; + + // Create option instance. Check that constructor throws. + scoped_ptr option; + EXPECT_NO_THROW(option.reset(new OptionDnr6(service_priority, adn, addresses, svc_params))); + 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()); + EXPECT_EQ(service_priority, option->getServicePriority()); + EXPECT_EQ(20, option->getAdnLength()); + EXPECT_EQ(adn, option->getAdn()); + EXPECT_EQ(16, option->getAddrLength()); + EXPECT_EQ(4, option->getSvcParamsLength()); + + // BTW let's check if len() works ok. + // expected len: 20 (FQDN) + 2 (ADN Len) + 2 (Service priority) + 4 (headers) = 28 + // + 16 (IPv6) + 2 (Addr Len) + 4 (Svc Params) = 50 + EXPECT_EQ(50, option->len()); + + // BTW let's check if toText() works ok. + // toText() len does not count in headers len. + EXPECT_EQ("type=144(V6_DNR), len=46, " + "service_priority=9, adn_length=20, " + "adn='myhost.example.com.', addr_length=16, " + "address(es): 2001:db8:1::baca, svc_params='alpn'", option->toText()); +} + +TEST(OptionDnr6Test, allFieldsCtorNoIpAddress) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = "myhost.example.com."; + const OptionDnr6::AddressContainer addresses; // no IPv6 address in here + const std::string svc_params = "alpn"; + + // Create option instance. Check that constructor throws. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(service_priority, adn, addresses, svc_params)), OutOfRange); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, svcParamsTwoEqualSignsPerParam) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = "myhost.example.com."; + OptionDnr6::AddressContainer addresses; + addresses.push_back(isc::asiolink::IOAddress("2001:db8:1::baca")); + const std::string svc_params = "key123=val1=val2 key234"; // invalid svc param - 2 equal signs + + // Create option instance. Check that constructor throws. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(service_priority, adn, addresses, svc_params)), InvalidOptionDnrSvcParams); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, svcParamsForbiddenKey) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = "myhost.example.com."; + OptionDnr6::AddressContainer addresses; + addresses.push_back(isc::asiolink::IOAddress("2001:db8:1::baca")); + const std::string svc_params = "key123=val1 ipv6hint"; // forbidden svc param key - ipv6hint + + // Create option instance. Check that constructor throws. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(service_priority, adn, addresses, svc_params)), InvalidOptionDnrSvcParams); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, svcParamsKeyRepeated) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = "myhost.example.com."; + OptionDnr6::AddressContainer addresses; + addresses.push_back(isc::asiolink::IOAddress("2001:db8:1::baca")); + const std::string svc_params = "key123=val1 key234 key123"; // svc param key key123 repeated + + // Create option instance. Check that constructor throws. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(service_priority, adn, addresses, svc_params)), InvalidOptionDnrSvcParams); + ASSERT_FALSE(option); +} + +TEST(OptionDnr6Test, svcParamsKeyTooLong) { + // Prepare example parameters + const uint16_t service_priority = 9; + const std::string adn = "myhost.example.com."; + OptionDnr6::AddressContainer addresses; + addresses.push_back(isc::asiolink::IOAddress("2001:db8:1::baca")); + const std::string svc_params = "thisisveryveryveryvery" + "veryveryveryveryveryvery" + "veryveryveryveryveryvery" + "veryveryverylongkey"; // svc param key longer than 63 + + // Create option instance. Check that constructor throws. + scoped_ptr option; + EXPECT_THROW(option.reset(new OptionDnr6(service_priority, adn, addresses, svc_params)), InvalidOptionDnrSvcParams); + ASSERT_FALSE(option); +} + } // namespace \ No newline at end of file