summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPiotrek Zadroga <piotrek@isc.org>2024-02-15 20:19:17 +0100
committerPiotrek Zadroga <piotrek@isc.org>2024-02-23 17:14:05 +0100
commit83ccbf70755dd92c94f97401808e34048ca48d95 (patch)
treebf4baaee1a0e8c3d440db2a295908ff0285629ac
parent[#3141] pack SvcParams to buffer (diff)
downloadkea-83ccbf70755dd92c94f97401808e34048ca48d95.tar.xz
kea-83ccbf70755dd92c94f97401808e34048ca48d95.zip
[#3141] DNRv4 config parser
-rw-r--r--src/lib/dhcp/option4_dnr.cc405
-rw-r--r--src/lib/dhcp/option4_dnr.h131
-rw-r--r--src/lib/dhcp/option6_dnr.cc281
-rw-r--r--src/lib/dhcp/option6_dnr.h11
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.