diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:15:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-13 12:15:43 +0000 |
commit | f5f56e1a1c4d9e9496fcb9d81131066a964ccd23 (patch) | |
tree | 49e44c6f87febed37efb953ab5485aa49f6481a7 /src/lib/dhcp/option4_dnr.cc | |
parent | Initial commit. (diff) | |
download | isc-kea-f5f56e1a1c4d9e9496fcb9d81131066a964ccd23.tar.xz isc-kea-f5f56e1a1c4d9e9496fcb9d81131066a964ccd23.zip |
Adding upstream version 2.4.1.upstream/2.4.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/lib/dhcp/option4_dnr.cc')
-rw-r--r-- | src/lib/dhcp/option4_dnr.cc | 489 |
1 files changed, 489 insertions, 0 deletions
diff --git a/src/lib/dhcp/option4_dnr.cc b/src/lib/dhcp/option4_dnr.cc new file mode 100644 index 0000000..f1b50cb --- /dev/null +++ b/src/lib/dhcp/option4_dnr.cc @@ -0,0 +1,489 @@ +// Copyright (C) 2023 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include <config.h> + +#include <dhcp/option4_dnr.h> +#include <dns/labelsequence.h> +#include <util/strutil.h> + +using namespace isc::asiolink; +using namespace isc::util; + +namespace isc { +namespace dhcp { + +Option4Dnr::Option4Dnr(OptionBufferConstIter begin, OptionBufferConstIter end) + : Option(V4, DHO_V4_DNR) { + unpack(begin, end); +} + +OptionPtr +Option4Dnr::clone() const { + return (cloneInternal<Option4Dnr>()); +} + +void +Option4Dnr::pack(OutputBuffer& buf, bool check) const { + packHeader(buf, check); + for (const DnrInstance& dnr_instance : dnr_instances_) { + buf.writeUint16(dnr_instance.getDnrInstanceDataLength()); + buf.writeUint16(dnr_instance.getServicePriority()); + buf.writeUint8(dnr_instance.getAdnLength()); + dnr_instance.packAdn(buf); + if (dnr_instance.isAdnOnlyMode()) { + continue; + } + + buf.writeUint8(dnr_instance.getAddrLength()); + dnr_instance.packAddresses(buf); + dnr_instance.packSvcParams(buf); + } +} + +void +Option4Dnr::unpack(OptionBufferConstIter begin, OptionBufferConstIter end) { + setData(begin, end); + while (begin != end) { + DnrInstance dnr_instance(V4); + if (std::distance(begin, end) < dnr_instance.getMinimalLength()) { + isc_throw(OutOfRange, dnr_instance.getLogPrefix() + << "DNR instance data truncated to size " + << std::distance(begin, end)); + } + + // Unpack DnrInstanceDataLength. + dnr_instance.unpackDnrInstanceDataLength(begin, end); + + const OptionBufferConstIter dnr_instance_end = begin + + dnr_instance.getDnrInstanceDataLength(); + + // Unpack Service priority. + dnr_instance.unpackServicePriority(begin); + + // Unpack ADN len + ADN. + dnr_instance.unpackAdn(begin, dnr_instance_end); + + if (begin == dnr_instance_end) { + // ADN only mode, other fields are not included. + addDnrInstance(dnr_instance); + continue; + } + + dnr_instance.setAdnOnlyMode(false); + + // Unpack Addr Len + IPv4 Address(es). + dnr_instance.unpackAddresses(begin, dnr_instance_end); + + // SvcParams (variable length) field is last. + dnr_instance.unpackSvcParams(begin, dnr_instance_end); + + addDnrInstance(dnr_instance); + } +} + +std::string +Option4Dnr::toText(int indent) const { + std::ostringstream stream; + std::string in(indent, ' '); // base indentation + stream << in << "type=" << type_ << "(V4_DNR), " + << "len=" << (len() - getHeaderLen()); + int i = 0; + for (const DnrInstance& dnr_instance : dnr_instances_) { + stream << ", DNR Instance " << ++i + << "(Instance len=" << dnr_instance.getDnrInstanceDataLength() << ", " + << dnr_instance.getDnrInstanceAsText() << ")"; + } + + return (stream.str()); +} + +uint16_t +Option4Dnr::len() const { + uint16_t len = OPTION4_HDR_LEN; + for (const DnrInstance& dnr_instance : dnr_instances_) { + len += dnr_instance.getDnrInstanceDataLength() + + dnr_instance.getDnrInstanceDataLengthSize(); + } + + return (len); +} + +void +Option4Dnr::addDnrInstance(DnrInstance& dnr_instance) { + dnr_instances_.push_back(dnr_instance); +} + +const std::unordered_set<std::string> DnrInstance::FORBIDDEN_SVC_PARAMS = {"ipv4hint", "ipv6hint"}; + +DnrInstance::DnrInstance(Option::Universe universe) + : universe_(universe), dnr_instance_data_length_(0), service_priority_(0), + adn_length_(0), addr_length_(0), svc_params_length_(0), + adn_only_mode_(true), dnr_instance_data_length_size_(0), + adn_length_size_(0), addr_length_size_(0), minimal_length_(0) { + initMembers(); +} + +DnrInstance::DnrInstance(Option::Universe universe, + const uint16_t service_priority, + const std::string& adn, + const DnrInstance::AddressContainer& ip_addresses, + const std::string& svc_params) + : universe_(universe), dnr_instance_data_length_(0), + service_priority_(service_priority), adn_length_(0), + addr_length_(0), ip_addresses_(ip_addresses), svc_params_length_(0), + adn_only_mode_(true), svc_params_(svc_params), + dnr_instance_data_length_size_(0), adn_length_size_(0), + addr_length_size_(0), minimal_length_(0) { + initMembers(); + setAdn(adn); + checkFields(); +} + +DnrInstance::DnrInstance(Option::Universe universe, + const uint16_t service_priority, + const std::string& adn) + : universe_(universe), dnr_instance_data_length_(0), + service_priority_(service_priority), adn_length_(0), + addr_length_(0), svc_params_length_(0), adn_only_mode_(true), + dnr_instance_data_length_size_(0), adn_length_size_(0), + addr_length_size_(0), minimal_length_(0) { + initMembers(); + setAdn(adn); +} + +void +DnrInstance::packAdn(OutputBuffer& buf) const { + if (!adn_) { + // This should not happen since Encrypted DNS options are designed + // to always include an authentication domain name. + isc_throw(InvalidOptionDnrDomainName, getLogPrefix() + << "Mandatory Authentication Domain Name fully " + "qualified domain-name is missing"); + } + + isc::dns::LabelSequence label_sequence(*adn_); + if (label_sequence.getDataLength() > 0) { + size_t data_length = 0; + const uint8_t* data = label_sequence.getData(&data_length); + buf.writeData(data, data_length); + } +} + +void +DnrInstance::packAddresses(OutputBuffer& buf) const { + AddressContainer::const_iterator address = ip_addresses_.begin(); + while (address != ip_addresses_.end()) { + buf.writeUint32(address->toUint32()); + ++address; + } +} + +void +DnrInstance::packSvcParams(OutputBuffer& buf) const { + if (svc_params_length_ > 0) { + buf.writeData(&(*svc_params_.begin()), svc_params_length_); + } +} + +std::string +DnrInstance::getAdnAsText() const { + return (adn_) ? (adn_->toText()) : (""); +} + +void +DnrInstance::setAdn(const std::string& adn) { + std::string trimmed_adn = str::trim(adn); + if (trimmed_adn.empty()) { + isc_throw(InvalidOptionDnrDomainName, getLogPrefix() + << "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, getLogPrefix() + << "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<uint16_t>::max()) { + isc_throw(InvalidOptionDnrDomainName, getLogPrefix() << "Given ADN FQDN length " << adn_len + << " is bigger than uint_16 MAX"); + } + + adn_length_ = adn_len; + if (universe_ == Option::V4) { + dnr_instance_data_length_ = dnrInstanceLen(); + } +} + +void +DnrInstance::unpackAdn(OptionBufferConstIter& begin, OptionBufferConstIter end) { + OpaqueDataTuple::LengthFieldType lft = OptionDataTypeUtil::getTupleLenFieldType(universe_); + OpaqueDataTuple adn_tuple(lft); + try { + adn_tuple.unpack(begin, end); + } catch (const Exception& ex) { + isc_throw(BadValue, getLogPrefix() << "failed to unpack ADN data" + << " - " << ex.what()); + } + + adn_length_ = adn_tuple.getLength(); + + // 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(InvalidOptionDnrDomainName, getLogPrefix() + << "Mandatory Authentication Domain Name fully " + "qualified domain-name is missing"); + } + + InputBuffer name_buf(adn_tuple.getData().data(), adn_length_); + try { + adn_.reset(new isc::dns::Name(name_buf, true)); + } catch (const Exception& ex) { + isc_throw(InvalidOptionDnrDomainName, getLogPrefix() + << "Failed to parse " + "fully qualified domain-name from wire format " + "- " << ex.what()); + } + + begin += adn_length_ + getAdnLengthSize(); +} + +void +DnrInstance::checkSvcParams(bool from_wire_data) { + std::string svc_params = str::trim(svc_params_); + if (svc_params.empty()) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "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<uint16_t>::max()) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "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<std::string> tokens = str::tokens(svc_params, " "); + + // Set of keys used to check if a key is not repeated. + std::unordered_set<std::string> keys; + // String sanitizer is used to check keys syntax. + 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 str::StringSanitizer(regex, "")); + + // Now let's check each SvcParamKey=SvcParamValue pair. + for (const std::string& token : tokens) { + std::vector<std::string> key_val = str::tokens(token, "="); + if (key_val.size() > 2) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "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 (key.length() > 63) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - key had more than 63 " + "characters - " << key); + } + + if (FORBIDDEN_SVC_PARAMS.find(key) != FORBIDDEN_SVC_PARAMS.end()) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() << "Wrong Svc Params syntax - key " + << key << " must not be used"); + } + + auto insert_res = keys.insert(key); + if (!insert_res.second) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() << "Wrong Svc Params syntax - key " + << key << " was duplicated"); + } + + std::string sanitized_key = sanitizer->scrub(key); + if (sanitized_key.size() < key.size()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() + << "Wrong Svc Params syntax - invalid character used in key - " << key); + } + } +} + +void +DnrInstance::checkFields() { + if (svc_params_.empty() && ip_addresses_.empty()) { + // ADN only mode, nothing more to do. + return; + } + + if (!svc_params_.empty() && ip_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, getLogPrefix() << "No IP 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; + const uint8_t addr_field_len = (universe_ == Option::V4) ? V4ADDRESS_LEN : V6ADDRESS_LEN; + const uint16_t max_addr_len = (universe_ == Option::V4) ? std::numeric_limits<uint8_t>::max() : + std::numeric_limits<uint16_t>::max(); + auto addr_len = ip_addresses_.size() * addr_field_len; + if (addr_len > max_addr_len) { + isc_throw(OutOfRange, getLogPrefix() << "Given IP addresses length " << addr_len + << " is bigger than MAX " << max_addr_len); + } + + addr_length_ = addr_len; + if (universe_ == Option::V4) { + dnr_instance_data_length_ = dnrInstanceLen(); + } +} + +std::string +DnrInstance::getDnrInstanceAsText() const { + std::ostringstream stream; + stream << "service_priority=" << service_priority_ << ", adn_length=" << adn_length_ << ", " + << "adn='" << getAdnAsText() << "'"; + if (!adn_only_mode_) { + stream << ", addr_length=" << addr_length_ << ", address(es):"; + for (const auto& address : ip_addresses_) { + stream << " " << address.toText(); + } + + if (svc_params_length_ > 0) { + stream << ", svc_params='" + svc_params_ + "'"; + } + } + + return (stream.str()); +} + +uint16_t +DnrInstance::dnrInstanceLen() const { + uint16_t len = SERVICE_PRIORITY_SIZE + adn_length_ + getAdnLengthSize(); + if (!adn_only_mode_) { + len += addr_length_ + getAddrLengthSize() + svc_params_length_; + } + + return (len); +} + +void +DnrInstance::addIpAddress(const IOAddress& ip_address) { + ip_addresses_.push_back(ip_address); +} + +void +DnrInstance::unpackDnrInstanceDataLength(OptionBufferConstIter& begin, OptionBufferConstIter end) { + dnr_instance_data_length_ = readUint16(&*begin, getDnrInstanceDataLengthSize()); + begin += getDnrInstanceDataLengthSize(); + if (std::distance(begin, end) < dnr_instance_data_length_) { + isc_throw(OutOfRange, getLogPrefix() + << "DNR instance data truncated to size " + << std::distance(begin, end) << " but it was supposed to be " + << dnr_instance_data_length_); + } +} + +void +DnrInstance::unpackServicePriority(OptionBufferConstIter& begin) { + service_priority_ = readUint16(&*begin, SERVICE_PRIORITY_SIZE); + begin += SERVICE_PRIORITY_SIZE; +} + +void +DnrInstance::unpackAddresses(OptionBufferConstIter& begin, const OptionBufferConstIter end) { + OpaqueDataTuple addr_tuple(OpaqueDataTuple::LENGTH_1_BYTE); + try { + addr_tuple.unpack(begin, end); + } catch (const Exception& ex) { + isc_throw(BadValue, getLogPrefix() << "failed to unpack IP Addresses data" + << " - " << ex.what()); + } + + addr_length_ = addr_tuple.getLength(); + // It MUST be a multiple of 4. + if ((addr_length_ % V4ADDRESS_LEN) != 0) { + isc_throw(OutOfRange, getLogPrefix() + << "Addr Len=" << addr_length_ << " is not divisible by 4"); + } + + // 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. + if (addr_length_ == 0) { + isc_throw(OutOfRange, getLogPrefix() + << "Addr Len=" << addr_length_ + << " but it must contain at least one valid IP address"); + } + + begin += getAddrLengthSize(); + OptionBufferConstIter addr_begin = begin; + OptionBufferConstIter addr_end = addr_begin + addr_length_; + + while (addr_begin != addr_end) { + const uint8_t* ptr = &(*addr_begin); + addIpAddress(IOAddress(readUint32(ptr, std::distance(addr_begin, addr_end)))); + addr_begin += V4ADDRESS_LEN; + begin += V4ADDRESS_LEN; + } +} + +void +DnrInstance::unpackSvcParams(OptionBufferConstIter& begin, OptionBufferConstIter end) { + svc_params_length_ = std::distance(begin, end); + if (svc_params_length_ > 0) { + svc_params_.assign(begin, end); + checkSvcParams(); + begin += svc_params_length_; + } +} + +void +DnrInstance::initMembers() { + dnr_instance_data_length_size_ = (universe_ == Option::V6) ? 0 : 2; + adn_length_size_ = (universe_ == Option::V6) ? 2 : 1; + addr_length_size_ = (universe_ == Option::V6) ? 2 : 1; + minimal_length_ = dnr_instance_data_length_size_ + SERVICE_PRIORITY_SIZE + adn_length_size_; + log_prefix_ = + (universe_ == Option::V4) ? + ("DHCPv4 Encrypted DNS Option (" + std::to_string(DHO_V4_DNR) + ") malformed: ") : + ("DHCPv6 Encrypted DNS Option (" + std::to_string(D6O_V6_DNR) + ") malformed: "); +} + +} // namespace dhcp +} // namespace isc |