diff options
author | Piotrek Zadroga <piotrek@isc.org> | 2024-02-15 20:19:17 +0100 |
---|---|---|
committer | Piotrek Zadroga <piotrek@isc.org> | 2024-02-23 17:14:05 +0100 |
commit | 83ccbf70755dd92c94f97401808e34048ca48d95 (patch) | |
tree | bf4baaee1a0e8c3d440db2a295908ff0285629ac | |
parent | [#3141] pack SvcParams to buffer (diff) | |
download | kea-83ccbf70755dd92c94f97401808e34048ca48d95.tar.xz kea-83ccbf70755dd92c94f97401808e34048ca48d95.zip |
[#3141] DNRv4 config parser
-rw-r--r-- | src/lib/dhcp/option4_dnr.cc | 405 | ||||
-rw-r--r-- | src/lib/dhcp/option4_dnr.h | 131 | ||||
-rw-r--r-- | src/lib/dhcp/option6_dnr.cc | 281 | ||||
-rw-r--r-- | src/lib/dhcp/option6_dnr.h | 11 |
4 files changed, 448 insertions, 380 deletions
diff --git a/src/lib/dhcp/option4_dnr.cc b/src/lib/dhcp/option4_dnr.cc index 17a6d4de1b..c07f708df0 100644 --- a/src/lib/dhcp/option4_dnr.cc +++ b/src/lib/dhcp/option4_dnr.cc @@ -127,18 +127,91 @@ Option4Dnr::addDnrInstance(DnrInstance& dnr_instance) { void Option4Dnr::parseConfigData(const std::string& config_txt) { - // TBD + // 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 std::map<std::string, uint16_t> DnrInstance::SVC_PARAMS = { + {"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), dnr_instance_data_length_size_(0), - adn_length_size_(0), addr_length_size_(0), minimal_length_(0) { + : 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(); } @@ -147,12 +220,11 @@ DnrInstance::DnrInstance(Option::Universe universe, 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) { + : 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), alpn_http_(false), + dnr_instance_data_length_size_(0), adn_length_size_(0), addr_length_size_(0), + minimal_length_(0) { initMembers(); setAdn(adn); checkFields(); @@ -161,10 +233,9 @@ DnrInstance::DnrInstance(Option::Universe universe, 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), + : 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), + alpn_http_(false), dnr_instance_data_length_size_(0), adn_length_size_(0), addr_length_size_(0), minimal_length_(0) { initMembers(); setAdn(adn); @@ -199,8 +270,8 @@ DnrInstance::packAddresses(OutputBuffer& buf) const { void DnrInstance::packSvcParams(OutputBuffer& buf) const { - if (svc_params_length_ > 0) { - buf.writeData(&(*svc_params_.begin()), svc_params_length_); + if (svc_params_length_ > 0 && !svc_params_buf_.empty()) { + buf.writeData(svc_params_buf_.data(), svc_params_length_); } } @@ -237,7 +308,7 @@ DnrInstance::setAdn(const std::string& adn) { adn_length_ = adn_len; if (universe_ == Option::V4) { - dnr_instance_data_length_ = dnrInstanceLen(); + setDnrInstanceDataLength(); } } @@ -267,9 +338,8 @@ DnrInstance::unpackAdn(OptionBufferConstIter& begin, OptionBufferConstIter end) 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()); + << "Failed to parse fully qualified domain-name " + << "from wire format - " << ex.what()); } begin += adn_length_ + getAdnLengthSize(); @@ -328,8 +398,8 @@ DnrInstance::checkSvcParams(bool from_wire_data) { 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); + getLogPrefix() << "Wrong Svc Params syntax - key had more " + << "than 63 characters - " << key); } if (FORBIDDEN_SVC_PARAMS.find(key) != FORBIDDEN_SVC_PARAMS.end()) { @@ -384,7 +454,7 @@ DnrInstance::checkFields() { addr_length_ = addr_len; if (universe_ == Option::V4) { - dnr_instance_data_length_ = dnrInstanceLen(); + setDnrInstanceDataLength(); } } @@ -482,8 +552,11 @@ 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(); + // This is used only when upacking hex bin option data. + // We only assign the data to svc_params_buf_ buffer. + // We do exact SvcParam syntax check when unpacking convenient option config notation + // in parseDnrInstanceConfigData(). + svc_params_buf_.assign(begin, end); begin += svc_params_length_; } } @@ -500,5 +573,285 @@ DnrInstance::initMembers() { ("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]); + + // 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 { + addIpAddress(IOAddress(str::trim(txt_addr))); + } 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; + } + + if (tokens.size() == 4) { + // parse Service Parameters + std::string txt_svc_params = str::trim(tokens[3]); + + // SvcParamKey=SvcParamValue pairs are separated with space + std::vector<std::string> svc_params_pairs = str::tokens(txt_svc_params, std::string(" ")); + std::vector<std::string> alpn_ids_tokens; + OpaqueDataTuple svc_param_val_tuple(OpaqueDataTuple::LENGTH_2_BYTES); + OutputBuffer out_buf(2); + 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.find(svc_param_key); + if (svc_params_iterator == SVC_PARAMS.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 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); + } + + svc_param_val_tuple.clear(); + switch (num_svc_param_key) { + case 1: + // alpn + // 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. + 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.starts_with('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(static_cast<const char*>(out_buf.getData()), + out_buf.getLength()); + out_buf.clear(); + } + + svc_params_map_.insert(std::make_pair(num_svc_param_key, svc_param_val_tuple)); + break; + case 3: + // port + // The wire format of the SvcParamValue is the corresponding 2-octet numeric value + // in network byte order. + 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(static_cast<const char*>(out_buf.getData()), + out_buf.getLength()); + out_buf.clear(); + svc_params_map_.insert(std::make_pair(num_svc_param_key, svc_param_val_tuple)); + break; + case 7: + // dohpath - 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). + + // 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. + auto const 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(num_svc_param_key, svc_param_val_tuple)); + break; + } + } + + // 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. + + 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(); + } +} + } // namespace dhcp } // namespace isc diff --git a/src/lib/dhcp/option4_dnr.h b/src/lib/dhcp/option4_dnr.h index 8b5b572326..8afa80c0e5 100644 --- a/src/lib/dhcp/option4_dnr.h +++ b/src/lib/dhcp/option4_dnr.h @@ -24,64 +24,6 @@ namespace isc { namespace dhcp { - -/// @brief Service parameters, used in DNR options in DHCPv4 and DHCPv6, but also in RA and DNS -/// -/// The IANA registry is maintained at https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml -const std::map<std::string, uint16_t> SVC_PARAMS = -{ - { "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 -}; - -/// @brief Possible ALPN protocol IDs. -/// -/// The IANA registry is maintained at -/// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids -static const std::unordered_set<std::string> 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 -}; - /// @brief Exception thrown when invalid domain name is specified. class InvalidOptionDnrDomainName : public Exception { public: @@ -125,8 +67,32 @@ public: /// included IP addresses. static const std::unordered_set<std::string> FORBIDDEN_SVC_PARAMS; + /// @brief Service parameters, used in DNR options in DHCPv4 and DHCPv6, but also in RA and DNS + /// + /// The IANA registry is maintained at https://www.iana.org/assignments/dns-svcb/dns-svcb.xhtml + static const std::map<std::string, uint16_t> SVC_PARAMS; + + /// @brief Ordered set of supported SvcParamKeys. + /// + /// As per RFC9463 Section 3.1.5: + /// The following service parameters MUST be supported by a DNR implementation: + /// SvcParamKey=1 alpn: Used to indicate the set of supported protocols (Section 7.1 of + /// [RFC9460]). + /// SvcParamKey=3 port: Used to indicate the target port number for the encrypted DNS connection + /// (Section 7.2 of [RFC9460]). + /// + /// In addition, the following service parameter is RECOMMENDED to be supported by a DNR + /// implementation: + /// SvcParamKey=7 dohpath: Used to supply a relative DoH URI Template + /// (Section 5.1 of [RFC9461]). static const std::set<uint8_t> SUPPORTED_SVC_PARAMS; + /// @brief Possible ALPN protocol IDs. + /// + /// The IANA registry is maintained at + /// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids + static const std::unordered_set<std::string> ALPN_IDS; + /// @brief Constructor of the empty DNR Instance. /// /// @param universe either V4 or V6 Option universe @@ -290,6 +256,15 @@ public: adn_only_mode_ = adn_only_mode; } + /// @brief Setter of the @c dnr_instance_data_length_ field. + /// + /// Size is calculated basing on set Service Priority, ADN, IP address/es and SvcParams. + /// This should be called after all fields are set. + /// This is only used for DHCPv4 Encrypted DNS %Option. + void setDnrInstanceDataLength() { + dnr_instance_data_length_ = dnrInstanceLen(); + } + /// @brief Writes the ADN FQDN in the wire format into a buffer. /// /// The Authentication Domain Name - fully qualified domain name of the encrypted @@ -356,9 +331,7 @@ public: /// Addr Len not divisible by 4, Addr Len is 0. virtual void unpackAddresses(OptionBufferConstIter& begin, OptionBufferConstIter end); - /// @brief Unpacks Service Parameters from wire data buffer and stores it in @c svc_params_. - /// - /// It may throw in case of malformed data detected during parsing. + /// @brief Unpacks Service Parameters from wire data buffer and stores it in @c svc_params_buf_. /// /// @param begin beginning of the buffer from which the field will be read /// @param end end of the buffer from which the field will be read @@ -397,6 +370,34 @@ public: /// @param ip_address IP address to be added void addIpAddress(const asiolink::IOAddress& ip_address); + /// @brief Parses a convenient notation of the option data, which may be used in config. + /// + /// As an alternative to the binary format, + /// we provide convenience option definition as a string in format: + /// (for DNRv6) + /// "100, dot1.example.org., 2001:db8::1 2001:db8::2, alpn=dot\\,doq\\,h2\\,h3 port=8530 dohpath=/q{?dns}" + /// "200, resolver.example." - ADN only mode + /// (for DNRv4) + /// "100, dot1.example.org., 10.0.3.4 10.1.5.6, alpn=dot\\,doq\\,h2\\,h3 port=8530 dohpath=/q{?dns}" + /// "200, resolver.example." - ADN only mode + /// + /// Note that comma and pipe chars ("," 0x2C and "|" 0x7C) are used as separators in this + /// syntax. That's why whenever they are used in config in fields' values, they must be escaped + /// with double backslash as in example. + /// + /// Note that this function parses single DnrInstance. For DNRv4 it is possible to have more + /// than one DnrInstance per one Option. In that case this function must be called for each + /// DnrInstance. + /// + /// @param config_txt convenient notation of the option data received as string + /// + /// @throw BadValue Thrown in case parser found wrong format of received string. + /// @throw InvalidOptionDnrDomainName Thrown in case parser had problems with extracting ADN + /// FQDN. + /// @throw InvalidOptionDnrSvcParams Thrown in case parser had problems with extracting + /// SvcParams. + void parseDnrInstanceConfigData(const std::string& config_txt); + protected: /// @brief Either V4 or V6 Option universe. Option::Universe universe_; @@ -616,11 +617,13 @@ private: /// /// As an alternative to the binary format, /// we provide convenience option definition as a string in format: - /// TBD + /// "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" /// - /// @param config_txt convenient notation of the option data received as string + /// It may throw BadValue, InvalidOptionDnrDomainName or InvalidOptionDnrSvcParams, + /// if DnrInstance#parseDnrInstanceConfigData() throws. /// - /// @throw BadValue Thrown in case parser found wrong format of received string. + /// @param config_txt convenient notation of the option data received as string. void parseConfigData(const std::string& config_txt); }; diff --git a/src/lib/dhcp/option6_dnr.cc b/src/lib/dhcp/option6_dnr.cc index 1b5fa314a6..f5fa160696 100644 --- a/src/lib/dhcp/option6_dnr.cc +++ b/src/lib/dhcp/option6_dnr.cc @@ -59,7 +59,7 @@ Option6Dnr::unpack(OptionBufferConstIter begin, OptionBufferConstIter end) { if (convenient_notation_) { // parse convenient notation std::string config_txt = std::string(begin, end); - parseConfigData(config_txt); + parseDnrInstanceConfigData(config_txt); } else { if (std::distance(begin, end) < getMinimalLength()) { isc_throw(OutOfRange, getLogPrefix() @@ -140,7 +140,7 @@ Option6Dnr::unpackAddresses(OptionBufferConstIter& begin, OptionBufferConstIter auto addr_end = begin + addr_length_; while (begin != addr_end) { try { - ip_addresses_.push_back(IOAddress::fromBytes(AF_INET6, &(*begin))); + addIpAddress(IOAddress::fromBytes(AF_INET6, &(*begin))); } catch (const Exception& ex) { isc_throw(BadValue, getLogPrefix() << "failed to parse IPv6 address" << " - " << ex.what()); @@ -150,282 +150,5 @@ Option6Dnr::unpackAddresses(OptionBufferConstIter& begin, OptionBufferConstIter } } -void -Option6Dnr::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 recommended resolvers' IP addresses, and SvcParams; - // second is an example of ADN-only mode): - // - // "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." - - // 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]); - - // IP addresses are separated with space - std::vector<std::string> addresses = str::tokens(txt_addresses, std::string(" ")); - for (auto const& txt_addr : addresses) { - try { - ip_addresses_.push_back(IOAddress(str::trim(txt_addr))); - } catch (const Exception& e) { - isc_throw(BadValue, getLogPrefix() << "Cannot parse IPv6 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() * V6ADDRESS_LEN; - } - - if (tokens.size() == 4) { - // parse Service Parameters - std::string txt_svc_params = str::trim(tokens[3]); - - // SvcParamKey=SvcParamValue pairs are separated with space - std::vector<std::string> svc_params_pairs = str::tokens(txt_svc_params, std::string(" ")); - std::vector<std::string> alpn_ids_tokens; - OpaqueDataTuple svc_param_val_tuple(OpaqueDataTuple::LENGTH_2_BYTES); - OutputBuffer out_buf(2); - 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.find(svc_param_key); - if (svc_params_iterator == SVC_PARAMS.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 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); - } - - svc_param_val_tuple.clear(); - switch (num_svc_param_key) { - case 1: - // alpn - // 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. - 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.starts_with('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(static_cast<const char*>(out_buf.getData()), - out_buf.getLength()); - out_buf.clear(); - } - - svc_params_map_.insert(std::make_pair(num_svc_param_key, svc_param_val_tuple)); - break; - case 3: - // port - // The wire format of the SvcParamValue is the corresponding 2-octet numeric value - // in network byte order. - 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(static_cast<const char*>(out_buf.getData()), - out_buf.getLength()); - out_buf.clear(); - svc_params_map_.insert(std::make_pair(num_svc_param_key, svc_param_val_tuple)); - break; - case 7: - // dohpath - 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). - - // 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. - auto const 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(num_svc_param_key, svc_param_val_tuple)); - break; - } - } - - // 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. - - std::ostringstream stream; - 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(); - - isc_throw(BadValue, getLogPrefix() - << "SvcParams: " + txt_svc_params << ", packed hex: " - << str::dumpAsHex(svc_params_buf_.data(), svc_params_length_)); - } -} - } // namespace dhcp } // namespace isc diff --git a/src/lib/dhcp/option6_dnr.h b/src/lib/dhcp/option6_dnr.h index 4a453f0524..f7fbeaa4d4 100644 --- a/src/lib/dhcp/option6_dnr.h +++ b/src/lib/dhcp/option6_dnr.h @@ -146,17 +146,6 @@ private: /// @brief Flag stating whether the %Option was constructed with a convenient notation string, /// that needs custom parsing, or binary data. bool convenient_notation_; - - /// @brief Parses a convenient notation of the option data, which may be used in config. - /// - /// As an alternative to the binary format, - /// we provide convenience option definition as a string in format: - /// TBD - /// - /// @param config_txt convenient notation of the option data received as string - /// - /// @throw BadValue Thrown in case parser found wrong format of received string. - void parseConfigData(const std::string& config_txt); }; /// A pointer to the @c Option6Dnr object. |