diff options
Diffstat (limited to 'src/lib/dhcp/option4_dnr.cc')
-rw-r--r-- | src/lib/dhcp/option4_dnr.cc | 868 |
1 files changed, 868 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..2c31662 --- /dev/null +++ b/src/lib/dhcp/option4_dnr.cc @@ -0,0 +1,868 @@ +// Copyright (C) 2023-2024 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/io.h> +#include <util/str.h> + +using namespace isc::asiolink; +using namespace isc::util; + +namespace isc { +namespace dhcp { + +Option4Dnr::Option4Dnr(OptionBufferConstIter begin, + OptionBufferConstIter end, + bool convenient_notation) + : Option(V4, DHO_V4_DNR), convenient_notation_(convenient_notation) { + 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) { + if (convenient_notation_) { + // parse convenient notation + std::string config_txt = std::string(begin, end); + parseConfigData(config_txt); + } else { + 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); +} + +void +Option4Dnr::parseConfigData(const std::string& config_txt) { + // This parses convenient option config notation. + // The config to be parsed may contain escaped characters like "\\," or "\\|". + // Example configs are below (first contains two DNR instances in one option with recommended + // resolvers' IP addresses, and SvcParams - DNR instances are separated with pipe "|" char; + // second is an example of ADN-only mode; + // third is like the first example, but for single DNR instance): + // + // "name": "v4-dnr", + // "data": "10, dot1.example.org., 10.0.2.3 10.3.4.5, alpn=dot\\,doq | 20, dot2.example.org., 10.0.2.3 10.3.4.5, alpn=dot" + // + // "name": "v4-dnr", + // "data": "200, resolver.example." + // + // "name": "v4-dnr", + // "data": "100, dot1.example.org., 10.0.3.4 10.1.5.6, alpn=dot\\,doq\\,h2\\,h3 port=8530 dohpath=/q{?dns}" + + // Get Dnr Instance tokens using pipe separator with double backslash escaping enabled. + std::vector<std::string> tokens = str::tokens(config_txt, std::string("|"), true); + + for (auto const& txt_dnr_instance : tokens) { + DnrInstance dnr_instance(V4); + dnr_instance.parseDnrInstanceConfigData(txt_dnr_instance); + dnr_instance.setDnrInstanceDataLength(); + addDnrInstance(dnr_instance); + } +} + +const std::unordered_set<std::string> DnrInstance::FORBIDDEN_SVC_PARAMS = {"ipv4hint", "ipv6hint"}; + +const DnrInstance::SvcParamsMap DnrInstance::SVC_PARAMS = + boost::assign::list_of<DnrInstance::SvcParamsMap::relation> + ("mandatory", 0) // RFC 9460, Section 14.3.2, not used in DNR + ("alpn", 1) // RFC 9460, Section 14.3.2, mandatory in DNR + ("no-default-alpn", 2) // RFC 9460, Section 14.3.2, not used in DNR + ("port", 3) // RFC 9460, Section 14.3.2, optional in DNR + ("ipv4hint", 4) // RFC 9460, Section 14.3.2, forbidden in DNR + ("ech", 5) // RFC 9460, Section 14.3.2, not used in DNR + ("ipv6hint", 6) // RFC 9460, Section 14.3.2, forbidden in DNR + ("dohpath", 7) // RFC 9461, optional in DNR + ("ohttp", 8) // https://datatracker.ietf.org/doc/draft-ietf-ohai-svcb-config, + // not used in DNR + ; + +const std::set<uint8_t> DnrInstance::SUPPORTED_SVC_PARAMS = {1, 3, 7}; + +const std::unordered_set<std::string> DnrInstance::ALPN_IDS = { + "http/0.9", // HTTP/0.9 + "http/1.0", // HTTP/1.0 + "http/1.1", // HTTP/1.1 + "spdy/1", // SPDY/1 + "spdy/2", // SPDY/2 + "spdy/3", // SPDY/3 + "stun.turn", // Traversal Using Relays around NAT (TURN) + "stun.nat-discovery", // NAT discovery using Session Traversal Utilities for NAT (STUN) + "h2", // HTTP/2 over TLS + "h2c", // HTTP/2 over TCP + "webrtc", // WebRTC Media and Data + "c-webrtc", // Confidential WebRTC Media and Data + "ftp", // FTP + "imap", // IMAP + "pop3", // POP3 + "managesieve", // ManageSieve + "coap", // CoAP + "xmpp-client", // XMPP jabber:client namespace + "xmpp-server", // XMPP jabber:server namespace + "acme-tls/1", // acme-tls/1 + "mqtt", // OASIS Message Queuing Telemetry Transport (MQTT) + "dot", // DNS-over-TLS + "ntske/1", // Network Time Security Key Establishment, version 1 + "sunrpc", // SunRPC + "h3", // HTTP/3 + "smb", // SMB2 + "irc", // IRC + "nntp", // NNTP (reading) + "nnsp", // NNTP (transit) + "doq", // DoQ + "sip/2", // SIP + "tds/8.0", // TDS/8.0 + "dicom" // DICOM +}; + +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), alpn_http_(false), + dnr_instance_data_length_size_(0), adn_length_size_(0), addr_length_size_(0), + minimal_length_(0) { + initMembers(); +} + +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 && !svc_params_buf_.empty()) { + buf.writeData(svc_params_buf_.data(), svc_params_length_); + } +} + +std::string +DnrInstance::getAdnAsText() const { + return (adn_) ? (adn_->toText()) : (""); +} + +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(); +} + +std::string +DnrInstance::svcParamValAsText(const std::pair<uint16_t, OpaqueDataTuple>& svc_param) const { + OptionBufferConstIter alpn_begin; + OptionBufferConstIter alpn_end; + std::ostringstream stream; + OpaqueDataTuple alpn_id_tuple(OpaqueDataTuple::LENGTH_1_BYTE); + bool first = true; + std::string ret; + + switch (svc_param.first) { + case 1: + // alpn + // read all protocols and concatenate them with comma + alpn_begin = svc_param.second.getData().begin(); + alpn_end = svc_param.second.getData().end(); + while (alpn_begin != alpn_end) { + try { + alpn_id_tuple.unpack(alpn_begin, alpn_end); + } catch (const Exception& e) { + isc_throw(BadValue, getLogPrefix() + << "Exception happened when tried to parse ALPN IDs" + << ". Error: " << e.what()); + } + + if (first) { + first = false; + } else { + stream << ","; + } + + stream << alpn_id_tuple.getText(); + alpn_begin += alpn_id_tuple.getTotalLength(); + } + + ret = stream.str(); + break; + case 3: + // port + // read uint16 from data buffer and return as string + ret = std::to_string( + readUint16(svc_param.second.getData().data(), svc_param.second.getLength())); + break; + case 7: + // dohpath + // conversion not needed, let's return data as string + ret = svc_param.second.getText(); + break; + } + + return (ret); +} + +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 (auto const& address : ip_addresses_) { + stream << " " << address.toText(); + } + + if (svc_params_length_ > 0) { + stream << ", svc_params='"; + bool first = true; + for (auto const& it : svc_params_map_) { + auto const& k = SVC_PARAMS.right.at(it.first); + if (first) { + first = false; + } else { + stream << " "; + } + + stream << k << "=" << svcParamValAsText(it); + } + + stream << "'"; + } + } + + 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 RFC9463 Section 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_buf_.assign(begin, end); + + // used to check correct order of SvcParams + int prev_svc_param_key = -1; + + // When the list of SvcParams is non-empty, it contains a series of + // SvcParamKey=SvcParamValue pairs, represented as: + // - a 2-octet field containing the SvcParamKey as an integer in network byte order. + // - a 2-octet field containing the length of the SvcParamValue as an integer + // between 0 and 65535 in network byte order. (uint16) + // - an octet string of this length whose contents are the SvcParamValue in a format + // determined by the SvcParamKey. + while (begin != end) { + // Minimum SvcParams len shall be 4: + // 2 octets SvcParamKey + 2 octets SvcParamValue Len + if (std::distance(begin, end) < 4) { + isc_throw(OutOfRange, getLogPrefix() << "DNR SvcParams data truncated to size " + << std::distance(begin, end)); + } + + uint16_t num_svc_param_key = readUint16(&*begin, 2); + begin += 2; + + // Check if SvcParamKey is known in + // https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml + auto it = SVC_PARAMS.right.find(num_svc_param_key); + if (it == SVC_PARAMS.right.end()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - key " << num_svc_param_key + << " not found in SvcParamKeys registry"); + } + + std::string svc_param_key = it->second; + + // As per RFC9463 Section 3.1.8: + // The service parameters do not include "ipv4hint" or "ipv6hint" parameters. + if (FORBIDDEN_SVC_PARAMS.find(svc_param_key) != FORBIDDEN_SVC_PARAMS.end()) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "Wrong Svc Params syntax - key " + << svc_param_key << " must not be used"); + } + + // Check if SvcParamKey usage is supported by DNR DHCP option. + // Note that SUPPORTED_SVC_PARAMS set may expand in the future. + if (SUPPORTED_SVC_PARAMS.find(num_svc_param_key) == SUPPORTED_SVC_PARAMS.end()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - key " << svc_param_key + << " not supported in DNR option SvcParams"); + } + + // As per RFC9460 Section 2.2: + // SvcParamKeys SHALL appear in increasing numeric order. (...) + // There are no duplicate SvcParamKeys. + // + // We check for duplicates here. + if (svc_params_map_.find(num_svc_param_key) != svc_params_map_.end()) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "Wrong Svc Params syntax - key " + << svc_param_key << " is duplicated."); + } + + // And we check correct order here. + if (num_svc_param_key <= prev_svc_param_key) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - SvcParamKeys" + << " SHALL appear in increasing numeric order."); + } + + prev_svc_param_key = num_svc_param_key; + + // Let's try to unpack SvcParamVal into a tuple. + OpaqueDataTuple svc_param_tuple(OpaqueDataTuple::LENGTH_2_BYTES); + try { + svc_param_tuple.unpack(begin, end); + } catch (const Exception& e) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() + << "Wrong Svc Params syntax - failed to unpack SvcParamVal for " + << "SvcParamKey " << svc_param_key << ". Error: " << e.what()); + } + + svc_params_map_.insert(std::make_pair(num_svc_param_key, svc_param_tuple)); + begin += svc_param_tuple.getTotalLength(); + } + } +} + +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: "); +} + +void +DnrInstance::parseDnrInstanceConfigData(const std::string& config_txt) { + // This parses convenient option config notation. + // The config to be parsed may contain escaped characters like "\\," or "\\|". + // Example configs are below (first contains recommended resolvers' IP addresses, and SvcParams; + // second is an example of ADN-only mode; + // third is like the first example, but for DNRv4 - single DNR instance): + // + // "name": "v6-dnr", + // "data": "100, dot1.example.org., 2001:db8::1 2001:db8::2, alpn=dot\\,doq\\,h2\\,h3 port=8530 dohpath=/q{?dns}" + // + // "name": "v6-dnr", + // "data": "200, resolver.example." + // + // "name": "v4-dnr", + // "data": "100, dot1.example.org., 10.0.3.4 10.1.5.6, alpn=dot\\,doq\\,h2\\,h3 port=8530 dohpath=/q{?dns}" + + // get tokens using comma separator with double backslash escaping enabled + std::vector<std::string> tokens = str::tokens(config_txt, std::string(","), true); + + if (tokens.size() < 2) { + isc_throw(BadValue, getLogPrefix() << "Option config requires at least comma separated " + << "Service Priority and ADN"); + } + + if (tokens.size() > 4) { + isc_throw(BadValue, getLogPrefix() << "Option config supports maximum 4 comma separated " + << "fields: Service Priority, ADN, resolver IP " + << "address(es) and SvcParams"); + } + + // parse Service Priority + std::string txt_svc_priority = str::trim(tokens[0]); + try { + service_priority_ = boost::lexical_cast<uint16_t>(txt_svc_priority); + } catch (const std::exception& e) { + isc_throw(BadValue, getLogPrefix() << "Cannot parse uint_16 integer Service priority " + << "from given value: " << txt_svc_priority + << ". Error: " << e.what()); + } + + // parse ADN + std::string txt_adn = str::trim(tokens[1]); + try { + adn_.reset(new isc::dns::Name(txt_adn, true)); + } catch (const std::exception& e) { + isc_throw(InvalidOptionDnrDomainName, getLogPrefix() << "Cannot parse ADN FQDN " + << "from given value: " << txt_adn + << ". Error: " << e.what()); + } + + adn_length_ = adn_->getLength(); + if (adn_length_ == 0) { + isc_throw(InvalidOptionDnrDomainName, getLogPrefix() + << "Mandatory Authentication Domain Name fully " + << "qualified domain-name is missing"); + } + + if (tokens.size() > 2) { + setAdnOnlyMode(false); + + // parse resolver IP address(es) + std::string txt_addresses = str::trim(tokens[2]); + + parseIpAddresses(txt_addresses); + } + + if (tokens.size() == 4) { + // parse Service Parameters + std::string txt_svc_params = str::trim(tokens[3]); + + parseSvcParams(txt_svc_params); + } +} + +void +DnrInstance::parseIpAddresses(const std::string& txt_addresses) { + // determine v4/v6 universe + std::string ip_version = (universe_ == Option::V6) ? "IPv6" : "IPv4"; + const size_t addr_len = (universe_ == Option::V6) ? V6ADDRESS_LEN : V4ADDRESS_LEN; + + // IP addresses are separated with space + std::vector<std::string> addresses = str::tokens(txt_addresses, std::string(" ")); + for (auto const& txt_addr : addresses) { + try { + const IOAddress address = IOAddress(str::trim(txt_addr)); + if ((address.isV4() && universe_ == Option::V6) || + (address.isV6() && universe_ == Option::V4)) { + isc_throw(BadValue, "Given address is not " << ip_version << " address."); + } + + addIpAddress(address); + } catch (const Exception& e) { + isc_throw(BadValue, getLogPrefix() + << "Cannot parse " << ip_version << " address " + << "from given value: " << txt_addr << ". Error: " << e.what()); + } + } + + // As per RFC9463 section 3.1.8: + // (If ADN-only mode is not used) + // The option includes at least one valid IP address. + if (ip_addresses_.empty()) { + isc_throw(BadValue, getLogPrefix() << "Option config requires at least one valid IP " + << "address."); + } + + addr_length_ = ip_addresses_.size() * addr_len; +} + +void +DnrInstance::parseSvcParams(const std::string& txt_svc_params) { + // SvcParamKey=SvcParamValue pairs are separated with space + std::vector<std::string> svc_params_pairs = str::tokens(txt_svc_params, std::string(" ")); + + for (auto const& svc_param_pair : svc_params_pairs) { + std::vector<std::string> key_val_tokens = str::tokens(str::trim(svc_param_pair), "="); + if (key_val_tokens.size() != 2) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - SvcParamKey=SvcParamValue " + << "pair syntax must be used"); + } + + // SvcParam Key related checks come below. + std::string svc_param_key = str::trim(key_val_tokens[0]); + + // As per RFC9463 Section 3.1.8: + // The service parameters do not include "ipv4hint" or "ipv6hint" parameters. + if (FORBIDDEN_SVC_PARAMS.find(svc_param_key) != FORBIDDEN_SVC_PARAMS.end()) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "Wrong Svc Params syntax - key " + << svc_param_key << " must not be used"); + } + + // Check if SvcParamKey is known in + // https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml + auto svc_params_iterator = SVC_PARAMS.left.find(svc_param_key); + if (svc_params_iterator == SVC_PARAMS.left.end()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - key " << svc_param_key + << " not found in SvcParamKeys registry"); + } + + // Check if SvcParamKey usage is supported by DNR DHCP option. + // Note that SUPPORTED_SVC_PARAMS set may expand in the future. + uint16_t num_svc_param_key = svc_params_iterator->second; + if (SUPPORTED_SVC_PARAMS.find(num_svc_param_key) == SUPPORTED_SVC_PARAMS.end()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - key " << svc_param_key + << " not supported in DNR option SvcParams"); + } + + // As per RFC9460 Section 2.2: + // SvcParamKeys SHALL appear in increasing numeric order. (...) + // There are no duplicate SvcParamKeys. + // + // We check for duplicates here. Correct ordering is done when option gets packed. + if (svc_params_map_.find(num_svc_param_key) != svc_params_map_.end()) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "Wrong Svc Params syntax - key " + << svc_param_key << " is duplicated."); + } + + // SvcParam Val check. + std::string svc_param_val = str::trim(key_val_tokens[1]); + if (svc_param_val.empty()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - empty SvcParamValue for key " + << svc_param_key); + } + + switch (num_svc_param_key) { + case 1: + parseAlpnSvcParam(svc_param_val); + break; + case 3: + parsePortSvcParam(svc_param_val); + break; + case 7: + parseDohpathSvcParam(svc_param_val); + break; + default: + // This should not happen because we check if num_svc_param_key is + // in SUPPORTED_SVC_PARAMS before. But in case new SvcParam appears in Supported, + // and is not handled here... + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "Wrong Svc Params syntax - key " + << num_svc_param_key << " not supported yet."); + } + } + + // If the "alpn" SvcParam indicates support for HTTP, "dohpath" MUST be present. + if (alpn_http_ && svc_params_map_.find(7) == svc_params_map_.end()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - dohpath SvcParam missing. " + << "When alpn SvcParam indicates " + << "support for HTTP, dohpath must be present."); + } + + // At this step all given SvcParams should be fine. We can pack everything to data + // buffer according to RFC9460 Section 2.2. + // + // When the list of SvcParams is non-empty, it contains a series of + // SvcParamKey=SvcParamValue pairs, represented as: + // - a 2-octet field containing the SvcParamKey as an integer in network byte order. + // - a 2-octet field containing the length of the SvcParamValue as an integer + // between 0 and 65535 in network byte order. (uint16) + // - an octet string of this length whose contents are the SvcParamValue in a format + // determined by the SvcParamKey. + // (...) + // SvcParamKeys SHALL appear in increasing numeric order. + // Note that (...) there are no duplicate SvcParamKeys. + OutputBuffer out_buf(2); + + for (auto const& svc_param_key : SUPPORTED_SVC_PARAMS) { + auto it = svc_params_map_.find(svc_param_key); + if (it != svc_params_map_.end()) { + // Write 2-octet field containing the SvcParamKey as an integer + // in network byte order. + out_buf.writeUint16(it->first); + // Write 2-octet field containing the length of the SvcParamValue + // and an octet string of this length whose contents are the SvcParamValue. + // We use OpaqueDataTuple#pack(&buf) here that will write correct len-data + // tuple to the buffer. + (it->second).pack(out_buf); + } + } + + // Copy SvcParams buffer from OutputBuffer to OptionBuffer. + const uint8_t* ptr = static_cast<const uint8_t*>(out_buf.getData()); + OptionBuffer temp_buf(ptr, ptr + out_buf.getLength()); + svc_params_buf_ = temp_buf; + svc_params_length_ = out_buf.getLength(); + out_buf.clear(); +} + +void +DnrInstance::parseAlpnSvcParam(const std::string& svc_param_val) { + // The wire-format value for "alpn" consists of at least one alpn-id prefixed by its + // length as a single octet, and these length-value pairs are concatenated to form + // the SvcParamValue. + OutputBuffer out_buf(2); + OpaqueDataTuple svc_param_val_tuple(OpaqueDataTuple::LENGTH_2_BYTES); + std::vector<std::string> alpn_ids_tokens = str::tokens(svc_param_val, std::string(",")); + for (auto const& alpn_id : alpn_ids_tokens) { + // Check if alpn-id is known in + // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids + if (ALPN_IDS.find(alpn_id) == ALPN_IDS.end()) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - alpn-id " << alpn_id + << " not found in ALPN-IDs registry"); + } + + // Make notice if this is any of http alpn-ids. + if (alpn_id[0] == 'h') { + alpn_http_ = true; + } + + OpaqueDataTuple alpn_id_tuple(OpaqueDataTuple::LENGTH_1_BYTE); + alpn_id_tuple.append(alpn_id); + alpn_id_tuple.pack(out_buf); + } + + svc_param_val_tuple.append(out_buf.getData(), out_buf.getLength()); + svc_params_map_.insert(std::make_pair(1, svc_param_val_tuple)); + out_buf.clear(); +} + +void +DnrInstance::parsePortSvcParam(const std::string& svc_param_val) { + // The wire format of the SvcParamValue is the corresponding 2-octet numeric value + // in network byte order. + OutputBuffer out_buf(2); + OpaqueDataTuple svc_param_val_tuple(OpaqueDataTuple::LENGTH_2_BYTES); + uint16_t port; + try { + port = boost::lexical_cast<uint16_t>(svc_param_val); + } catch (const std::exception& e) { + isc_throw(InvalidOptionDnrSvcParams, getLogPrefix() + << "Cannot parse uint_16 integer port nr " + << "from given value: " << svc_param_val + << ". Error: " << e.what()); + } + + out_buf.writeUint16(port); + svc_param_val_tuple.append(out_buf.getData(), out_buf.getLength()); + out_buf.clear(); + svc_params_map_.insert(std::make_pair(3, svc_param_val_tuple)); +} + +void +DnrInstance::parseDohpathSvcParam(const std::string& svc_param_val) { + // RFC9461 Section 5 + // single-valued SvcParamKey whose value (in both presentation format and wire + // format) MUST be a URI Template in relative form ([RFC6570], Section 1.1) encoded + // in UTF-8 [RFC3629]. If the "alpn" SvcParam indicates support for HTTP, + // "dohpath" MUST be present. The URI Template MUST contain a "dns" variable, + // and MUST be chosen such that the result after DoH URI Template expansion + // (Section 6 of [RFC8484]) is always a valid and functional ":path" value + // ([RFC9113], Section 8.3.1). + std::vector<uint8_t> utf8_encoded; + OpaqueDataTuple svc_param_val_tuple(OpaqueDataTuple::LENGTH_2_BYTES); + + // Check that "dns" variable is there + if (svc_param_val.find("{?dns}") == std::string::npos) { + isc_throw(InvalidOptionDnrSvcParams, + getLogPrefix() << "Wrong Svc Params syntax - dohpath SvcParamValue URI" + << " Template MUST contain a 'dns' variable."); + } + + // We hope to have URI containing < 0x80 ASCII chars, however to be sure + // and to be inline with RFC9461 Section 5, let's encode the dohpath with utf8. + utf8_encoded = encode::encodeUtf8(svc_param_val); + svc_param_val_tuple.append(utf8_encoded.begin(), utf8_encoded.size()); + svc_params_map_.insert(std::make_pair(7, svc_param_val_tuple)); +} + +} // namespace dhcp +} // namespace isc |