From 178347cdf048d2b64b2dbf5a3176fadb8c3dd088 Mon Sep 17 00:00:00 2001 From: Benjamin Fry Date: Thu, 11 Mar 2021 19:22:26 -0800 Subject: [PATCH] add zone file parsing for SVCB and HTTPS --- crates/client/src/error/parse_error.rs | 6 + .../client/src/serialize/txt/parse_rdata.rs | 6 +- .../src/serialize/txt/rdata_parsers/mod.rs | 1 + .../src/serialize/txt/rdata_parsers/svcb.rs | 414 ++++++++++++++++++ crates/proto/src/rr/rdata/svcb.rs | 175 +++++++- 5 files changed, 586 insertions(+), 16 deletions(-) create mode 100644 crates/client/src/serialize/txt/rdata_parsers/svcb.rs diff --git a/crates/client/src/error/parse_error.rs b/crates/client/src/error/parse_error.rs index ff3cee5b7b..8bdad6cf8e 100644 --- a/crates/client/src/error/parse_error.rs +++ b/crates/client/src/error/parse_error.rs @@ -196,6 +196,12 @@ impl From for Error { } } +impl From for Error { + fn from(_e: std::convert::Infallible) -> Error { + panic!("infallible") + } +} + impl From for io::Error { fn from(e: Error) -> Self { match *e.kind() { diff --git a/crates/client/src/serialize/txt/parse_rdata.rs b/crates/client/src/serialize/txt/parse_rdata.rs index 7c4d1d4ebb..b7eafa9ca8 100644 --- a/crates/client/src/serialize/txt/parse_rdata.rs +++ b/crates/client/src/serialize/txt/parse_rdata.rs @@ -46,8 +46,7 @@ impl RDataParser for RData { RecordType::CAA => caa::parse(tokens).map(RData::CAA)?, RecordType::CNAME => RData::CNAME(name::parse(tokens, origin)?), RecordType::HINFO => RData::HINFO(hinfo::parse(tokens)?), - // FIXME: actually implement this - RecordType::HTTPS => unimplemented!("HTTPS records not yet supported in zone files"), + RecordType::HTTPS => svcb::parse(tokens).map(RData::SVCB)?, RecordType::IXFR => panic!("parsing IXFR doesn't make sense"), // valid panic, never should happen RecordType::MX => RData::MX(mx::parse(tokens, origin)?), RecordType::NAPTR => RData::NAPTR(naptr::parse(tokens, origin)?), @@ -59,8 +58,7 @@ impl RDataParser for RData { RecordType::SOA => RData::SOA(soa::parse(tokens, origin)?), RecordType::SRV => RData::SRV(srv::parse(tokens, origin)?), RecordType::SSHFP => RData::SSHFP(sshfp::parse(tokens)?), - // FIXME: actually implement this - RecordType::SVCB => unimplemented!("SVCB records not yet supported in zone files"), + RecordType::SVCB => svcb::parse(tokens).map(RData::SVCB)?, RecordType::TLSA => RData::TLSA(tlsa::parse(tokens)?), RecordType::TXT => RData::TXT(txt::parse(tokens)?), RecordType::DNSSEC(DNSSECRecordType::SIG) => panic!("parsing SIG doesn't make sense"), // valid panic, never should happen diff --git a/crates/client/src/serialize/txt/rdata_parsers/mod.rs b/crates/client/src/serialize/txt/rdata_parsers/mod.rs index 034ef6d098..de7f8e0512 100644 --- a/crates/client/src/serialize/txt/rdata_parsers/mod.rs +++ b/crates/client/src/serialize/txt/rdata_parsers/mod.rs @@ -31,5 +31,6 @@ pub(crate) mod openpgpkey; pub(crate) mod soa; pub(crate) mod srv; pub(crate) mod sshfp; +pub(crate) mod svcb; pub(crate) mod tlsa; pub(crate) mod txt; diff --git a/crates/client/src/serialize/txt/rdata_parsers/svcb.rs b/crates/client/src/serialize/txt/rdata_parsers/svcb.rs new file mode 100644 index 0000000000..0f692405db --- /dev/null +++ b/crates/client/src/serialize/txt/rdata_parsers/svcb.rs @@ -0,0 +1,414 @@ +// Copyright 2015-2021 Benjamin Fry +// +// Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be +// copied, modified, or distributed except according to those terms. + +//! SVCB records in presentation format + +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::str::FromStr; + +use crate::error::*; +use crate::rr::rdata::svcb::*; +use crate::rr::Name; +use crate::serialize::txt::{Lexer, Token}; + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-2.2) +/// +/// ```text +/// 2.1. Zone file presentation format +/// +/// The presentation format of the record is: +/// +/// Name TTL IN SVCB SvcPriority TargetName SvcParams +/// +/// The SVCB record is defined specifically within the Internet ("IN") +/// Class ([RFC1035]). +/// +/// SvcPriority is a number in the range 0-65535, TargetName is a domain +/// name, and the SvcParams are a whitespace-separated list, with each +/// SvcParam consisting of a SvcParamKey=SvcParamValue pair or a +/// standalone SvcParamKey. SvcParamKeys are subject to IANA control +/// (Section 14.3). +/// +/// Each SvcParamKey SHALL appear at most once in the SvcParams. In +/// presentation format, SvcParamKeys are lower-case alphanumeric +/// strings. Key names should contain 1-63 characters from the ranges +/// "a"-"z", "0"-"9", and "-". In ABNF [RFC5234], +/// +/// alpha-lc = %x61-7A ; a-z +/// SvcParamKey = 1*63(alpha-lc / DIGIT / "-") +/// SvcParam = SvcParamKey ["=" SvcParamValue] +/// SvcParamValue = char-string +/// value = *OCTET +/// +/// The SvcParamValue is parsed using the character-string decoding +/// algorithm (Appendix A), producing a "value". The "value" is then +/// validated and converted into wire-format in a manner specific to each +/// key. +/// +/// When the "=" is omitted, the "value" is interpreted as empty. +/// +/// Unrecognized keys are represented in presentation format as +/// "keyNNNNN" where NNNNN is the numeric value of the key type without +/// leading zeros. A SvcParam in this form SHALL be parsed as specified +/// above, and the decoded "value" SHALL be used as its wire format +/// encoding. +/// +/// For some SvcParamKeys, the "value" corresponds to a list or set of +/// items. Presentation formats for such keys SHOULD use a comma- +/// separated list (Appendix A.1). +/// +/// SvcParams in presentation format MAY appear in any order, but keys +/// MUST NOT be repeated. +/// ``` +pub(crate) fn parse<'i, I: Iterator>(mut tokens: I) -> ParseResult { + // SvcPriority + let svc_priority: u16 = tokens + .next() + .ok_or_else(|| ParseError::from(ParseErrorKind::MissingToken("SvcPriority".to_string()))) + .and_then(|s| s.parse().map_err(Into::into))?; + + // svcb target + let target_name: Name = tokens + .next() + .ok_or_else(|| ParseError::from(ParseErrorKind::MissingToken("Target".to_string()))) + .and_then(|s| Name::from_str(s).map_err(ParseError::from))?; + + // Loop over all of the + let mut svc_params = Vec::new(); + for token in tokens { + // first need to split the key and (optional) value + let mut key_value = token.splitn(2, '='); + let key = key_value.next().ok_or_else(|| { + ParseError::from(ParseErrorKind::MissingToken( + "SVCB SvcbParams missing".to_string(), + )) + })?; + + // get the value, and remove any quotes + let value = key_value.next(); + svc_params.push(into_svc_param(key, value)?); + } + + Ok(SVCB::new(svc_priority, target_name, svc_params)) +} + +// first take the param and convert to +fn into_svc_param( + key: &str, + value: Option<&str>, +) -> Result<(SvcParamKey, SvcParamValue), ParseError> { + let key = SvcParamKey::from_str(key)?; + let value = parse_value(key, value)?; + + Ok((key, value)) +} + +fn parse_value(key: SvcParamKey, value: Option<&str>) -> Result { + match key { + SvcParamKey::Mandatory => parse_mandatory(value), + SvcParamKey::Alpn => parse_alpn(value), + SvcParamKey::NoDefaultAlpn => parse_no_default_alpn(value), + SvcParamKey::Port => parse_port(value), + SvcParamKey::Ipv4Hint => parse_ipv4_hint(value), + SvcParamKey::EchConfig => parse_ech_config(value), + SvcParamKey::Ipv6Hint => parse_ipv6_hint(value), + SvcParamKey::Key(_) => parse_unknown(value), + SvcParamKey::Key65535 | SvcParamKey::Unknown(_) => { + Err(ParseError::from(ParseErrorKind::Message( + "Bad Key type or unsupported, see generic key option, e.g. key1234", + ))) + } + } +} + +fn parse_char_data(value: &str) -> Result { + let mut lex = Lexer::new(value); + let ch_data = lex + .next_token()? + .ok_or_else(|| ParseError::from(ParseErrorKind::Message("expected character data")))?; + + match ch_data { + Token::CharData(data) => Ok(data), + _ => Err(ParseError::from(ParseErrorKind::Message( + "expected character data", + ))), + } +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-7) +/// ```text +/// The presentation "value" SHALL be a comma-separated list +/// (Appendix A.1) of one or more valid SvcParamKeys, either by their +/// registered name or in the unknown-key format (Section 2.1). Keys MAY +/// appear in any order, but MUST NOT appear more than once. For self- +/// consistency (Section 2.4.3), listed keys MUST also appear in the +/// SvcParams. +/// +/// To enable simpler parsing, this SvcParamValue MUST NOT contain escape +/// sequences. +/// +/// For example, the following is a valid list of SvcParams: +/// +/// echconfig=... key65333=ex1 key65444=ex2 mandatory=key65444,echconfig +/// ``` +/// +/// Currently this does not validate that the mandatory section matches the other keys +fn parse_mandatory(value: Option<&str>) -> Result { + let value = value.ok_or_else(|| { + ParseError::from(ParseErrorKind::Message( + "expected at least one Mandatory field", + )) + })?; + + let mandatories = parse_list::(value)?; + Ok(SvcParamValue::Mandatory(Mandatory(mandatories))) +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-6.1) +/// ```text +/// ALPNs are identified by their registered "Identification Sequence" +/// ("alpn-id"), which is a sequence of 1-255 octets. +/// +/// alpn-id = 1*255OCTET +/// +/// The presentation "value" SHALL be a comma-separated list +/// (Appendix A.1) of one or more "alpn-id"s. +/// ``` +/// +/// This does not currently check to see if the ALPN code is legitimate +fn parse_alpn(value: Option<&str>) -> Result { + let value = value.ok_or_else(|| { + ParseError::from(ParseErrorKind::Message("expected at least one ALPN code")) + })?; + + let alpns = parse_list::(value).expect("infallible"); + Ok(SvcParamValue::Alpn(Alpn(alpns))) +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-6.1) +/// ```text +/// For "no-default-alpn", the presentation and wire format values MUST +/// be empty. When "no-default-alpn" is specified in an RR, "alpn" must +/// also be specified in order for the RR to be "self-consistent" +/// (Section 2.4.3). +/// ``` +fn parse_no_default_alpn(value: Option<&str>) -> Result { + if value.is_some() { + return Err(ParseErrorKind::Message("no value expected for NoDefaultAlpn").into()); + } + + Ok(SvcParamValue::NoDefaultAlpn) +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-6.2) +/// ```text +/// The presentation "value" of the SvcParamValue is a single decimal +/// integer between 0 and 65535 in ASCII. Any other "value" (e.g. an +/// empty value) is a syntax error. To enable simpler parsing, this +/// SvcParam MUST NOT contain escape sequences. +/// ``` +fn parse_port(value: Option<&str>) -> Result { + let value = value.ok_or_else(|| { + ParseError::from(ParseErrorKind::Message("a port number for the port option")) + })?; + + let value = parse_char_data(value)?; + let port = u16::from_str(&value)?; + Ok(SvcParamValue::Port(port)) +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-6.4) +/// ```text +/// The presentation "value" SHALL be a comma-separated list +/// (Appendix A.1) of one or more IP addresses of the appropriate family +/// in standard textual format [RFC5952]. To enable simpler parsing, +/// this SvcParamValue MUST NOT contain escape sequences. +/// ``` +fn parse_ipv4_hint(value: Option<&str>) -> Result { + let value = value.ok_or_else(|| { + ParseError::from(ParseErrorKind::Message("expected at least one ipv4 hint")) + })?; + + let hints = parse_list::(value)?; + Ok(SvcParamValue::Ipv4Hint(IpHint(hints))) +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-9) +/// ```text +/// In presentation format, the value is a +/// single ECHConfigs encoded in Base64 [base64]. Base64 is used here to +/// simplify integration with TLS server software. To enable simpler +/// parsing, this SvcParam MUST NOT contain escape sequences. +/// ``` +fn parse_ech_config(value: Option<&str>) -> Result { + let value = value.ok_or_else(|| { + ParseError::from(ParseErrorKind::Message( + "expected a base64 encoded string for EchConfig", + )) + })?; + + let value = parse_char_data(value)?; + let ech_config_bytes = data_encoding::BASE64.decode(value.as_bytes())?; + Ok(SvcParamValue::EchConfig(EchConfig(ech_config_bytes))) +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-6.4) +/// ```text +/// The presentation "value" SHALL be a comma-separated list +/// (Appendix A.1) of one or more IP addresses of the appropriate family +/// in standard textual format [RFC5952]. To enable simpler parsing, +/// this SvcParamValue MUST NOT contain escape sequences. +/// ``` +fn parse_ipv6_hint(value: Option<&str>) -> Result { + let value = value.ok_or_else(|| { + ParseError::from(ParseErrorKind::Message("expected at least one ipv6 hint")) + })?; + + let hints = parse_list::(value)?; + Ok(SvcParamValue::Ipv6Hint(IpHint(hints))) +} + +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-2.1) +/// ```text +/// Unrecognized keys are represented in presentation format as +/// "keyNNNNN" where NNNNN is the numeric value of the key type without +/// leading zeros. A SvcParam in this form SHALL be parsed as specified +/// above, and the decoded "value" SHALL be used as its wire format +/// encoding. +/// +/// For some SvcParamKeys, the "value" corresponds to a list or set of +/// items. Presentation formats for such keys SHOULD use a comma- +/// separated list (Appendix A.1). +/// +/// SvcParams in presentation format MAY appear in any order, but keys +/// MUST NOT be repeated. +/// ``` +#[allow(clippy::unnecessary_wraps)] +fn parse_unknown(value: Option<&str>) -> Result { + let unknown: Vec> = if let Some(value) = value { + let unknown = parse_list::(value).expect("infallible"); + + unknown.into_iter().map(|s| s.as_bytes().to_vec()).collect() + } else { + Vec::new() + }; + + Ok(SvcParamValue::Unknown(Unknown(unknown))) +} + +fn parse_list(value: &str) -> Result, ParseError> +where + T: FromStr, + T::Err: Into, +{ + let mut result = Vec::new(); + + let values = value.trim_end_matches(',').split(','); + for value in values { + let value = parse_char_data(value)?; + let value = T::from_str(&value).map_err(|e| e.into())?; + result.push(value); + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use crate::rr::DNSClass; + use crate::serialize::txt::{Lexer, Parser}; + + use super::*; + + // this assumes that only a single record is parsed + // TODO: make Parser return an iterator over all records in a stream. + fn parse_record(txt: &str) -> SVCB { + let lex = Lexer::new(txt); + let mut parser = Parser::new(); + + let records = parser + .parse(lex, Some(Name::root()), Some(DNSClass::IN)) + .expect("failed to parse record") + .1; + let record_set = records.into_iter().next().expect("no record found").1; + record_set + .into_iter() + .next() + .unwrap() + .rdata() + .as_svcb() + .expect("Not an SVCB record") + .clone() + } + + #[test] + fn test_parsing() { + let svcb = parse_record("crypto.cloudflare.com. 299 IN SVCB 1 . alpn=h2, ipv4hint=162.159.135.79,162.159.136.79, echconfig=\"/gkAQwATY2xvdWRmbGFyZS1lc25pLmNvbQAgUbBtC3UeykwwE6C87TffqLJ/1CeaAvx3iESGyds85l8AIAAEAAEAAQAAAAA=\" ipv6hint=2606:4700:7::a29f:874f,2606:4700:7::a29f:884f,"); + + assert_eq!(svcb.svc_priority(), 1); + assert_eq!(*svcb.target_name(), Name::root()); + + let mut params = svcb.svc_params().iter(); + + // alpn + let param = params.next().expect("not alpn"); + assert_eq!(param.0, SvcParamKey::Alpn); + assert_eq!(param.1.as_alpn().expect("not alpn").0, &["h2"]); + + // ipv4 hint + let param = params.next().expect("ipv4hint"); + assert_eq!(SvcParamKey::Ipv4Hint, param.0); + assert_eq!( + param.1.as_ipv4_hint().expect("ipv4hint").0, + &[ + Ipv4Addr::from([162, 159, 135, 79]), + Ipv4Addr::from([162, 159, 136, 79]) + ] + ); + + // echconfig + let param = params.next().expect("echconfig"); + assert_eq!(SvcParamKey::EchConfig, param.0); + assert_eq!( + param.1.as_ech_config().expect("echconfig").0, + data_encoding::BASE64.decode("/gkAQwATY2xvdWRmbGFyZS1lc25pLmNvbQAgUbBtC3UeykwwE6C87TffqLJ/1CeaAvx3iESGyds85l8AIAAEAAEAAQAAAAA=".as_bytes()).unwrap() + ); + + // ipv6 hint + let param = params.next().expect("ipv6hint"); + assert_eq!(SvcParamKey::Ipv6Hint, param.0); + assert_eq!( + param.1.as_ipv6_hint().expect("ipv6hint").0, + &[ + Ipv6Addr::from([0x2606, 0x4700, 0x7, 0, 0, 0, 0xa29f, 0x874f]), + Ipv6Addr::from([0x2606, 0x4700, 0x7, 0, 0, 0, 0xa29f, 0x884f]) + ] + ); + } + + #[test] + fn test_parse_display() { + let svcb = parse_record("crypto.cloudflare.com. 299 IN SVCB 1 . alpn=h2, ipv4hint=162.159.135.79,162.159.136.79, echconfig=\"/gkAQwATY2xvdWRmbGFyZS1lc25pLmNvbQAgUbBtC3UeykwwE6C87TffqLJ/1CeaAvx3iESGyds85l8AIAAEAAEAAQAAAAA=\" ipv6hint=2606:4700:7::a29f:874f,2606:4700:7::a29f:884f,"); + + let svcb_display = svcb.to_string(); + + // add back the name, etc... + let svcb_display = format!("crypto.cloudflare.com. 299 IN SVCB {}", svcb_display); + let svcb_display = parse_record(&svcb_display); + + assert_eq!(svcb, svcb_display); + } + + /// sanity check for https + #[test] + fn test_parsing_https() { + let svcb = parse_record("crypto.cloudflare.com. 299 IN HTTPS 1 . alpn=h2, ipv4hint=162.159.135.79,162.159.136.79, echconfig=\"/gkAQwATY2xvdWRmbGFyZS1lc25pLmNvbQAgUbBtC3UeykwwE6C87TffqLJ/1CeaAvx3iESGyds85l8AIAAEAAEAAQAAAAA=\" ipv6hint=2606:4700:7::a29f:874f,2606:4700:7::a29f:884f,"); + + assert_eq!(svcb.svc_priority(), 1); + assert_eq!(*svcb.target_name(), Name::root()); + } +} diff --git a/crates/proto/src/rr/rdata/svcb.rs b/crates/proto/src/rr/rdata/svcb.rs index 06b7071fd2..26bb011de3 100644 --- a/crates/proto/src/rr/rdata/svcb.rs +++ b/crates/proto/src/rr/rdata/svcb.rs @@ -15,6 +15,8 @@ use std::{ net::Ipv6Addr, }; +use enum_as_inner::EnumAsInner; + use crate::error::*; use crate::rr::Name; use crate::serialize::binary::*; @@ -82,6 +84,68 @@ impl SVCB { svc_params, } } + + /// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-2.4.1) + /// ```text + /// 2.4.1. SvcPriority + /// + /// When SvcPriority is 0 the SVCB record is in AliasMode + /// (Section 2.4.2). Otherwise, it is in ServiceMode (Section 2.4.3). + /// + /// Within a SVCB RRSet, all RRs SHOULD have the same Mode. If an RRSet + /// contains a record in AliasMode, the recipient MUST ignore any + /// ServiceMode records in the set. + /// + /// RRSets are explicitly unordered collections, so the SvcPriority field + /// is used to impose an ordering on SVCB RRs. SVCB RRs with a smaller + /// SvcPriority value SHOULD be given preference over RRs with a larger + /// SvcPriority value. + /// + /// When receiving an RRSet containing multiple SVCB records with the + /// same SvcPriority value, clients SHOULD apply a random shuffle within + /// a priority level to the records before using them, to ensure uniform + /// load-balancing. + /// ``` + pub fn svc_priority(&self) -> u16 { + self.svc_priority + } + + /// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-2.5) + /// ```text + /// 2.5. Special handling of "." in TargetName + /// + /// If TargetName has the value "." (represented in the wire format as a + /// zero-length label), special rules apply. + /// + /// 2.5.1. AliasMode + /// + /// For AliasMode SVCB RRs, a TargetName of "." indicates that the + /// service is not available or does not exist. This indication is + /// advisory: clients encountering this indication MAY ignore it and + /// attempt to connect without the use of SVCB. + /// + /// 2.5.2. ServiceMode + /// + /// For ServiceMode SVCB RRs, if TargetName has the value ".", then the + /// owner name of this record MUST be used as the effective TargetName. + /// + /// For example, in the following example "svc2.example.net" is the + /// effective TargetName: + /// + /// example.com. 7200 IN HTTPS 0 svc.example.net. + /// svc.example.net. 7200 IN CNAME svc2.example.net. + /// svc2.example.net. 7200 IN HTTPS 1 . port=8002 echconfig="..." + /// svc2.example.net. 300 IN A 192.0.2.2 + /// svc2.example.net. 300 IN AAAA 2001:db8::2 + /// ``` + pub fn target_name(&self) -> &Name { + &self.target_name + } + + /// See [`SvcParamKey`] for details on each parameter + pub fn svc_params(&self) -> &[(SvcParamKey, SvcParamValue)] { + &self.svc_params + } } /// ```text @@ -220,6 +284,40 @@ impl fmt::Display for SvcParamKey { } } +impl std::str::FromStr for SvcParamKey { + type Err = ProtoError; + + fn from_str(s: &str) -> Result { + /// keys are in the format of key#, e.g. key12344, with a max value of u16 + fn parse_unknown_key(key: &str) -> Result { + let key_value = key.strip_prefix("key").ok_or_else(|| { + ProtoError::from(ProtoErrorKind::Msg(format!( + "bad formatted key ({}), expected key1234", + key + ))) + })?; + + let key_value = u16::from_str(key_value)?; + let key = SvcParamKey::from(key_value); + Ok(key) + } + + let key = match s { + "mandatory" => SvcParamKey::Mandatory, + "alpn" => SvcParamKey::Alpn, + "no-default-alpn" => SvcParamKey::NoDefaultAlpn, + "port" => SvcParamKey::Port, + "ipv4hint" => SvcParamKey::Ipv4Hint, + "echconfig" => SvcParamKey::EchConfig, + "ipv6hint" => SvcParamKey::Ipv6Hint, + "key65535" => SvcParamKey::Key65535, + _ => parse_unknown_key(s)?, + }; + + Ok(key) + } +} + impl Ord for SvcParamKey { fn cmp(&self, other: &Self) -> Ordering { u16::from(*self).cmp(&u16::from(*other)) @@ -241,7 +339,7 @@ impl PartialOrd for SvcParamKey { /// * an octet string of this length whose contents are in a format /// determined by the SvcParamKey. /// ``` -#[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, EnumAsInner)] pub enum SvcParamValue { /// In a ServiceMode RR, a SvcParamKey is considered "mandatory" if the /// RR will not function correctly for clients that ignore this @@ -323,7 +421,7 @@ pub enum SvcParamValue { /// /// This will be left as is when read off the wire, and encoded in bas64 /// for presentation. - Unknown(Vec), + Unknown(Unknown), } impl SvcParamValue { @@ -367,8 +465,7 @@ impl SvcParamValue { SvcParamKey::EchConfig => Self::EchConfig(EchConfig::read(&mut decoder)?), SvcParamKey::Ipv6Hint => Self::Ipv6Hint(IpHint::::read(&mut decoder)?), SvcParamKey::Key(_) | SvcParamKey::Key65535 | SvcParamKey::Unknown(_) => { - let data = decoder.read_vec(len)?.unverified(/*Consumer must verify the data*/); - Self::Unknown(data) + Self::Unknown(Unknown::read(&mut decoder)?) } }; @@ -392,7 +489,7 @@ impl BinEncodable for SvcParamValue { SvcParamValue::Ipv4Hint(ip_hint) => ip_hint.emit(encoder)?, SvcParamValue::EchConfig(ech_config) => ech_config.emit(encoder)?, SvcParamValue::Ipv6Hint(ip_hint) => ip_hint.emit(encoder)?, - SvcParamValue::Unknown(data) => encoder.emit_vec(data.as_slice())?, + SvcParamValue::Unknown(unknown) => unknown.emit(encoder)?, } // go back and set the length @@ -414,7 +511,7 @@ impl fmt::Display for SvcParamValue { SvcParamValue::Ipv4Hint(ip_hint) => write!(f, "{}", ip_hint)?, SvcParamValue::EchConfig(ech_config) => write!(f, "{}", ech_config)?, SvcParamValue::Ipv6Hint(ip_hint) => write!(f, "{}", ip_hint)?, - SvcParamValue::Unknown(data) => write!(f, "{}", data_encoding::BASE64.encode(data))?, + SvcParamValue::Unknown(unknown) => write!(f, "{}", unknown)?, } Ok(()) @@ -462,7 +559,7 @@ impl fmt::Display for SvcParamValue { /// ``` #[derive(Debug, PartialEq, Eq, Hash, Clone)] #[repr(transparent)] -pub struct Mandatory(Vec); +pub struct Mandatory(pub Vec); impl<'r> BinDecodable<'r> for Mandatory { /// This expects the decoder to be limited to only this field, i.e. the end of input for the decoder @@ -625,7 +722,7 @@ impl fmt::Display for Mandatory { /// ``` #[derive(Debug, PartialEq, Eq, Hash, Clone)] #[repr(transparent)] -pub struct Alpn(Vec); +pub struct Alpn(pub Vec); impl<'r> BinDecodable<'r> for Alpn { /// This expects the decoder to be limited to only this field, i.e. the end of input for the decoder @@ -710,7 +807,7 @@ impl fmt::Display for Alpn { /// ``` #[derive(Debug, PartialEq, Eq, Hash, Clone)] #[repr(transparent)] -pub struct EchConfig(Vec); +pub struct EchConfig(pub Vec); impl<'r> BinDecodable<'r> for EchConfig { /// In wire format, the value @@ -757,7 +854,7 @@ impl fmt::Display for EchConfig { /// *note* while the on the wire the EchConfig has a redundant length, /// the RFC is not explicit about including it in the base64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - write!(f, "{}", data_encoding::BASE64.encode(&self.0)) + write!(f, "\"{}\"", data_encoding::BASE64.encode(&self.0)) } } @@ -804,7 +901,7 @@ impl fmt::Display for EchConfig { /// ``` #[derive(Debug, PartialEq, Eq, Hash, Clone)] #[repr(transparent)] -pub struct IpHint(Vec); +pub struct IpHint(pub Vec); impl<'r, T> BinDecodable<'r> for IpHint where @@ -859,6 +956,60 @@ where } } +/// [draft-ietf-dnsop-svcb-https-03 SVCB and HTTPS RRs for DNS, February 2021](https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-svcb-https-03#section-2.1) +/// ```text +/// Unrecognized keys are represented in presentation format as +/// "keyNNNNN" where NNNNN is the numeric value of the key type without +/// leading zeros. A SvcParam in this form SHALL be parsed as specified +/// above, and the decoded "value" SHALL be used as its wire format +/// encoding. +/// +/// For some SvcParamKeys, the "value" corresponds to a list or set of +/// items. Presentation formats for such keys SHOULD use a comma- +/// separated list (Appendix A.1). +/// +/// SvcParams in presentation format MAY appear in any order, but keys +/// MUST NOT be repeated. +/// ``` +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[repr(transparent)] +pub struct Unknown(pub Vec>); + +impl<'r> BinDecodable<'r> for Unknown { + fn read(decoder: &mut BinDecoder<'r>) -> ProtoResult { + let mut unknowns = Vec::new(); + + while decoder.peek().is_some() { + let data = decoder.read_character_data()?; + let data = data.unverified(/*any data is valid here*/).to_vec(); + unknowns.push(data) + } + + Ok(Unknown(unknowns)) + } +} + +impl BinEncodable for Unknown { + fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> { + for unknown in self.0.iter() { + encoder.emit_character_data(unknown)?; + } + + Ok(()) + } +} + +impl fmt::Display for Unknown { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + for unknown in self.0.iter() { + // TODO: this needs to be properly encoded + write!(f, "\"{}\",", String::from_utf8_lossy(unknown))?; + } + + Ok(()) + } +} + /// Reads the SVCB record from the decoder. /// /// ```text @@ -953,7 +1104,7 @@ impl fmt::Display for SVCB { )?; for (key, param) in self.svc_params.iter() { - write!(f, " {key}=\"{param}\"", key = key, param = param)? + write!(f, " {key}={param}", key = key, param = param)? } Ok(())