From bc14e8a68cd1d8f86fe9da36d941aa54a969ee20 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Sat, 25 Sep 2021 09:56:16 +1000 Subject: [PATCH 01/26] char-string parser --- Cargo.lock | 32 +++++++ dns/Cargo.toml | 1 + dns/src/lib.rs | 3 + dns/src/strings.rs | 1 - dns/src/value_list.rs | 188 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 1 deletion(-) create mode 100644 dns/src/value_list.rs diff --git a/Cargo.lock b/Cargo.lock index 93d4c8c..19adfc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "addr2line" version = "0.14.1" @@ -136,6 +138,7 @@ dependencies = [ "byteorder", "log", "mutagen", + "nom", "pretty_assertions", "unic-idna", ] @@ -296,6 +299,18 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "minimal-lexical" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d" + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -357,6 +372,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nom" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + [[package]] name = "object" version = "0.23.0" @@ -762,6 +788,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb" +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" + [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" diff --git a/dns/Cargo.toml b/dns/Cargo.toml index 0dc7ba5..4faf059 100644 --- a/dns/Cargo.toml +++ b/dns/Cargo.toml @@ -15,6 +15,7 @@ log = "0.4" # protocol parsing helper byteorder = "1.3" +nom = "7.0.0" # printing of certain packets base64 = "0.13" diff --git a/dns/src/lib.rs b/dns/src/lib.rs index fe2d443..cb49a85 100644 --- a/dns/src/lib.rs +++ b/dns/src/lib.rs @@ -38,6 +38,9 @@ pub use self::types::*; mod strings; pub use self::strings::Labels; +mod value_list; +pub use self::value_list::ValueList; + mod wire; pub use self::wire::{Wire, WireError, MandatedLength}; diff --git a/dns/src/strings.rs b/dns/src/strings.rs index fee4a43..5c33494 100644 --- a/dns/src/strings.rs +++ b/dns/src/strings.rs @@ -9,7 +9,6 @@ use log::*; use crate::wire::*; - /// Domain names in the DNS protocol are encoded as **Labels**, which are /// segments of ASCII characters prefixed by their length. When written out, /// each segment is followed by a dot. diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs new file mode 100644 index 0000000..b2ba2be --- /dev/null +++ b/dns/src/value_list.rs @@ -0,0 +1,188 @@ +#![allow(dead_code)] + +use std::io::Cursor; + +/// Parameters to a SVCB/HTTPS record can be multi-valued. +/// This is a fancy comma-separated list, where escaped commas \, and \044 do not separate +/// values. +/// +/// # References: +/// +/// [Draft RFC](https://tools.ietf.org/id/draft-ietf-dnsop-svcb-https-02.html#name-the-svcb-record-type), section A.1 +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] +pub struct ValueList { + values: Vec>, +} + +impl ValueList { + pub fn new() -> Self { + Self { values: Vec::new() } + } + + /// for tests + pub fn encode(input: &str) -> Self { + todo!() + } +} + +fn read_string(_list: &mut ValueList, _c: &mut Cursor<&[u8]>) { + todo!() +} + +use nom::Parser; +use nom::branch::alt; +use nom::combinator::recognize; +use nom::error::ParseError; +use nom::sequence::preceded; +use nom::IResult; +use nom::bytes::complete::{tag, take_while1}; + +/// A parser as defined by Appendix A of the draft, which describes RFC 1035 § 5.1 +/// +/// Note: Appendix A says it's not limited to 255 characters +pub fn parse_char_string(buf: &[u8]) -> IResult<&[u8], Vec> { + if buf.starts_with(b"\"") { + quoted(buf) + } else { + contiguous(buf) + } +} + +#[cfg(test)] +fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { + x + .map(|(remain, output)| { + (std::str::from_utf8(remain).unwrap(), String::from_utf8(output).unwrap()) + }) + .map_err(|x| x.map(|y| <_ as ParseError<&str>>::from_error_kind(std::str::from_utf8(y.input).unwrap(), y.code))) +} + +#[test] +fn test_escaping() { + // let parse = |slice| strings(non_special(slice).map(|(a, b)| (a, Vec::from(b)))); + let mini = |slice| char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))); + assert_eq!(mini(b"\\044"), Ok(("", ','))); + + let parse = |slice| strings(contiguous(slice)); + assert!(parse(b"").is_err()); + assert_eq!(parse(b"hello"), Ok(("", "hello".to_owned()))); + assert_eq!(parse(b"hello\\044"), Ok(("", "hello,".to_owned()))); + assert_eq!(parse(b"hello\\\\"), Ok(("", "hello\\".to_owned()))); + assert_eq!(parse(b"hello\\\\\\\\"), Ok(("", "hello\\\\".to_owned()))); + assert_eq!(parse(b"hello\\*"), Ok(("", "hello*".to_owned()))); + assert_eq!(parse(b"\\,hello\\*"), Ok(("", ",hello*".to_owned()))); + assert_eq!(parse(b"\\,hello\\*("), Ok(("(", ",hello*".to_owned()))); + assert_eq!(parse(b"*;"), Ok((";", "*".to_owned()))); + assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); + assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); +} + +fn non_special(input: &[u8]) -> IResult<&[u8], &[u8]> { + fn is_non_special(c: u8) -> bool { + match c { + // + // + // + // + // + // + // + // TODO: remove comma from this to make a value-list + // + // + // + // + // + // + // + // non-special. VCHAR minus DQUOTE, ";", "(", ")" and "\" + 0x21 | 0x23..=0x27 | 0x2A..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E => true, + _ => false, + } + } + recognize(take_while1(is_non_special))(input) +} + +fn char_escape(input: &[u8]) -> IResult<&[u8], u8> { + let (input, _backslash) = nom::bytes::complete::tag(b"\\")(input)?; + match input { + // dec-octet, a number 0..=255 as a three digit decimal number + &[c @ b'0'..=b'2', ref rest @ ..] => { + let (remain, byte) = dec_octet(rest, c - b'0')?; + Ok((remain, byte)) + } + // non-digit is VCHAR minus DIGIT + &[c @ (0x21..=0x2F | 0x3A..=0x7E), ref remain @ ..] => { + Ok((remain, c)) + } + _ => Err(nom::Err::Error(nom::error::Error::from_error_kind(input, nom::error::ErrorKind::Escaped))), + } +} + +fn dec_octet(buf: &[u8], zero_one_or_two: u8) -> IResult<&[u8], u8> { + let (hundreds, tens, ones, rest) = match zero_one_or_two { + hundreds @ (0 | 1) => match buf { + [tens @ b'0'..=b'9', ones @ b'0'..=b'9', rest @ ..] => (hundreds, tens - b'0', ones - b'0', rest), + _ => return Err(nom::Err::Error(nom::error::Error::from_error_kind(buf, nom::error::ErrorKind::Escaped))), + }, + hundreds @ 2 => match buf { + [tens @ b'0'..=b'4', ones @ b'0'..=b'9', rest @ ..] | [tens @ b'5', ones @ b'0'..=b'5', rest @ ..] => { + (hundreds, tens - b'0', ones - b'0', rest) + } + _ => return Err(nom::Err::Error(nom::error::Error::from_error_kind(buf, nom::error::ErrorKind::Escaped))), + }, + _ => return Err(nom::Err::Error(nom::error::Error::from_error_kind(buf, nom::error::ErrorKind::Escaped))), + }; + let byte = hundreds * 100 + tens * 10 + ones; + Ok((rest, byte)) +} + +#[derive(Debug)] +enum EscChunk<'a> { + Slice(&'a [u8]), + Byte(u8) +} + +fn chunk_1<'a, F, E>(mut inner: F) -> impl Parser<&'a [u8], Vec, E> +where + F: Parser<&'a [u8], EscChunk<'a>, E>, + E: ParseError<&'a [u8]> + core::fmt::Debug, +{ + move |input| { + let mut output = Vec::new(); + let parser = |slice| inner.parse(slice); + let mut iter = nom::combinator::iterator(input, parser); + let i = &mut iter; + let mut success = false; + for chunk in i { + success = true; + match chunk { + EscChunk::Slice(slice) => output.extend_from_slice(slice), + EscChunk::Byte(byte) => output.push(byte), + } + } + let (remain, ()) = iter.finish()?; + if success { + Ok((remain, output)) + } else { + Err(nom::Err::Error(E::from_error_kind(input, nom::error::ErrorKind::Eof))) + } + } +} + +fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { + alt((non_special.map(EscChunk::Slice), char_escape.map(EscChunk::Byte)))(input) +} + +fn contiguous(input: &[u8]) -> IResult<&[u8], Vec> { + chunk_1(contiguous_chunk).parse(input) +} + +fn quoted(input: &[u8]) -> IResult<&[u8], Vec> { + let wsc = take_while1(|b| b == b' ' || b == b'\t'); + let wsc2 = take_while1(|b| b == b' ' || b == b'\t'); + let wsc_chunk = alt((wsc, preceded(tag(b"\\"), wsc2))).map(EscChunk::Slice); + let parser = chunk_1(alt((contiguous_chunk, wsc_chunk))); + let mut parser = nom::sequence::delimited(tag(b"\""), parser, tag(b"\"")); + parser.parse(input) +} From 000b3e4c746fe067491d84c9060af92244543e3f Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Sun, 26 Sep 2021 22:00:33 +1000 Subject: [PATCH 02/26] Implement SVCB and HTTPS record types (some todos) --- Cargo.lock | 64 +++ dns/Cargo.toml | 3 +- dns/src/record/mod.rs | 24 +- dns/src/record/svcb_https.rs | 1029 ++++++++++++++++++++++++++++++++++ dns/src/value_list.rs | 357 +++++++++--- dns/src/wire.rs | 2 + src/colours.rs | 2 + src/output.rs | 12 + src/table.rs | 2 + 9 files changed, 1397 insertions(+), 98 deletions(-) create mode 100644 dns/src/record/svcb_https.rs diff --git a/Cargo.lock b/Cargo.lock index 19adfc9..41f8dc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -136,6 +145,7 @@ version = "0.2.0-pre" dependencies = [ "base64", "byteorder", + "env_logger", "log", "mutagen", "nom", @@ -170,6 +180,19 @@ dependencies = [ "rand", ] +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "failure" version = "0.1.8" @@ -248,6 +271,12 @@ version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "ipconfig" version = "0.2.2" @@ -534,6 +563,23 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -667,6 +713,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "unic-char-property" version = "0.9.0" @@ -822,6 +877,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/dns/Cargo.toml b/dns/Cargo.toml index 4faf059..9ab1467 100644 --- a/dns/Cargo.toml +++ b/dns/Cargo.toml @@ -18,7 +18,7 @@ byteorder = "1.3" nom = "7.0.0" # printing of certain packets -base64 = "0.13" +base64 = "0.13.0" # idna encoding unic-idna = { version = "0.9.0", optional = true } @@ -28,6 +28,7 @@ mutagen = { git = "https://github.com/llogiq/mutagen", optional = true } [dev-dependencies] pretty_assertions = "0.7" +env_logger = "0.9.0" [features] default = [] # idna is enabled in the main dog crate diff --git a/dns/src/record/mod.rs b/dns/src/record/mod.rs index 908fb4d..0a6f34c 100644 --- a/dns/src/record/mod.rs +++ b/dns/src/record/mod.rs @@ -2,7 +2,6 @@ use crate::wire::*; - mod a; pub use self::a::A; @@ -54,6 +53,9 @@ pub use self::soa::SOA; mod srv; pub use self::srv::SRV; +mod svcb_https; +pub use self::svcb_https::{HTTPS, SVCB}; + mod tlsa; pub use self::tlsa::TLSA; @@ -63,11 +65,9 @@ pub use self::txt::TXT; mod uri; pub use self::uri::URI; - mod others; pub use self::others::UnknownQtype; - /// A record that’s been parsed from a byte buffer. #[derive(PartialEq, Debug)] #[allow(missing_docs)] @@ -79,6 +79,7 @@ pub enum Record { EUI48(EUI48), EUI64(EUI64), HINFO(HINFO), + HTTPS(HTTPS), LOC(LOC), MX(MX), NAPTR(NAPTR), @@ -89,13 +90,13 @@ pub enum Record { SSHFP(SSHFP), SOA(SOA), SRV(SRV), + SVCB(SVCB), TLSA(TLSA), TXT(TXT), URI(URI), /// A record with a type that we don’t recognise. Other { - /// The number that’s meant to represent the record type. type_number: UnknownQtype, @@ -104,7 +105,6 @@ pub enum Record { }, } - /// The type of a record that may or may not be one of the known ones. Has no /// data associated with it other than what type of record it is. #[derive(PartialEq, Debug, Copy, Clone)] @@ -117,6 +117,7 @@ pub enum RecordType { EUI48, EUI64, HINFO, + HTTPS, LOC, MX, NAPTR, @@ -126,6 +127,7 @@ pub enum RecordType { SSHFP, SOA, SRV, + SVCB, TLSA, TXT, URI, @@ -141,7 +143,7 @@ impl From for RecordType { if $record::RR_TYPE == type_number { return RecordType::$record; } - } + }; } try_record!(A); @@ -151,6 +153,7 @@ impl From for RecordType { try_record!(EUI48); try_record!(EUI64); try_record!(HINFO); + try_record!(HTTPS); try_record!(LOC); try_record!(MX); try_record!(NAPTR); @@ -161,6 +164,7 @@ impl From for RecordType { try_record!(SSHFP); try_record!(SOA); try_record!(SRV); + try_record!(SVCB); try_record!(TLSA); try_record!(TXT); try_record!(URI); @@ -169,9 +173,7 @@ impl From for RecordType { } } - impl RecordType { - /// Determines the record type with a given name, or `None` if none is /// known. Matches names case-insensitively. pub fn from_type_name(type_name: &str) -> Option { @@ -180,7 +182,7 @@ impl RecordType { if $record::NAME.eq_ignore_ascii_case(type_name) { return Some(Self::$record); } - } + }; } try_record!(A); @@ -190,6 +192,7 @@ impl RecordType { try_record!(EUI48); try_record!(EUI64); try_record!(HINFO); + try_record!(HTTPS); try_record!(LOC); try_record!(MX); try_record!(NAPTR); @@ -200,6 +203,7 @@ impl RecordType { try_record!(SSHFP); try_record!(SOA); try_record!(SRV); + try_record!(SVCB); try_record!(TLSA); try_record!(TXT); try_record!(URI); @@ -217,6 +221,7 @@ impl RecordType { Self::EUI48 => EUI48::RR_TYPE, Self::EUI64 => EUI64::RR_TYPE, Self::HINFO => HINFO::RR_TYPE, + Self::HTTPS => HTTPS::RR_TYPE, Self::LOC => LOC::RR_TYPE, Self::MX => MX::RR_TYPE, Self::NAPTR => NAPTR::RR_TYPE, @@ -227,6 +232,7 @@ impl RecordType { Self::SSHFP => SSHFP::RR_TYPE, Self::SOA => SOA::RR_TYPE, Self::SRV => SRV::RR_TYPE, + Self::SVCB => SVCB::RR_TYPE, Self::TLSA => TLSA::RR_TYPE, Self::TXT => TXT::RR_TYPE, Self::URI => URI::RR_TYPE, diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs new file mode 100644 index 0000000..e80782d --- /dev/null +++ b/dns/src/record/svcb_https.rs @@ -0,0 +1,1029 @@ +//! The format of both SVCB and HTTPS RRs is identical. + +use core::fmt; +use std::collections::HashMap; +use std::io::{self, Seek, SeekFrom}; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use log::*; + +use crate::strings::{Labels, ReadLabels}; +use crate::wire::*; + +/// A kinda hacky but alright way to avoid copying tons of data +trait CursorExt { + /// Replace this when #[feature(cursor_remaining)] is stabilised + fn std_remaining_slice(&self) -> &[u8]; + + /// Convenience + fn truncated(&self, length: u64) -> Self; + fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T; +} + +impl CursorExt for Cursor<&[u8]> { + fn std_remaining_slice(&self) -> &[u8] { + let inner = self.get_ref(); + let len = self.position().min(inner.as_ref().len() as u64); + &inner[(len as usize)..] + } + fn truncated(&self, to_length: u64) -> Self { + let inner = self.get_ref(); + let len = inner.len() as u64; + let start = self.position().min(len); + let end = (start + to_length).min(len); + let trunc = &inner[(start as usize)..(end as usize)]; + Cursor::new(trunc) + } + fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T { + let mut trunc = self.truncated(length); + let len_hint = trunc.get_ref().len(); + let ret = f(&mut trunc, len_hint); + self.seek(SeekFrom::Current(trunc.position() as i64)) + .unwrap(); + ret + } +} + +macro_rules! u16_enum { + { + $(#[$attr:meta])* + $vis:vis enum $name:ident { + $( + $(#[$vattr:meta])* + $variant:ident = $lit:literal,)+ + } + } => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + #[repr(u16)] + $vis enum $name { + $( + $(#[$vattr])* + $variant = $lit,)+ + } + impl core::convert::TryFrom for $name { + type Error = std::io::Error; + + fn try_from(int: u16) -> Result { + match int { + $($lit => Ok(Self::$variant),)+ + _ => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("invalid value for {}: {:04x}", stringify!($name), int) + ) + ) + } + } + } + }; + { + $(#[$attr:meta])* + $vis:vis enum $name:ident { + $( + $(#[$vattr:meta])* + $variant:ident = $lit:literal,)+ + $( + @unknown + $(#[$uattr:meta])* + $unknown:ident (u16), + $( + $(#[$vattr2:meta])* + $variant2:ident = $lit2:literal,)* + )? + } + } => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + #[repr(u16)] + $vis enum $name { + $( + $(#[$vattr])* + $variant = $lit,)+ + $( + $(#[$uattr])* + $unknown(u16), + $( + $(#[$vattr2])* + $variant2 = $lit2,)* + )? + } + impl From for $name { + fn from(int: u16) -> Self { + match int { + $($lit => Self::$variant,)+ + $( + $($lit2 => Self::$variant2,)* + _ => Self::$unknown(int), + )? + } + } + } + }; +} + +// TODO: reimplement debug and use ... to truncate (base64?) output +#[derive(Debug, Clone, PartialEq)] +pub struct Opaque(/* u16 len */ Vec); + +/// Same as [Opaque] but min length is 1 +#[derive(Debug, Clone, PartialEq)] +pub struct Opaque1(/* u16 len */ Vec); + +trait ReadFromCursor: Sized { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result; +} + +impl ReadFromCursor for Opaque { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { + let len = cursor.read_u16::()?; + log::trace!("read opaque length = {}", len); + let mut vec = vec![0u8; usize::from(len)]; + cursor.read_exact(&mut vec[..])?; + Ok(Opaque(vec)) + } +} + +impl ReadFromCursor for Opaque1 { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { + let len = cursor.read_u16::()?; + log::trace!("read opaque1 length = {}", len); + if len == 0 { + return Err(io::Error::new( + io::ErrorKind::Other, + "length of opaque field was zero, but must be at least 1", + )); + } + let mut vec = vec![0u8; usize::from(len)]; + cursor.read_exact(&mut vec[..])?; + Ok(Opaque1(vec)) + } +} + +macro_rules! opaque { + ($vis:vis struct $ident:ident) => { + #[derive(Debug, Clone, PartialEq)] + $vis struct $ident($crate::record::svcb_https::Opaque); + impl $crate::record::svcb_https::ReadFromCursor for $ident { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + $crate::record::svcb_https::Opaque::read_from(cursor).map(Self) + } + } + } +} + +/// A **SVCB** record, which contains an IP address as well as a port number, +/// for specifying the location of services more precisely. +/// +/// # References +/// +/// - [RFC Draft 7](https://datatracker.ietf.org/doc/draft-ietf-dnsop-svcb-https/07/) — A DNS RR for +/// specifying the location of services (February 2000) +#[derive(PartialEq, Debug)] +pub struct SVCB { + priority: u16, + target: Labels, + parameters: Option, +} + +#[derive(PartialEq, Debug)] +pub struct HTTPS(SVCB); + +u16_enum! { + /// 14.3.2. Initial contents (subject to IANA additions) + #[derive(Copy, Eq, PartialOrd, Hash)] + enum SvcParam { + /// `mandatory` + Mandatory = 0, + /// `alpn` + Alpn = 1, + /// `no-default-alpn` + NoDefaultAlpn = 2, + Port = 3, + Ipv4Hint = 4, + Ech = 5, + Ipv6Hint = 6, + @unknown KeyNNNNN(u16), + InvalidKey = 65535, + } +} + +#[test] +fn svc_param_from_u16() { + assert_eq!(SvcParam::from(12345u16), SvcParam::KeyNNNNN(12345u16)); +} + +impl fmt::Display for SvcParam { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Ok(match self { + Self::Mandatory => f.write_str("mandatory")?, + Self::Alpn => f.write_str("alpn")?, + Self::NoDefaultAlpn => f.write_str("no-default-alpn")?, + Self::Port => f.write_str("port")?, + Self::Ipv4Hint => f.write_str("ipv4hint")?, + Self::Ech => f.write_str("echconfig")?, + Self::Ipv6Hint => f.write_str("ipv6hint")?, + Self::KeyNNNNN(n) => write!(f, "key{}", n)?, + Self::InvalidKey => f.write_str("[invalid key]")?, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +struct SvcParams { + /// List of keys that must be understood by a client to use the RR properly. + /// + /// Wire format: list of u16 network endian svcparam values + /// Presentation format: a comma-separated [ValueList] + mandatory: Vec, + /// Draft 7 section 6.1 + /// + /// Wire format: + /// Presentation format: comma-separated list of alpn-id, 1-255 characters each + /// (also, "Zone file implementations MAY disallow" commas/backslash escapes, use \002 (ascii + /// 0x02 STX (start text) character). That's a TODO + alpn: Option, + port: Option, + ipv4hint: Vec, + /// An ECHConfigList from the [ECH RFC][ech-rfc] + /// + /// [ech-rfc]: https://datatracker.ietf.org/doc/draft-ietf-tls-esni/13/ + /// + /// Wire format, the value of the parameter is an ECHConfigList [ECH], including the redundant length prefix. + /// Presentation format, the value is a single ECHConfigList encoded in Base64 [base64]. + ech: Option, + ipv6hint: Vec, + + /// For any unrecognised keys + other: HashMap, +} + +impl SvcParams { + fn read(cursor: &mut Cursor<&[u8]>) -> Result { + let mut mandatory = Default::default(); + let mut no_default_alpn: Option = None; + let mut alpn_ids = Default::default(); + let mut port = Default::default(); + let mut ipv4hint = Default::default(); + let mut ech = Default::default(); + let mut ipv6hint = Default::default(); + let mut other = HashMap::new(); + + let mut last_param = None; + + while let Ok(param) = cursor.read_u16::().map(SvcParam::from) { + trace!("read param: {:?}", param); + + // clients must consider RR invalid if params not in "strictly increasing numeric order" + // this implies no duplicate keys + if let Some(last) = last_param.replace(param) { + if param <= last { + error!("params out of order: read {:?} after {:?}", param, last,); + return Err(WireError::IO); + } + } + + let param_length = cursor.read_u16::()?; + trace!("read param length: {}", param_length); + + cursor.with_truncated(param_length as u64, |cursor, len_hint| { + match param { + SvcParam::Mandatory => { + let mps = read_convert(cursor, len_hint, |c| c.read_u16::())?; + // mandatory must not appear in its own value list + if mps.contains(&SvcParam::Mandatory) { + return Err(WireError::IO); + } + mandatory = mps + } + + SvcParam::Ipv4Hint => { + ipv4hint = read_convert(cursor, len_hint, |c| c.read_u32::())?; + } + SvcParam::Ech => { + let parsed = ech::ECHConfigList::read_from(cursor)?; + ech = Some(parsed); + } + SvcParam::Ipv6Hint => { + ipv6hint = read_convert(cursor, len_hint, |c| c.read_u128::())?; + } + SvcParam::InvalidKey => { + return Err(WireError::IO); + } + SvcParam::NoDefaultAlpn => { + no_default_alpn = Some(false); + } + SvcParam::Alpn => { + let mut ids = Vec::new(); + while let Ok(alpn_id) = AlpnId::read_from(cursor) { + trace!("read alpn_id {:?}", alpn_id); + ids.push(alpn_id) + } + if ids.is_empty() { + return Err(WireError::IO); + } + alpn_ids = ids; + } + SvcParam::KeyNNNNN(_) => { + let value = Opaque::read_from(cursor)?; + other.insert(param, value); + } + SvcParam::Port => { + port = Some(cursor.read_u16::()?); + } + } + Ok(()) + })?; + } + + if no_default_alpn.is_some() && alpn_ids.is_empty() { + return Err(WireError::IO); + } + let alpn = if alpn_ids.is_empty() { + None + } else { + Some(Alpn { + alpn_ids, + no_default_alpn: no_default_alpn.unwrap_or(false), + }) + }; + + Ok(Self { + mandatory, + alpn, + port, + ipv4hint, + ech, + ipv6hint, + other, + }) + } +} + +fn read_convert>( + cursor: &mut Cursor<&[u8]>, + len: usize, + mut f: impl FnMut(&mut Cursor<&[u8]>) -> io::Result, +) -> Result, WireError> { + let size = core::mem::size_of::(); + if len % size != 0 { + return Err(WireError::IO); + } + let mut collector = Vec::with_capacity(len / size); + let reader = core::iter::from_fn(|| f(cursor).ok().map(Nice::from)); + collector.extend(reader); + Ok(collector) +} + +#[derive(Debug, Clone, PartialEq)] +struct Alpn { + alpn_ids: Vec, + no_default_alpn: bool, +} + +#[derive(Clone, PartialEq)] +#[repr(transparent)] +struct AlpnId(Vec); + +impl From<&str> for AlpnId { + fn from(s: &str) -> Self { + let v = s.as_bytes().to_owned(); + AlpnId(v) + } +} + +impl ReadFromCursor for AlpnId { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { + let len = cursor.read_u8()?; + trace!("alpn len {}", len); + let mut vec = vec![0u8; len as _]; + cursor.read_exact(&mut vec)?; + Ok(AlpnId(vec)) + } +} + +impl fmt::Debug for AlpnId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = &self.0[..]; + f.write_str(&String::from_utf8_lossy(bytes)) + } +} + +/// ECH RFC draft 13 section 4 +mod ech { + use core::fmt; + use std::{ + convert::TryInto, + io::{self, Read}, + }; + + use byteorder::{BigEndian, ReadBytesExt}; + + use super::{CursorExt, Opaque, Opaque1, ReadFromCursor}; + + #[derive(Debug, Clone, PartialEq)] + pub struct ECHConfigList { + configs: Vec, + + /// Need a copy of the whole thing to encode as base64 + base64: String, + } + + impl ReadFromCursor for ECHConfigList { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { + let mut configs = Vec::new(); + let configs_length = cursor.read_u16::()?; + + let buf = &cursor.std_remaining_slice()[..configs_length.into()]; + let base64 = base64::encode(buf); + + for _ in 0..configs_length { + let config = ECHConfig::read_from(cursor)?; + configs.push(config); + } + Ok(Self { configs, base64 }) + } + } + + impl ReadFromCursor for ECHConfig { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { + let version = cursor.read_u16::()?; + let length = cursor.read_u16::()?; + match version { + 0xfe0d => cursor.with_truncated(u64::from(length), |cursor, _len_hint| { + let key_config = tls13::HpkeKeyConfig::read_from(cursor)?; + let maximum_name_length = cursor.read_u8()?; + let public_name = PublicName::read_from(cursor)?; + + let mut extensions = Vec::new(); + + while let ext = tls13::Extension::read_from(cursor)? { + extensions.push(ext); + } + + Ok(Self::EchConfigContents { + key_config, + maximum_name_length, + public_name, + extensions, + }) + }), + _ => { + let mut vec = vec![0u8; usize::from(length)]; + cursor.read_exact(&mut vec)?; + Ok(ECHConfig::UnknownECHVersion(version, Opaque(vec))) + } + } + } + } + + #[derive(Clone, PartialEq)] + pub struct PublicName { + inner: Vec, + } + + impl fmt::Debug for PublicName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = &self.inner[..]; + f.write_str(&String::from_utf8_lossy(bytes)) + } + } + + impl ReadFromCursor for PublicName { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { + let len = cursor.read_u8()?; + log::trace!("read ECHConfig.public_name length = {}", len); + if len == 0 || len > 254 { + return Err(io::Error::new( + io::ErrorKind::Other, + "length of opaque field was zero, but must be at least 1", + )); + } + let mut vec = vec![0u8; usize::from(len)]; + cursor.read_exact(&mut vec[..])?; + Ok(Self { inner: vec }) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub enum ECHConfig { + // if version == 0xfe0d + EchConfigContents { + key_config: tls13::HpkeKeyConfig, + maximum_name_length: u8, + // min len 1, max len 255 + public_name: PublicName, + + // any length up to 65535 + // + // each is a TLS 1.3 Extension, defined in RFC8446 section 4.2 + // + // > extensions MAY appear in any order, but + // > there MUST NOT be more than one extension of the same type in the + // > extensions block. An extension can be tagged as mandatory by using + // > an extension type codepoint with the high order bit set to 1. + + // > Clients MUST parse the extension list and check for unsupported + // > mandatory extensions. If an unsupported mandatory extension is + // > present, clients MUST ignore the "ECHConfig". + extensions: Vec, + }, + UnknownECHVersion(u16, Opaque), + } + + #[derive(Debug, Clone, PartialEq)] + pub enum EncryptedClientHello { + Outer { + cipher_suite: tls13::HpkeSymmetricCipherSuite, + config_id: u8, + enc: Opaque, + payload: Opaque1, + }, + Inner, + } + + u16_enum! { + pub enum ECHClientHelloType { + Outer = 0, + Inner = 1, + } + } + + impl ReadFromCursor for EncryptedClientHello { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + let ty: ECHClientHelloType = cursor.read_u16::()?.try_into()?; + Ok(match ty { + ECHClientHelloType::Inner => EncryptedClientHello::Inner, + ECHClientHelloType::Outer => EncryptedClientHello::Outer { + cipher_suite: ReadFromCursor::read_from(cursor)?, + config_id: cursor.read_u8()?, + enc: super::Opaque::read_from(cursor)?, + payload: super::Opaque1::read_from(cursor)?, + }, + }) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct EchOuterExtensions { + outer: Vec, + } + + mod tls13 { + use byteorder::{BigEndian, ReadBytesExt}; + + use crate::record::svcb_https::ReadFromCursor; + + // const MANDATORY: u16 = 0x1 << 15; + + // We will implement the mandatory-to-implement extensions only from RFC8446 + // + // - Supported Versions ("supported_versions"; Section 4.2.1) + // - Cookie ("cookie"; Section 4.2.2) + // - Signature Algorithms ("signature_algorithms"; Section 4.2.3) + // - Signature Algorithms Certificate ("signature_algorithms_cert"; Section 4.2.3) + // - Negotiated Groups ("supported_groups"; Section 4.2.7) + // - Key Share ("key_share"; Section 4.2.8) + // - Server Name Indication ("server_name"; Section 3 of [RFC6066]) + // + #[derive(Debug, Clone, PartialEq)] + pub enum Extension { + /// `encrypted_client_hello` + EncryptedClientHello(super::EncryptedClientHello), + /// `ech_outer_extensions` + EchOuterExtensions(super::EchOuterExtensions), + + /// `server_name`. This is the SNI field. + /// + /// This probably shouldn't appear in an ECH config! The whole point is to avoid it! + /// So we'll parse it to show if it's being used + ServerName(ServerName), + + /// `supported_versions` (TLS version negotiation) + SupportedVersions(SupportedVersions), + + // /// `supported_groups` + // SupportedGroups(NamedGroupList), + + // /// `cookie` + // Cookie(Cookie), + + // /// `key_share` + // /// + // /// We assume a KeyShareClientHello version of this structure, because these + // /// extensions are for adding to a client hello message + // KeyShare(KeyShareClientHello), + Other(ExtensionType, UnknownExtension), + } + + impl ReadFromCursor for Extension { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + let ty: ExtensionType = cursor.read_u16::()?.into(); + match ty { + // ExtensionType::ServerName => Extension::ServerName(ServerName::read_) + _ => Ok(Extension::Other(ty, UnknownExtension::read_from(cursor)?)), + } + } + } + + opaque!(pub struct UnknownExtension); + + #[derive(Debug, Clone, PartialEq)] + pub enum ServerName { + // name type 0x0000 + HostName(HostName), + Unknown(UnknownNameType), + } + pub type NameType = u16; + + opaque!(pub struct HostName); + opaque!(pub struct UnknownNameType); + + u16_enum! { + /// Draft RFC + /// 7.1. Key Encapsulation Mechanisms (KEMs) + #[allow(non_camel_case_types)] + enum HpkeKemId { + Reserved = 0x0000, + DHKEM_P256_HKDF_SHA256 = 0x0010, + DHKEM_P384_HKDF_SHA384 = 0x0011, + DHKEM_P512_HKDF_SHA512 = 0x0012, + DHKEM_X25519_HKDF_SHA512 = 0x0020, + DHKEM_X448_HKDF_SHA512 = 0x0021, + @unknown Unknown(u16), + } + } + + u16_enum! { + /// Draft RFC + /// 7.2. Key Derivation Functions (KDFs) + #[allow(non_camel_case_types)] + enum HpkeKdfId { + Reserved = 0, + HKDF_SHA256 = 1, + HKDF_SHA384 = 2, + HKDF_SHA512 = 3, + @unknown Unknown(u16), + } + } + + u16_enum! { + /// Draft RFC + /// 7.3. Authenticated Encryption with Associated Data (AEAD) Functions + #[allow(non_camel_case_types)] + enum HpkeAeadId { + Reserved = 0, + AES_128_GCM = 1, + AES_256_GCM = 2, + ChaCha20Poly1305 = 3, + @unknown Unknown(u16), + ExportOnly = 0xffff, + } + } + + #[test] + fn test_hpke() { + let hpke = HpkeAeadId::from(0x0003); + } + + #[derive(Debug, Clone, PartialEq)] + pub struct HpkeSymmetricCipherSuite { + kdf_id: HpkeKdfId, + aead_id: HpkeAeadId, + } + + impl ReadFromCursor for HpkeSymmetricCipherSuite { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + Ok(Self { + kdf_id: cursor.read_u16::()?.into(), + aead_id: cursor.read_u16::()?.into(), + }) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct HpkeKeyConfig { + config_id: u8, + kem_id: HpkeKemId, + public_key: HpkePublicKey, + // u16 len + cipher_suites: Vec, + } + + impl ReadFromCursor for HpkeKeyConfig { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + let config_id = cursor.read_u8()?; + let kem_id = cursor.read_u16::()?.into(); + let public_key = HpkePublicKey::read_from(cursor)?; + let cs_len = cursor.read_u16::()?; + let n_cipher_suites = + cs_len as usize / core::mem::size_of::(); + let mut cipher_suites = Vec::with_capacity(n_cipher_suites); + for _ in 0..n_cipher_suites { + let suite = HpkeSymmetricCipherSuite::read_from(cursor)?; + cipher_suites.push(suite); + } + Ok(Self { + config_id, + kem_id, + public_key, + cipher_suites, + }) + } + } + + opaque!(pub struct HpkePublicKey); + + #[derive(Debug, Clone, PartialEq)] + pub struct SupportedVersions { + // min length 2 (otherwise implied by TLS version field), max length 254 + // len is a u8 i suppose? + versions: Vec, + } + + // #[derive(Debug, Clone, PartialEq)] + // pub struct NamedGroupList(/* u16 len */ Vec); + + // u16_enum! { + // pub enum NamedGroup { + // /* Elliptic Curve Groups (ECDHE) */ + // Secp256r1 = 0x0017, + // Secp384r1 = 0x0018, + // Secp521r1 = 0x0019, + // X25519 = 0x001D, + // X448 = 0x001E, + // /* Finite Field Groups (DHE) */ + // Ffdhe2048 = 0x0100, + // Ffdhe3072 = 0x0101, + // Ffdhe4096 = 0x0102, + // Ffdhe6144 = 0x0103, + // Ffdhe8192 = 0x0104, + // /* Reserved Code Points */ + // @unknown + // /// ffdhe_private_use(0x01FC..0x01FF), + // /// ecdhe_private_use(0xFE00..0xFEFF), + // PrivateUse(u16), + // } + // } + + // #[derive(Debug, Clone, PartialEq)] + // struct KeyShareEntry { + // group: NamedGroup, + // key_exchange: Vec, + // } + + // #[derive(Debug, Clone, PartialEq)] + // struct KeyShareClientHello { + // // u16 len + // client_shares: Vec, + // } + + u16_enum! { + pub enum TlsVersion { + Ssl3_0 = 0x300, + Tls1_0 = 0x301, + Tls1_1 = 0x302, + Tls1_2 = 0x303, + Tls1_3 = 0x304, + @unknown Other(u16), + } + } + + // opaque!(pub struct Cookie); + + u16_enum! { + pub enum ExtensionType { + ServerName = 0, /* RFC 6066 */ + MaxFragmentLength = 1, /* RFC 6066 */ + StatusRequest = 5, /* RFC 6066 */ + SupportedGroups = 10, /* RFC 8422, 7919 */ + SignatureAlgorithms = 13, /* RFC 8446 */ + UseSrtp = 14, /* RFC 5764 */ + Heartbeat = 15, /* RFC 6520 */ + ApplicationLayerProtocolNegotiation = 16, /* RFC 7301 */ + SignedCertificateTimestamp = 18, /* RFC 6962 */ + ClientCertificateType = 19, /* RFC 7250 */ + ServerCertificateType = 20, /* RFC 7250 */ + Padding = 21, /* RFC 7685 */ + PreSharedKey = 41, /* RFC 8446 */ + EarlyData = 42, /* RFC 8446 */ + SupportedVersions = 43, /* RFC 8446 */ + Cookie = 44, /* RFC 8446 */ + PskKeyExchangeModes = 45, /* RFC 8446 */ + CertificateAuthorities = 47, /* RFC 8446 */ + OidFilters = 48, /* RFC 8446 */ + PostHandshakeAuth = 49, /* RFC 8446 */ + SignatureAlgorithmsCert = 50, /* RFC 8446 */ + KeyShare = 51, /* RFC 8446 */ + // This is the ECH extension + EncryptedClientHello = 0xfe0d, + EchOuterExtensions = 0xfd00, + @unknown Other(u16), + } + } + } +} + +impl SvcParams { + /// Takes a valid SvcParams struct and encodes it as human-readable input syntax + /// The input syntax (not RDATA) + // + // ```text,ignore + // alpha-lc = %x61-7A ; a-z + // SvcParamKey = 1*63(alpha-lc / DIGIT / "-") + // SvcParam = SvcParamKey ["=" SvcParamValue] + // SvcParamValue = char-string + // value = *OCTET + // ``` + pub fn encode(&self) -> String { + todo!() + } +} + +impl Wire for HTTPS { + const NAME: &'static str = "HTTPS"; + const RR_TYPE: u16 = 65; + + #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] + fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { + // TODO: default mandatory fields? something like that? + SVCB::read(stated_length, c).map(HTTPS) + } +} + +// +impl Wire for SVCB { + const NAME: &'static str = "SVCB"; + const RR_TYPE: u16 = 64; + + #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] + fn read(stated_length: u16, cursor: &mut Cursor<&[u8]>) -> Result { + let initial_pos = cursor.position(); + + let ret = cursor.with_truncated( + stated_length as _, + move |cursor, _| -> Result { + let priority = cursor.read_u16::()?; + trace!("Parsed priority -> {:?}", priority); + + // technically the labels are uncompressed, but this will succeed, hope nobody is + // out there compressing their labels in SVCB records + let (target, _target_length) = cursor.read_labels()?; + trace!("Parsed target -> {:?}", target); + + // AliasMode + let alias_mode = priority == 0; + + let parameters = if alias_mode { + None + } else { + Some(SvcParams::read(cursor)?) + }; + let ret = Self { + priority, + target, + parameters, + }; + Ok(ret) + }, + )?; + + let total_read = (cursor.position() - initial_pos) as u16; + if total_read != stated_length { + warn!( + "Length is incorrect (stated length {:?}, fields plus target length {:?})", + stated_length, total_read + ); + let remain = cursor.std_remaining_slice(); + warn!("remaining: {:?}", remain); + Err(WireError::WrongLabelLength { + stated_length, + length_after_labels: total_read, + }) + } else { + Ok(ret) + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + use std::sync::Once; + + fn init_logs() { + static LOG_INIT: Once = Once::new(); + LOG_INIT.call_once(|| { + env_logger::init(); + }); + } + + #[test] + fn parses() { + init_logs(); + // dog HTTPS cloudflare.com, I think + let buf = &[ + 0, 1, // priority 1 + 0, // zero length target name + // param + 0, 1, // alpn + 0, 24, // len 24 + 2, 104, 51, // len 2 "h3" + 5, 104, 51, 45, 50, 57, // len 5 "h3-..." + 5, 104, 51, 45, 50, 56, // len 5 "h3-..." + 5, 104, 51, 45, 50, 55, // len 5 "h3-..." + 2, 104, 50, // len 2 "h2" + // param + 0, 4, // ipv4hint + 0, 8, // len 8 (2 ipv4 addresses) + 104, 16, 132, 229, // address 1 + 104, 16, 133, 229, // address 2 + // param + 0, 6, // ipv6hint + 0, 32, // len 32 (2 ipv6 addresses) + 38, 6, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 104, 16, 132, 229, // 2606:4700::6810:84e5 + 38, 6, 71, 0, 0, 0, 0, 0, 0, 0, 0, 0, 104, 16, 133, 229, // 2606:4700::6810:85e8 + ]; + + let result = HTTPS::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(); + assert_eq!( + result, + HTTPS(SVCB { + priority: 1, + target: Labels::root(), + parameters: Some(SvcParams { + mandatory: vec![], + alpn: Some(Alpn { + alpn_ids: vec![ + "h3".into(), + "h3-29".into(), + "h3-28".into(), + "h3-27".into(), + "h2".into() + ], + no_default_alpn: false + }), + port: None, + ipv4hint: vec![ + "104.16.132.229".parse().unwrap(), + "104.16.133.229".parse().unwrap() + ], + ech: None, + ipv6hint: vec![ + "2606:4700::6810:84e5".parse().unwrap(), + "2606:4700::6810:85e5".parse().unwrap() + ], + other: HashMap::new(), + }), + }) + ); + } + + #[test] + fn corrupted_alpn() { + init_logs(); + let buf = &[ + 0x00, 0x01, // SvcPriority + 0, // TargetName = . + // SvcParams + 0, 1, 0, 0, 0, 0, 0, // corrupted alpn record, len 0 despite covering three bytes + 0, 3, 0, 2, 0x01, 0xbb, // port, len 2, "443" + ]; + assert_eq!(SVCB::read(16, &mut Cursor::new(buf)), Err(WireError::IO)); + } + + #[test] + fn incorrect_record_length() { + init_logs(); + let buf = &[ + 0, 1, // SvcPriority + 0, // TargetName = . + // SvcParams + 0, 3, 0, 2, 0x01, 0xbb, // port, len 2, "443" + ]; + assert_eq!( + SVCB::read(16, &mut Cursor::new(buf)), + Err(WireError::WrongLabelLength { + stated_length: 16, + length_after_labels: 9 + }) + ); + } + + #[test] + fn record_empty() { + init_logs(); + assert_eq!(SVCB::read(0, &mut Cursor::new(&[])), Err(WireError::IO)); + } + + #[test] + fn buffer_ends_abruptly() { + init_logs(); + let buf = &[ + 0x00, // half a priority + ]; + + assert_eq!(SVCB::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); + } +} diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index b2ba2be..74d1074 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::io::Cursor; +use std::io::{Cursor, Read}; /// Parameters to a SVCB/HTTPS record can be multi-valued. /// This is a fancy comma-separated list, where escaped commas \, and \044 do not separate @@ -14,93 +14,124 @@ pub struct ValueList { values: Vec>, } +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] +pub struct SingleValue { + value: Vec, +} + impl ValueList { + /// New pub fn new() -> Self { Self { values: Vec::new() } } - /// for tests - pub fn encode(input: &str) -> Self { - todo!() + /// Parses a comma separated list with escaping as defined in Appendix A.1. This variant has + /// unlimited size for each value. + pub fn parse(input: &str) -> Result { + value_list_decoding::parse(input.as_bytes()) + .finish() + .map_err(|e| std::str::from_utf8(e.input).unwrap()) + .and_then(|(remain, values)| { + if remain.is_empty() { + Ok(ValueList { values }) + } else { + Err(std::str::from_utf8(remain).unwrap()) + } + }) + } + + pub fn read_value_max( + stated_length: u16, + cursor: &mut Cursor<&[u8]>, + single_value_max: usize, + ) -> Result { + // These methods would be better with Cursor::remaining_slice if it were stable + let mut buf = vec![0u8; usize::from(stated_length)]; + cursor.read_exact(&mut buf)?; + let values = value_list_decoding::parse(&buf).no_remaining()?; + if values.iter().any(|val| val.len() > single_value_max) { + return Err(WireError::IO); + } else { + Ok(Self { values }) + } + } + + pub fn read_unlimited( + stated_length: u16, + cursor: &mut Cursor<&[u8]>, + ) -> Result { + // These methods would be better with Cursor::remaining_slice if it were stable + let mut buf = vec![0u8; usize::from(stated_length)]; + cursor.read_exact(&mut buf)?; + let values = value_list_decoding::parse(&buf).no_remaining()?; + Ok(Self { values }) + } +} + +impl SingleValue { + pub fn read(stated_length: u16, cursor: &mut Cursor<&[u8]>) -> Result { + // These methods would be better with Cursor::remaining_slice if it were stable + let mut buf = vec![0u8; usize::from(stated_length)]; + cursor.read_exact(&mut buf)?; + let value = char_string::parse(&buf).no_remaining()?; + Ok(Self { value }) } } -fn read_string(_list: &mut ValueList, _c: &mut Cursor<&[u8]>) { - todo!() +trait NoRemaining: Finish { + fn no_remaining(self) -> Result; +} + +impl NoRemaining for T +where + T: Finish, +{ + fn no_remaining(self) -> Result { + let (i, o) = self.finish().map_err(|_| WireError::IO)?; + if i.input_len() == 0 { + Ok(o) + } else { + Err(WireError::IO) + } + } } -use nom::Parser; use nom::branch::alt; +use nom::bytes::complete::{tag, take_while1}; use nom::combinator::recognize; use nom::error::ParseError; use nom::sequence::preceded; -use nom::IResult; -use nom::bytes::complete::{tag, take_while1}; +use nom::{Finish, Parser}; +use nom::{IResult, InputLength}; -/// A parser as defined by Appendix A of the draft, which describes RFC 1035 § 5.1 -/// -/// Note: Appendix A says it's not limited to 255 characters -pub fn parse_char_string(buf: &[u8]) -> IResult<&[u8], Vec> { - if buf.starts_with(b"\"") { - quoted(buf) - } else { - contiguous(buf) - } -} +use crate::WireError; #[cfg(test)] fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { - x - .map(|(remain, output)| { - (std::str::from_utf8(remain).unwrap(), String::from_utf8(output).unwrap()) + x.map(|(remain, output)| { + ( + std::str::from_utf8(remain).unwrap(), + String::from_utf8(output).unwrap(), + ) + }) + .map_err(|x| { + x.map(|y| { + <_ as ParseError<&str>>::from_error_kind(std::str::from_utf8(y.input).unwrap(), y.code) }) - .map_err(|x| x.map(|y| <_ as ParseError<&str>>::from_error_kind(std::str::from_utf8(y.input).unwrap(), y.code))) + }) } -#[test] -fn test_escaping() { - // let parse = |slice| strings(non_special(slice).map(|(a, b)| (a, Vec::from(b)))); - let mini = |slice| char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))); - assert_eq!(mini(b"\\044"), Ok(("", ','))); - - let parse = |slice| strings(contiguous(slice)); - assert!(parse(b"").is_err()); - assert_eq!(parse(b"hello"), Ok(("", "hello".to_owned()))); - assert_eq!(parse(b"hello\\044"), Ok(("", "hello,".to_owned()))); - assert_eq!(parse(b"hello\\\\"), Ok(("", "hello\\".to_owned()))); - assert_eq!(parse(b"hello\\\\\\\\"), Ok(("", "hello\\\\".to_owned()))); - assert_eq!(parse(b"hello\\*"), Ok(("", "hello*".to_owned()))); - assert_eq!(parse(b"\\,hello\\*"), Ok(("", ",hello*".to_owned()))); - assert_eq!(parse(b"\\,hello\\*("), Ok(("(", ",hello*".to_owned()))); - assert_eq!(parse(b"*;"), Ok((";", "*".to_owned()))); - assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); - assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); +fn is_non_special(split_comma: bool) -> impl Fn(u8) -> bool { + move |c: u8| match c { + b',' if split_comma => false, + // non-special. VCHAR minus DQUOTE, ";", "(", ")" and "\" + 0x21 | 0x23..=0x27 | 0x2A..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E => true, + _ => false, + } } -fn non_special(input: &[u8]) -> IResult<&[u8], &[u8]> { - fn is_non_special(c: u8) -> bool { - match c { - // - // - // - // - // - // - // - // TODO: remove comma from this to make a value-list - // - // - // - // - // - // - // - // non-special. VCHAR minus DQUOTE, ";", "(", ")" and "\" - 0x21 | 0x23..=0x27 | 0x2A..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E => true, - _ => false, - } - } - recognize(take_while1(is_non_special))(input) +fn non_special(split_comma: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], &[u8]> { + move |input| recognize(take_while1(is_non_special(split_comma)))(input) } fn char_escape(input: &[u8]) -> IResult<&[u8], u8> { @@ -112,26 +143,45 @@ fn char_escape(input: &[u8]) -> IResult<&[u8], u8> { Ok((remain, byte)) } // non-digit is VCHAR minus DIGIT - &[c @ (0x21..=0x2F | 0x3A..=0x7E), ref remain @ ..] => { - Ok((remain, c)) - } - _ => Err(nom::Err::Error(nom::error::Error::from_error_kind(input, nom::error::ErrorKind::Escaped))), + &[c @ (0x21..=0x2F | 0x3A..=0x7E), ref remain @ ..] => Ok((remain, c)), + _ => Err(nom::Err::Error(nom::error::Error::from_error_kind( + input, + nom::error::ErrorKind::Escaped, + ))), } } fn dec_octet(buf: &[u8], zero_one_or_two: u8) -> IResult<&[u8], u8> { let (hundreds, tens, ones, rest) = match zero_one_or_two { hundreds @ (0 | 1) => match buf { - [tens @ b'0'..=b'9', ones @ b'0'..=b'9', rest @ ..] => (hundreds, tens - b'0', ones - b'0', rest), - _ => return Err(nom::Err::Error(nom::error::Error::from_error_kind(buf, nom::error::ErrorKind::Escaped))), + [tens @ b'0'..=b'9', ones @ b'0'..=b'9', rest @ ..] => { + (hundreds, tens - b'0', ones - b'0', rest) + } + _ => { + return Err(nom::Err::Error(nom::error::Error::from_error_kind( + buf, + nom::error::ErrorKind::Escaped, + ))) + } }, hundreds @ 2 => match buf { - [tens @ b'0'..=b'4', ones @ b'0'..=b'9', rest @ ..] | [tens @ b'5', ones @ b'0'..=b'5', rest @ ..] => { + [tens @ b'0'..=b'4', ones @ b'0'..=b'9', rest @ ..] + | [tens @ b'5', ones @ b'0'..=b'5', rest @ ..] => { (hundreds, tens - b'0', ones - b'0', rest) } - _ => return Err(nom::Err::Error(nom::error::Error::from_error_kind(buf, nom::error::ErrorKind::Escaped))), + _ => { + return Err(nom::Err::Error(nom::error::Error::from_error_kind( + buf, + nom::error::ErrorKind::Escaped, + ))) + } }, - _ => return Err(nom::Err::Error(nom::error::Error::from_error_kind(buf, nom::error::ErrorKind::Escaped))), + _ => { + return Err(nom::Err::Error(nom::error::Error::from_error_kind( + buf, + nom::error::ErrorKind::Escaped, + ))) + } }; let byte = hundreds * 100 + tens * 10 + ones; Ok((rest, byte)) @@ -140,7 +190,7 @@ fn dec_octet(buf: &[u8], zero_one_or_two: u8) -> IResult<&[u8], u8> { #[derive(Debug)] enum EscChunk<'a> { Slice(&'a [u8]), - Byte(u8) + Byte(u8), } fn chunk_1<'a, F, E>(mut inner: F) -> impl Parser<&'a [u8], Vec, E> @@ -165,24 +215,155 @@ where if success { Ok((remain, output)) } else { - Err(nom::Err::Error(E::from_error_kind(input, nom::error::ErrorKind::Eof))) + Err(nom::Err::Error(E::from_error_kind( + input, + nom::error::ErrorKind::Eof, + ))) } } } -fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { - alt((non_special.map(EscChunk::Slice), char_escape.map(EscChunk::Byte)))(input) -} +mod char_string { + use super::*; + fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { + alt(( + non_special(false).map(EscChunk::Slice), + char_escape.map(EscChunk::Byte), + ))(input) + } + + fn contiguous(input: &[u8]) -> IResult<&[u8], Vec> { + chunk_1(contiguous_chunk).parse(input) + } + + fn quoted(input: &[u8]) -> IResult<&[u8], Vec> { + let wsc = take_while1(|b| b == b' ' || b == b'\t'); + let wsc2 = take_while1(|b| b == b' ' || b == b'\t'); + let wsc_chunk = alt((wsc, preceded(tag(b"\\"), wsc2))).map(EscChunk::Slice); + let parser = chunk_1(alt((contiguous_chunk, wsc_chunk))); + let mut parser = nom::sequence::delimited(tag(b"\""), parser, tag(b"\"")); + parser.parse(input) + } -fn contiguous(input: &[u8]) -> IResult<&[u8], Vec> { - chunk_1(contiguous_chunk).parse(input) + /// A parser as defined by Appendix A of the draft, which describes RFC 1035 § 5.1 + /// + /// Note: Appendix A says it's not limited to 255 characters + pub fn parse(input: &[u8]) -> IResult<&[u8], Vec> { + quoted.or(contiguous).parse(input) + } + + #[test] + fn test_escaping() { + // let parse = |slice| strings(non_special(slice).map(|(a, b)| (a, Vec::from(b)))); + let mini = |slice| { + char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))) + }; + assert_eq!(mini(b"\\044"), Ok(("", ','))); + let parse = |slice| strings(contiguous(slice)); + assert!(parse(b"").is_err()); + assert_eq!(parse(b"hello"), Ok(("", "hello".to_owned()))); + assert_eq!(parse(b"hello\\044"), Ok(("", "hello,".to_owned()))); + assert_eq!(parse(b"hello\\\\"), Ok(("", "hello\\".to_owned()))); + assert_eq!(parse(b"hello\\\\\\\\"), Ok(("", "hello\\\\".to_owned()))); + assert_eq!(parse(b"hello\\*"), Ok(("", "hello*".to_owned()))); + assert_eq!(parse(b"\\,hello\\*"), Ok(("", ",hello*".to_owned()))); + assert_eq!(parse(b"\\,hello\\*("), Ok(("(", ",hello*".to_owned()))); + assert_eq!(parse(b"*;"), Ok((";", "*".to_owned()))); + assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); + assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); + } } -fn quoted(input: &[u8]) -> IResult<&[u8], Vec> { - let wsc = take_while1(|b| b == b' ' || b == b'\t'); - let wsc2 = take_while1(|b| b == b' ' || b == b'\t'); - let wsc_chunk = alt((wsc, preceded(tag(b"\\"), wsc2))).map(EscChunk::Slice); - let parser = chunk_1(alt((contiguous_chunk, wsc_chunk))); - let mut parser = nom::sequence::delimited(tag(b"\""), parser, tag(b"\"")); - parser.parse(input) +mod value_list_decoding { + use super::*; + + fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { + alt(( + non_special(true).map(EscChunk::Slice), + char_escape.map(EscChunk::Byte), + ))(input) + } + + fn value_within_contiguous(input: &[u8]) -> IResult<&[u8], Vec> { + chunk_1(contiguous_chunk).parse(input) + } + + fn values_contiguous(input: &[u8]) -> IResult<&[u8], Vec>> { + nom::multi::separated_list1(tag(b","), value_within_contiguous).parse(input) + } + + fn value_within_quotes(input: &[u8]) -> IResult<&[u8], Vec> { + let wsc = take_while1(|b| b == b' ' || b == b'\t'); + let wsc2 = take_while1(|b| b == b' ' || b == b'\t'); + let wsc_chunk = alt((wsc, preceded(tag(b"\\"), wsc2))).map(EscChunk::Slice); + let mut parser = chunk_1(alt((contiguous_chunk, wsc_chunk))); + parser.parse(input) + } + + fn values_quoted(input: &[u8]) -> IResult<&[u8], Vec>> { + let values = nom::multi::separated_list1(tag(b","), value_within_quotes); + let mut parser = nom::sequence::delimited(tag(b"\""), values, tag(b"\"")); + parser.parse(input) + } + + pub fn parse(input: &[u8]) -> IResult<&[u8], Vec>> { + values_quoted.or(values_contiguous).parse(input) + } + + #[cfg(test)] + fn strings(x: IResult<&[u8], Vec>>) -> IResult<&str, Vec> { + x.map(|(remain, output)| { + ( + std::str::from_utf8(remain).unwrap(), + output + .into_iter() + .map(String::from_utf8) + .map(Result::unwrap) + .collect(), + ) + }) + .map_err(|x| { + x.map(|y| { + <_ as ParseError<&str>>::from_error_kind( + std::str::from_utf8(y.input).unwrap(), + y.code, + ) + }) + }) + } + + #[test] + fn test_escaping() { + // let parse = |slice| strings(non_special(slice).map(|(a, b)| (a, Vec::from(b)))); + let mini = |slice| { + char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))) + }; + assert_eq!(mini(b"\\044"), Ok(("", ','))); + + let parse = |slice| strings(values_contiguous(slice)); + assert!(parse(b"").is_err()); + assert_eq!(parse(b"hello"), Ok(("", vec!["hello".to_owned()]))); + assert_eq!(parse(b"hello\\044"), Ok(("", vec!["hello,".to_owned()]))); + assert_eq!( + parse(b"hello\\\\\\044"), + Ok(("", vec!["hello\\,".to_owned()])) + ); + assert_eq!( + parse(b"hello,\\\\\\044"), + Ok(("", vec!["hello".to_owned(), "\\,".to_owned()])) + ); + assert_eq!( + parse(b"hello\\\\\\\\,"), + Ok((",", vec!["hello\\\\".to_owned()])) + ); + assert_eq!(parse(b"hello\\*"), Ok(("", vec!["hello*".to_owned()]))); + assert_eq!(parse(b"\\,hello\\*"), Ok(("", vec![",hello*".to_owned()]))); + assert_eq!( + parse(b"\\,hello\\*("), + Ok(("(", vec![",hello*".to_owned()])) + ); + assert_eq!(parse(b"*;"), Ok((";", vec!["*".to_owned()]))); + assert_eq!(parse(b"*\""), Ok(("\"", vec!["*".to_owned()]))); + assert_eq!(parse(b"*\""), Ok(("\"", vec!["*".to_owned()]))); + } } diff --git a/dns/src/wire.rs b/dns/src/wire.rs index 2559ab3..cd3f11d 100644 --- a/dns/src/wire.rs +++ b/dns/src/wire.rs @@ -188,6 +188,7 @@ impl Record { RecordType::EUI48 => read_record!(EUI48), RecordType::EUI64 => read_record!(EUI64), RecordType::HINFO => read_record!(HINFO), + RecordType::HTTPS => read_record!(HTTPS), RecordType::LOC => read_record!(LOC), RecordType::MX => read_record!(MX), RecordType::NAPTR => read_record!(NAPTR), @@ -197,6 +198,7 @@ impl Record { RecordType::SSHFP => read_record!(SSHFP), RecordType::SOA => read_record!(SOA), RecordType::SRV => read_record!(SRV), + RecordType::SVCB => read_record!(SVCB), RecordType::TLSA => read_record!(TLSA), RecordType::TXT => read_record!(TXT), RecordType::URI => read_record!(URI), diff --git a/src/colours.rs b/src/colours.rs index 9535722..cefa0b9 100644 --- a/src/colours.rs +++ b/src/colours.rs @@ -30,6 +30,7 @@ pub struct Colours { pub sshfp: Style, pub soa: Style, pub srv: Style, + pub svcb: Style, pub tlsa: Style, pub txt: Style, pub uri: Style, @@ -65,6 +66,7 @@ impl Colours { sshfp: Cyan.normal(), soa: Purple.normal(), srv: Cyan.normal(), + svcb: Cyan.normal(), tlsa: Yellow.normal(), txt: Yellow.normal(), uri: Yellow.normal(), diff --git a/src/output.rs b/src/output.rs index 9ef1fba..7ec21de 100644 --- a/src/output.rs +++ b/src/output.rs @@ -285,6 +285,12 @@ impl TextFormat { Record::Other { bytes, .. } => { format!("{:?}", bytes) } + Record::HTTPS(https) => { + format!("{:?}", https) + } + Record::SVCB(svcb) => { + format!("{:?}", svcb) + } } } @@ -400,6 +406,7 @@ fn json_record_type_name(record: RecordType) -> JsonValue { RecordType::EUI48 => "EUI48".into(), RecordType::EUI64 => "EUI64".into(), RecordType::HINFO => "HINFO".into(), + RecordType::HTTPS => "HTTPS".into(), RecordType::LOC => "LOC".into(), RecordType::MX => "MX".into(), RecordType::NAPTR => "NAPTR".into(), @@ -409,6 +416,7 @@ fn json_record_type_name(record: RecordType) -> JsonValue { RecordType::SOA => "SOA".into(), RecordType::SRV => "SRV".into(), RecordType::SSHFP => "SSHFP".into(), + RecordType::SVCB => "SVCB".into(), RecordType::TLSA => "TLSA".into(), RecordType::TXT => "TXT".into(), RecordType::URI => "URI".into(), @@ -431,6 +439,7 @@ fn json_record_name(record: &Record) -> JsonValue { Record::EUI48(_) => "EUI48".into(), Record::EUI64(_) => "EUI64".into(), Record::HINFO(_) => "HINFO".into(), + Record::HTTPS(_) => "HTTPS".into(), Record::LOC(_) => "LOC".into(), Record::MX(_) => "MX".into(), Record::NAPTR(_) => "NAPTR".into(), @@ -440,6 +449,7 @@ fn json_record_name(record: &Record) -> JsonValue { Record::SOA(_) => "SOA".into(), Record::SRV(_) => "SRV".into(), Record::SSHFP(_) => "SSHFP".into(), + Record::SVCB(_) => "SVCB".into(), Record::TLSA(_) => "TLSA".into(), Record::TXT(_) => "TXT".into(), Record::URI(_) => "URI".into(), @@ -589,6 +599,8 @@ fn json_record_data(record: Record) -> JsonValue { "bytes": bytes, } } + Record::HTTPS(_https) => todo!(), + Record::SVCB(_svcb) => todo!(), } } diff --git a/src/table.rs b/src/table.rs index 26ff0f6..0cacca4 100644 --- a/src/table.rs +++ b/src/table.rs @@ -133,6 +133,8 @@ impl Table { Record::TLSA(_) => self.colours.tlsa.paint("TLSA"), Record::TXT(_) => self.colours.txt.paint("TXT"), Record::URI(_) => self.colours.uri.paint("URI"), + Record::HTTPS(_) => self.colours.svcb.paint("HTTPS"), + Record::SVCB(_) => self.colours.svcb.paint("SVCB"), Record::Other { ref type_number, .. } => self.colours.unknown.paint(type_number.to_string()), } From 57111f35748831e8ebd94c548bd5961bbdfaa01d Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 00:37:53 +1000 Subject: [PATCH 03/26] impl fmt::Display for SVCB/HTTPS --- Cargo.lock | 7 ++ dns/Cargo.toml | 1 + dns/src/record/svcb_https.rs | 128 +++++++++++++++++++++++++++++++---- dns/src/strings.rs | 4 +- dns/src/value_list.rs | 125 +++++++++++++++++++++++++++++++--- src/output.rs | 4 +- 6 files changed, 243 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 41f8dc3..1ad37e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,12 +139,19 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" +[[package]] +name = "display_utils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cabb9d45c20801e16b8483e998bd1ebe4a07bc56406135ded5b4e3ad140c30" + [[package]] name = "dns" version = "0.2.0-pre" dependencies = [ "base64", "byteorder", + "display_utils", "env_logger", "log", "mutagen", diff --git a/dns/Cargo.toml b/dns/Cargo.toml index 9ab1467..393a876 100644 --- a/dns/Cargo.toml +++ b/dns/Cargo.toml @@ -25,6 +25,7 @@ unic-idna = { version = "0.9.0", optional = true } # mutation testing mutagen = { git = "https://github.com/llogiq/mutagen", optional = true } +display_utils = "0.4.0" [dev-dependencies] pretty_assertions = "0.7" diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index e80782d..d2a97f5 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -1,7 +1,7 @@ //! The format of both SVCB and HTTPS RRs is identical. use core::fmt; -use std::collections::HashMap; +use std::collections::BTreeMap; use std::io::{self, Seek, SeekFrom}; use std::net::{Ipv4Addr, Ipv6Addr}; @@ -10,6 +10,8 @@ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; +use crate::value_list::escaping; + /// A kinda hacky but alright way to avoid copying tons of data trait CursorExt { /// Replace this when #[feature(cursor_remaining)] is stabilised @@ -125,6 +127,12 @@ macro_rules! u16_enum { #[derive(Debug, Clone, PartialEq)] pub struct Opaque(/* u16 len */ Vec); +impl fmt::Display for Opaque { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + base64::display::Base64Display::with_config(&self.0[..], base64::STANDARD).fmt(f) + } +} + /// Same as [Opaque] but min length is 1 #[derive(Debug, Clone, PartialEq)] pub struct Opaque1(/* u16 len */ Vec); @@ -190,7 +198,7 @@ pub struct HTTPS(SVCB); u16_enum! { /// 14.3.2. Initial contents (subject to IANA additions) - #[derive(Copy, Eq, PartialOrd, Hash)] + #[derive(Copy, Eq, PartialOrd, Ord, Hash)] enum SvcParam { /// `mandatory` Mandatory = 0, @@ -228,7 +236,7 @@ impl fmt::Display for SvcParam { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Default)] struct SvcParams { /// List of keys that must be understood by a client to use the RR properly. /// @@ -253,20 +261,91 @@ struct SvcParams { ech: Option, ipv6hint: Vec, - /// For any unrecognised keys - other: HashMap, + /// For any unrecognised keys. BTreeMap, because keys are sorted this way + other: BTreeMap, +} + +impl fmt::Display for SvcParams { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + mandatory, + alpn, + port, + ipv4hint, + ech, + ipv6hint, + other, + } = self; + let mut after_first = false; + if !mandatory.is_empty() { + write!( + f, + "mandatory={}", + display_utils::join(mandatory.iter(), ",") + )?; + after_first = true; + } + if let Some(alpn) = alpn { + if after_first { + f.write_str(" ")?; + } + f.write_str("alpn=")?; + escaping::format_values_iter(alpn.alpn_ids.iter().map(|id| id.0.as_slice()), f)?; + if alpn.no_default_alpn { + write!(f, " no-default-alpn")?; + } + after_first = true; + } + if let &Some(port) = port { + if after_first { + f.write_str(" ")?; + } + write!(f, "port={}", port)?; + after_first = true; + } + if let Some(ech) = ech { + if after_first { + f.write_str(" ")?; + } + write!(f, "ech={}", ech.base64)?; + after_first = true; + } + if !ipv4hint.is_empty() { + if after_first { + f.write_str(" ")?; + } + write!(f, "ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; + after_first = true; + } + if !ipv6hint.is_empty() { + if after_first { + f.write_str(" ")?; + } + write!(f, "ipv6hint={}", display_utils::join(ipv6hint.iter(), ","))?; + after_first = true; + } + if !other.is_empty() { + if after_first { + f.write_str(" ")?; + } + display_utils::join_format(other.iter(), " ", |(k, v), f| write!(f, "{}={}", k, v)) + .fmt(f)?; + // after_first = true; + } + Ok(()) + } } impl SvcParams { fn read(cursor: &mut Cursor<&[u8]>) -> Result { let mut mandatory = Default::default(); - let mut no_default_alpn: Option = None; + let mut no_default_alpn = false; let mut alpn_ids = Default::default(); let mut port = Default::default(); let mut ipv4hint = Default::default(); let mut ech = Default::default(); let mut ipv6hint = Default::default(); - let mut other = HashMap::new(); + let mut other = BTreeMap::new(); let mut last_param = None; @@ -310,7 +389,7 @@ impl SvcParams { return Err(WireError::IO); } SvcParam::NoDefaultAlpn => { - no_default_alpn = Some(false); + no_default_alpn = true; } SvcParam::Alpn => { let mut ids = Vec::new(); @@ -335,7 +414,7 @@ impl SvcParams { })?; } - if no_default_alpn.is_some() && alpn_ids.is_empty() { + if no_default_alpn && alpn_ids.is_empty() { return Err(WireError::IO); } let alpn = if alpn_ids.is_empty() { @@ -343,7 +422,7 @@ impl SvcParams { } else { Some(Alpn { alpn_ids, - no_default_alpn: no_default_alpn.unwrap_or(false), + no_default_alpn, }) }; @@ -425,7 +504,7 @@ mod ech { configs: Vec, /// Need a copy of the whole thing to encode as base64 - base64: String, + pub base64: String, } impl ReadFromCursor for ECHConfigList { @@ -850,7 +929,6 @@ impl Wire for HTTPS { } } -// impl Wire for SVCB { const NAME: &'static str = "SVCB"; const RR_TYPE: u16 = 64; @@ -905,6 +983,28 @@ impl Wire for SVCB { } } +impl fmt::Display for HTTPS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl fmt::Display for SVCB { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + priority, + target, + parameters, + } = self; + + write!(f, "{} {:?}", priority, target.to_string())?; + if let Some(params) = parameters { + write!(f, " {}", params)?; + } + Ok(()) + } +} + #[cfg(test)] mod test { use super::*; @@ -962,7 +1062,7 @@ mod test { "h3-27".into(), "h2".into() ], - no_default_alpn: false + no_default_alpn: false, }), port: None, ipv4hint: vec![ @@ -974,7 +1074,7 @@ mod test { "2606:4700::6810:84e5".parse().unwrap(), "2606:4700::6810:85e5".parse().unwrap() ], - other: HashMap::new(), + other: BTreeMap::new(), }), }) ); diff --git a/dns/src/strings.rs b/dns/src/strings.rs index 5c33494..cea074b 100644 --- a/dns/src/strings.rs +++ b/dns/src/strings.rs @@ -83,6 +83,9 @@ impl Labels { impl fmt::Display for Labels { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.len() == 0 { + return write!(f, "."); + } for (_, segment) in &self.segments { write!(f, "{}.", segment)?; } @@ -204,7 +207,6 @@ fn read_string_recursive(labels: &mut Labels, c: &mut Cursor<&[u8]>, recursions: Ok(bytes_read) } - #[cfg(test)] mod test { use super::*; diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index 74d1074..1d0ddcc 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] +use core::fmt::{self, Display}; use std::io::{Cursor, Read}; /// Parameters to a SVCB/HTTPS record can be multi-valued. @@ -121,17 +122,125 @@ fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { }) } -fn is_non_special(split_comma: bool) -> impl Fn(u8) -> bool { +fn is_non_special(split_comma: bool, whitespace: bool) -> impl Fn(u8) -> bool { move |c: u8| match c { b',' if split_comma => false, // non-special. VCHAR minus DQUOTE, ";", "(", ")" and "\" 0x21 | 0x23..=0x27 | 0x2A..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E => true, + b' ' | b'\t' if whitespace => true, _ => false, } } -fn non_special(split_comma: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], &[u8]> { - move |input| recognize(take_while1(is_non_special(split_comma)))(input) +fn non_special(split_comma: bool, whitespace: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], &[u8]> { + move |input| recognize(take_while1(is_non_special(split_comma, whitespace)))(input) +} + +pub mod escaping { + use super::*; + + fn iter_parser( + input: I, + mut parser: impl Parser + Clone, + ) -> impl Iterator> + Clone { + let mut remain = input; + core::iter::from_fn(move || match parser.parse(remain) { + Ok((rest, chunk)) => { + remain = rest; + Some(Ok(chunk)) + } + Err(nom::Err::Error(..)) => None, + Err(nom::Err::Failure(..)) => Some(Err(fmt::Error)), + Err(nom::Err::Incomplete(..)) => Some(Err(fmt::Error)), + }) + } + + mod valuelist { + + fn single_byte(input: &[u8]) -> IResult<&[u8], u8> { + let (byte, remain) = input.split_first().ok_or_else(|| { + nom::Err::Error(ParseError::from_error_kind( + input, + nom::error::ErrorKind::Char, + )) + })?; + Ok((remain, *byte)) + } + + use super::*; + fn chunk(quoted: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], EscChunk<'_>> + Clone { + move |remain| { + non_special(true, false) + .map(EscChunk::Slice) + .or(single_byte.map(EscChunk::Byte)) + .parse(remain) + } + } + pub fn iter_unquoted( + input: &[u8], + ) -> impl Iterator, fmt::Error>> + Clone { + iter_parser(input, chunk(false)) + } + + pub fn iter_quoted( + input: &[u8], + ) -> impl Iterator, fmt::Error>> + Clone { + iter_parser(input, chunk(true)) + } + } + + // fn format_iter<'a, 'f>(f: &mut fmt::Formatter<'f>, mut iter: impl Iterator, fmt::Error>>) -> fmt::Result { + // iter.try_for_each() + // } + + fn format_iter<'a, I>(iter: I) -> impl fmt::Display + 'a + where + I: Iterator, fmt::Error>> + Clone + 'a, + { + display_utils::join_format(iter, ",", |chunk, f| match chunk? { + EscChunk::Slice(slice) => { + // Technically we know this is printable ASCII. is that utf8? + let string = std::str::from_utf8(slice).map_err(|e| { + log::error!("error escaping string: {}", e); + fmt::Error + })?; + f.write_str(string)?; + Ok(()) + } + EscChunk::Byte(byte) => { + write!(f, "\\{:3o}", byte) + } + }) + } + + pub fn format_values_iter<'a>( + iter: impl Iterator + Clone, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + // technically this could pull up a false positive, but that's fine, quotes are always valid + let should_quote = iter + .clone() + .any(|val| val.contains(&b' ') || val.contains(&b'\t')); + if should_quote { + let iter_fmt = display_utils::join_format(iter, ",", |value, f| { + let iter = valuelist::iter_quoted(value); + format_iter(iter).fmt(f) + }); + write!(f, "\"{}\"", iter_fmt)?; + } else { + display_utils::join_format(iter, ",", |value, f| { + let iter = valuelist::iter_unquoted(value); + format_iter(iter).fmt(f) + }).fmt(f)?; + } + Ok(()) + } + + impl fmt::Display for ValueList { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + format_values_iter(self.values.iter().map(|val| val.as_slice()), f) + } + } } fn char_escape(input: &[u8]) -> IResult<&[u8], u8> { @@ -187,8 +296,8 @@ fn dec_octet(buf: &[u8], zero_one_or_two: u8) -> IResult<&[u8], u8> { Ok((rest, byte)) } -#[derive(Debug)] -enum EscChunk<'a> { +#[derive(Debug, Clone)] +pub enum EscChunk<'a> { Slice(&'a [u8]), Byte(u8), } @@ -227,7 +336,7 @@ mod char_string { use super::*; fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { alt(( - non_special(false).map(EscChunk::Slice), + non_special(false, false).map(EscChunk::Slice), char_escape.map(EscChunk::Byte), ))(input) } @@ -254,7 +363,6 @@ mod char_string { #[test] fn test_escaping() { - // let parse = |slice| strings(non_special(slice).map(|(a, b)| (a, Vec::from(b)))); let mini = |slice| { char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))) }; @@ -279,7 +387,7 @@ mod value_list_decoding { fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { alt(( - non_special(true).map(EscChunk::Slice), + non_special(true, false).map(EscChunk::Slice), char_escape.map(EscChunk::Byte), ))(input) } @@ -334,7 +442,6 @@ mod value_list_decoding { #[test] fn test_escaping() { - // let parse = |slice| strings(non_special(slice).map(|(a, b)| (a, Vec::from(b)))); let mini = |slice| { char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))) }; diff --git a/src/output.rs b/src/output.rs index 7ec21de..abb2714 100644 --- a/src/output.rs +++ b/src/output.rs @@ -286,10 +286,10 @@ impl TextFormat { format!("{:?}", bytes) } Record::HTTPS(https) => { - format!("{:?}", https) + format!("{}", https) } Record::SVCB(svcb) => { - format!("{:?}", svcb) + format!("{}", svcb) } } } From cb67e51f9348b61261e898c19208d469131e080a Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 00:40:18 +1000 Subject: [PATCH 04/26] Add some of the test vectors from the RFC --- dns/src/record/svcb_https.rs | 109 ++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 9 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index d2a97f5..f7404df 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -1005,20 +1005,20 @@ impl fmt::Display for SVCB { } } +#[cfg(test)] +fn init_logs() { + use std::sync::Once; + static LOG_INIT: Once = Once::new(); + LOG_INIT.call_once(|| { + env_logger::init(); + }); +} + #[cfg(test)] mod test { use super::*; use pretty_assertions::assert_eq; - use std::sync::Once; - - fn init_logs() { - static LOG_INIT: Once = Once::new(); - LOG_INIT.call_once(|| { - env_logger::init(); - }); - } - #[test] fn parses() { init_logs(); @@ -1127,3 +1127,94 @@ mod test { assert_eq!(SVCB::read(23, &mut Cursor::new(buf)), Err(WireError::IO)); } } + +/// See the draft RFC +#[cfg(test)] +mod test_vectors { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn alias_form() { + init_logs(); + let buf = b"\x00\x00\x03foo\x07example\x03com\x00"; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), + Ok(SVCB { + priority: 0, + target: Labels::encode("foo.example.com").unwrap(), + parameters: None, + }) + ); + } + + #[test] + fn service_form() { + init_logs(); + let buf = b"\x00\x01\x00"; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), + Ok(SVCB { + priority: 1, + target: Labels::encode(".").unwrap(), + parameters: Some(SvcParams::default()), + }) + ); + } + + #[test] + fn service_form_2() { + init_logs(); + let buf = &[ + 0x00, 0x10, // priority + 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, // target + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, + 0x03, // key 3 + 0x00, 0x02, // length 2 + 0x00, 0x35, // value + ]; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), + Ok(SVCB { + priority: 16, + target: Labels::encode("foo.example.com.").unwrap(), + parameters: Some(SvcParams { + port: Some(53), + ..SvcParams::default() + }), + }) + ); + } + + #[test] + fn service_form_3() { + init_logs(); + let buf = &[ + 0x00, 0x01, // priority + 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, // target + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // + 0x03, 0x63, 0x6f, 0x6d, 0x00, // + 0x02, 0x9b, // key 667 + 0x00, 0x05, // length 5 + 0x68, 0x65, 0x6c, 0x6c, 0x6f, // value + ]; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), + Ok(SVCB { + priority: 1, + target: Labels::encode("foo.example.com.").unwrap(), + parameters: Some(SvcParams { + other: { + let mut map = BTreeMap::new(); + map.insert( + SvcParam::KeyNNNNN(667), + Opaque(vec![0x68, 0x65, 0x6c, 0x6c, 0x6f]), + ); + map + }, + ..SvcParams::default() + }), + }) + ); + } +} From 5a1dfcc677a1da8f33b7612aea87c84bfa0d6c0c Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 00:42:08 +1000 Subject: [PATCH 05/26] Add failing test for ignoring params in alias mode --- dns/src/record/svcb_https.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index f7404df..c789ac7 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -1111,6 +1111,25 @@ mod test { ); } + #[test] + fn ignore_alias_mode_params() { + init_logs(); + let buf = &[ + 0, 0, // SvcPriority + 0, // TargetName = . + // SvcParams + 0, 3, 0, 2, 0x01, 0xbb, // port, len 2, "443" + ]; + assert_eq!( + SVCB::read(16, &mut Cursor::new(buf)), + Ok(SVCB { + priority: 0, + target: Labels::root(), + parameters: None, + }) + ); + } + #[test] fn record_empty() { init_logs(); From 48b9268acfe4975c0a32a5dc8ad955fae3dfb80f Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 00:42:12 +1000 Subject: [PATCH 06/26] Fix double parsing of param length field for unknown keys --- dns/src/record/svcb_https.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index c789ac7..3a7d5c2 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -403,8 +403,9 @@ impl SvcParams { alpn_ids = ids; } SvcParam::KeyNNNNN(_) => { - let value = Opaque::read_from(cursor)?; - other.insert(param, value); + let mut vec = vec![0u8; param_length as usize]; + cursor.read_exact(&mut vec)?; + other.insert(param, Opaque(vec)); } SvcParam::Port => { port = Some(cursor.read_u16::()?); From 5d165dc1db2f8c9f49e4ead35fb1cff643b4e1ef Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 00:47:28 +1000 Subject: [PATCH 07/26] fix ignore params in AliasMode --- dns/src/record/svcb_https.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 3a7d5c2..b0e68b3 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -949,14 +949,11 @@ impl Wire for SVCB { let (target, _target_length) = cursor.read_labels()?; trace!("Parsed target -> {:?}", target); - // AliasMode - let alias_mode = priority == 0; + // ServiceMode + let service_mode = priority > 0; - let parameters = if alias_mode { - None - } else { - Some(SvcParams::read(cursor)?) - }; + // parse them anyway, but reduce to None if in alias mode + let parameters = Some(SvcParams::read(cursor)?).filter(|_| service_mode); let ret = Self { priority, target, @@ -1116,13 +1113,13 @@ mod test { fn ignore_alias_mode_params() { init_logs(); let buf = &[ - 0, 0, // SvcPriority + 0, 0, // SvcPriority 0, therefore AliasMode 0, // TargetName = . // SvcParams 0, 3, 0, 2, 0x01, 0xbb, // port, len 2, "443" ]; assert_eq!( - SVCB::read(16, &mut Cursor::new(buf)), + SVCB::read(9, &mut Cursor::new(buf)), Ok(SVCB { priority: 0, target: Labels::root(), @@ -1213,7 +1210,7 @@ mod test_vectors { 0x00, 0x01, // priority 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, // target 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, // - 0x03, 0x63, 0x6f, 0x6d, 0x00, // + 0x03, 0x63, 0x6f, 0x6d, 0x00, // 0x02, 0x9b, // key 667 0x00, 0x05, // length 5 0x68, 0x65, 0x6c, 0x6c, 0x6f, // value From 08c16b826b3084b5b0ccb5ec1c63544b069b52ce Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 02:13:23 +1000 Subject: [PATCH 08/26] More test vectors (fail on ValueList::parse) --- dns/src/record/svcb_https.rs | 326 +++++++++++++++++++++++++++++------ dns/src/value_list.rs | 84 ++++----- 2 files changed, 311 insertions(+), 99 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index b0e68b3..68452d1 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -129,7 +129,7 @@ pub struct Opaque(/* u16 len */ Vec); impl fmt::Display for Opaque { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - base64::display::Base64Display::with_config(&self.0[..], base64::STANDARD).fmt(f) + escaping::format_values_iter(false, core::iter::once(&self.0[..]), f) } } @@ -290,7 +290,7 @@ impl fmt::Display for SvcParams { f.write_str(" ")?; } f.write_str("alpn=")?; - escaping::format_values_iter(alpn.alpn_ids.iter().map(|id| id.0.as_slice()), f)?; + escaping::format_values_iter(true, alpn.alpn_ids.iter().map(|id| id.0.as_slice()), f)?; if alpn.no_default_alpn { write!(f, " no-default-alpn")?; } @@ -484,7 +484,7 @@ impl ReadFromCursor for AlpnId { impl fmt::Debug for AlpnId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bytes = &self.0[..]; - f.write_str(&String::from_utf8_lossy(bytes)) + String::from_utf8_lossy(bytes).fmt(f) } } @@ -903,22 +903,6 @@ mod ech { } } -impl SvcParams { - /// Takes a valid SvcParams struct and encodes it as human-readable input syntax - /// The input syntax (not RDATA) - // - // ```text,ignore - // alpha-lc = %x61-7A ; a-z - // SvcParamKey = 1*63(alpha-lc / DIGIT / "-") - // SvcParam = SvcParamKey ["=" SvcParamValue] - // SvcParamValue = char-string - // value = *OCTET - // ``` - pub fn encode(&self) -> String { - todo!() - } -} - impl Wire for HTTPS { const NAME: &'static str = "HTTPS"; const RR_TYPE: u16 = 65; @@ -995,9 +979,9 @@ impl fmt::Display for SVCB { parameters, } = self; - write!(f, "{} {:?}", priority, target.to_string())?; + write!(f, "{} {}", priority, target)?; if let Some(params) = parameters { - write!(f, " {}", params)?; + write!(f, "{}{}", if target.len() > 0 { " " } else { "" }, params)?; } Ok(()) } @@ -1148,6 +1132,8 @@ mod test { /// See the draft RFC #[cfg(test)] mod test_vectors { + use crate::ValueList; + use super::*; use pretty_assertions::assert_eq; @@ -1155,28 +1141,32 @@ mod test_vectors { fn alias_form() { init_logs(); let buf = b"\x00\x00\x03foo\x07example\x03com\x00"; + let value = SVCB { + priority: 0, + target: Labels::encode("foo.example.com").unwrap(), + parameters: None, + }; assert_eq!( - SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), - Ok(SVCB { - priority: 0, - target: Labels::encode("foo.example.com").unwrap(), - parameters: None, - }) + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) ); + assert_eq!(value.to_string(), "0 foo.example.com."); } #[test] fn service_form() { init_logs(); let buf = b"\x00\x01\x00"; + let value = SVCB { + priority: 1, + target: Labels::encode(".").unwrap(), + parameters: Some(SvcParams::default()), + }; assert_eq!( - SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), - Ok(SVCB { - priority: 1, - target: Labels::encode(".").unwrap(), - parameters: Some(SvcParams::default()), - }) + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) ); + assert_eq!(value.to_string(), "1 ."); } #[test] @@ -1190,17 +1180,19 @@ mod test_vectors { 0x00, 0x02, // length 2 0x00, 0x35, // value ]; + let value = SVCB { + priority: 16, + target: Labels::encode("foo.example.com.").unwrap(), + parameters: Some(SvcParams { + port: Some(53), + ..SvcParams::default() + }), + }; assert_eq!( - SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), - Ok(SVCB { - priority: 16, - target: Labels::encode("foo.example.com.").unwrap(), - parameters: Some(SvcParams { - port: Some(53), - ..SvcParams::default() - }), - }) + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) ); + assert_eq!(value.to_string(), "16 foo.example.com. port=53"); } #[test] @@ -1215,23 +1207,243 @@ mod test_vectors { 0x00, 0x05, // length 5 0x68, 0x65, 0x6c, 0x6c, 0x6f, // value ]; + let value = SVCB { + priority: 1, + target: Labels::encode("foo.example.com.").unwrap(), + parameters: Some(SvcParams { + other: { + let mut map = BTreeMap::new(); + map.insert( + SvcParam::KeyNNNNN(667), + Opaque(vec![0x68, 0x65, 0x6c, 0x6c, 0x6f]), + ); + map + }, + ..SvcParams::default() + }), + }; assert_eq!( - SVCB::read(buf.len() as u16, &mut Cursor::new(buf)), - Ok(SVCB { - priority: 1, - target: Labels::encode("foo.example.com.").unwrap(), - parameters: Some(SvcParams { - other: { - let mut map = BTreeMap::new(); - map.insert( - SvcParam::KeyNNNNN(667), - Opaque(vec![0x68, 0x65, 0x6c, 0x6c, 0x6f]), - ); - map - }, - ..SvcParams::default() + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) + ); + assert_eq!(value.to_string(), "1 foo.example.com. key667=hello"); + } + + #[test] + fn service_form_4() { + let buf = &[ + 0x00, 0x01, // priority + 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, + 0x6f, 0x6d, 0x00, // target + 0x02, 0x9b, // key 667 + 0x00, 0x09, // length 9 + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2, 0x71, 0x6f, 0x6f, // value + ]; + let value = SVCB { + priority: 1, + target: Labels::encode("foo.example.com.").unwrap(), + parameters: Some(SvcParams { + other: { + let mut map = BTreeMap::new(); + map.insert( + SvcParam::KeyNNNNN(667), + Opaque(vec![0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2, 0x71, 0x6f, 0x6f]), + ); + map + }, + ..SvcParams::default() + }), + }; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) + ); + + // we avoid writing the quotes on values where possible, so this differs from the test + // vector (which is for a parser, not a formatter?) + assert_eq!(value.to_string(), "1 foo.example.com. key667=hello\\210qoo"); + } + + #[test] + fn service_form_5() { + let buf = &[ + 0x00, 0x01, // priority + 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, + 0x6f, 0x6d, 0x00, // target + 0x00, 0x06, // key 6 + 0x00, 0x20, // length 0x32, + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, // first address + 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, + 0x00, 0x01, // second address + ]; + let value = SVCB { + priority: 1, + target: Labels::encode("foo.example.com.").unwrap(), + parameters: Some(SvcParams { + ipv6hint: vec![ + "2001:db8::1".parse().unwrap(), + "2001:db8::53:1".parse().unwrap(), + ], + ..Default::default() + }), + }; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) + ); + + // we avoid writing the quotes on values where possible, so this differs from the test + // vector (which is for a parser, not a formatter?) + assert_eq!( + value.to_string(), + "1 foo.example.com. ipv6hint=2001:db8::1,2001:db8::53:1" + ); + } + + #[test] + fn service_form_6() { + let buf = &[ + 0x00, 0x01, // priority + 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, + 0x6f, 0x6d, 0x00, // target + 0x00, 0x06, // key 6 + 0x00, 0x10, // length 0x32, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xc6, 0x33, + 0x64, 0x64, // first address + ]; + let value = SVCB { + priority: 1, + target: Labels::encode("foo.example.com.").unwrap(), + parameters: Some(SvcParams { + ipv6hint: vec!["::ffff:198.51.100.100".parse().unwrap()], + ..Default::default() + }), + }; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) + ); + + // we avoid writing the quotes on values where possible, so this differs from the test + // vector (which is for a parser, not a formatter?) + assert_eq!( + value.to_string(), + "1 foo.example.com. ipv6hint=::ffff:198.51.100.100" + ); + } + + #[test] + fn service_form_7() { + let buf = &[ + 0x00, 0x10, // priority + 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x6f, + 0x72, 0x67, 0x00, // target + 0x00, 0x00, // key 0 + 0x00, 0x04, // param length 4 + 0x00, 0x01, // value: key 1 + 0x00, 0x04, // value: key 4 + 0x00, 0x01, // key 1 + 0x00, 0x09, // param length 9 + 0x02, // alpn length 2 + 0x68, 0x32, // alpn value + 0x05, // alpn length 5 + 0x68, 0x33, 0x2d, 0x31, 0x39, // alpn value + 0x00, 0x04, // key 4 + 0x00, 0x04, // param length 4 + 0xc0, 0x00, 0x02, 0x01, // param value + ]; + let value = SVCB { + priority: 16, + target: Labels::encode("foo.example.org.").unwrap(), + parameters: Some(SvcParams { + mandatory: vec![SvcParam::Alpn, SvcParam::Ipv4Hint], + alpn: Some(Alpn { + alpn_ids: vec!["h2".into(), "h3-19".into()], + no_default_alpn: false, }), - }) + ipv4hint: vec!["192.0.2.1".parse().unwrap()], + ..Default::default() + }), + }; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) + ); + + assert_eq!( + value.to_string(), + "16 foo.example.org. mandatory=alpn,ipv4hint alpn=h2,h3-19 ipv4hint=192.0.2.1" ); } + + #[test] + fn service_form_8() { + let buf = &[ + 0x00, 0x10, // priority + 0x03, 0x66, 0x6f, 0x6f, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x6f, + 0x72, 0x67, 0x00, // target + 0x00, 0x01, // key 1 + 0x00, 0x0c, // param length 12, + 0x08, // alpn length 8 + 0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, // alpn value + 0x02, // alpn length 2 + 0x68, 0x32, // alpn value + ]; + let value = SVCB { + priority: 16, + target: Labels::encode("foo.example.org.").unwrap(), + parameters: Some(SvcParams { + alpn: Some(Alpn { + alpn_ids: vec!["f\\oo,bar".into(), "h2".into()], + no_default_alpn: false, + }), + ..Default::default() + }), + }; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) + ); + + assert_eq!( + value.to_string(), + r#"16 foo.example.org. alpn=f\092oo\044bar,h2"# + ); + } + + #[test] + fn test_8_again() { + let result = Ok(ValueList { + values: vec![br#"f\oo,bar"#.to_vec(), b"h2".to_vec()], + }); + let result_bin = Ok(ValueList { + values: vec![ + [0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72].to_vec(), + [0x68, 0x32].to_vec(), + ], + }); + // result_bin is taken directly from the binary part of the test vector + assert_eq!(result, result_bin); + + // I think the test vector's presentation formats include too many backslashes. + // At one point is has three backslashes, preceding a '0'. That can't be right. + assert_eq!(ValueList::parse(r#""f\\\\oo\\,bar,h2""#), result); + assert_eq!(ValueList::parse(r#"f\\\092oo\092,bar,h2"#), result); + } + + // the failure case is not useful, because we don't parse the presentation format. +} + +#[cfg(test)] +mod test_ech { + #[test] + fn ech_param() { + let buf = &[ + 0x00, 0x01, // priority + 0x00, // target + 0x00, 0x05, // ech param + 0x00, 0x24, + ]; + } } diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index 1d0ddcc..e25a6fb 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -12,12 +12,12 @@ use std::io::{Cursor, Read}; /// [Draft RFC](https://tools.ietf.org/id/draft-ietf-dnsop-svcb-https-02.html#name-the-svcb-record-type), section A.1 #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct ValueList { - values: Vec>, + pub values: Vec>, } #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct SingleValue { - value: Vec, + pub value: Vec, } impl ValueList { @@ -155,49 +155,41 @@ pub mod escaping { }) } - mod valuelist { - - fn single_byte(input: &[u8]) -> IResult<&[u8], u8> { - let (byte, remain) = input.split_first().ok_or_else(|| { - nom::Err::Error(ParseError::from_error_kind( - input, - nom::error::ErrorKind::Char, - )) - })?; - Ok((remain, *byte)) - } - - use super::*; - fn chunk(quoted: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], EscChunk<'_>> + Clone { - move |remain| { - non_special(true, false) - .map(EscChunk::Slice) - .or(single_byte.map(EscChunk::Byte)) - .parse(remain) - } - } - pub fn iter_unquoted( - input: &[u8], - ) -> impl Iterator, fmt::Error>> + Clone { - iter_parser(input, chunk(false)) - } + fn single_byte(input: &[u8]) -> IResult<&[u8], u8> { + let (byte, remain) = input.split_first().ok_or_else(|| { + nom::Err::Error(ParseError::from_error_kind( + input, + nom::error::ErrorKind::Char, + )) + })?; + Ok((remain, *byte)) + } - pub fn iter_quoted( - input: &[u8], - ) -> impl Iterator, fmt::Error>> + Clone { - iter_parser(input, chunk(true)) + fn chunk( + split_comma: bool, + whitespace: bool, + ) -> impl FnMut(&[u8]) -> IResult<&[u8], EscChunk<'_>> + Clone { + move |remain| { + non_special(split_comma, whitespace) + .map(EscChunk::Slice) + .or(single_byte.map(EscChunk::Byte)) + .parse(remain) } } - // fn format_iter<'a, 'f>(f: &mut fmt::Formatter<'f>, mut iter: impl Iterator, fmt::Error>>) -> fmt::Result { - // iter.try_for_each() - // } + pub fn emit_chunks( + input: &[u8], + split_comma: bool, + whitespace: bool, + ) -> impl Iterator, fmt::Error>> + Clone { + iter_parser(input, chunk(split_comma, whitespace)) + } fn format_iter<'a, I>(iter: I) -> impl fmt::Display + 'a where I: Iterator, fmt::Error>> + Clone + 'a, { - display_utils::join_format(iter, ",", |chunk, f| match chunk? { + display_utils::join_format(iter, "", |chunk, f| match chunk? { EscChunk::Slice(slice) => { // Technically we know this is printable ASCII. is that utf8? let string = std::str::from_utf8(slice).map_err(|e| { @@ -208,12 +200,13 @@ pub mod escaping { Ok(()) } EscChunk::Byte(byte) => { - write!(f, "\\{:3o}", byte) + write!(f, "\\{:03}", byte) } }) } pub fn format_values_iter<'a>( + split_comma: bool, iter: impl Iterator + Clone, f: &mut fmt::Formatter<'_>, ) -> fmt::Result { @@ -222,23 +215,30 @@ pub mod escaping { .clone() .any(|val| val.contains(&b' ') || val.contains(&b'\t')); if should_quote { - let iter_fmt = display_utils::join_format(iter, ",", |value, f| { - let iter = valuelist::iter_quoted(value); + let joiner = if split_comma { "," } else { "" }; + let iter_fmt = display_utils::join_format(iter, joiner, |value, f| { + let iter = emit_chunks(value, split_comma, true); format_iter(iter).fmt(f) }); write!(f, "\"{}\"", iter_fmt)?; } else { display_utils::join_format(iter, ",", |value, f| { - let iter = valuelist::iter_unquoted(value); + let iter = emit_chunks(value, split_comma, false); format_iter(iter).fmt(f) - }).fmt(f)?; + }) + .fmt(f)?; } Ok(()) } impl fmt::Display for ValueList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - format_values_iter(self.values.iter().map(|val| val.as_slice()), f) + format_values_iter(true, self.values.iter().map(|val| val.as_slice()), f) + } + } + impl fmt::Display for SingleValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + format_values_iter(false, core::iter::once(self.value.as_slice()), f) } } } From 9aa903600fd0e211f1bb2131d96b2f0f65ded8ac Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 04:22:44 +1000 Subject: [PATCH 09/26] Rewrite string encoding --- dns/src/record/svcb_https.rs | 13 +- dns/src/value_list.rs | 426 ++++++++++++++++++++--------------- 2 files changed, 248 insertions(+), 191 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 68452d1..b3948ed 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -129,7 +129,7 @@ pub struct Opaque(/* u16 len */ Vec); impl fmt::Display for Opaque { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - escaping::format_values_iter(false, core::iter::once(&self.0[..]), f) + escaping::escape_char_string(&self.0[..], f) } } @@ -290,7 +290,7 @@ impl fmt::Display for SvcParams { f.write_str(" ")?; } f.write_str("alpn=")?; - escaping::format_values_iter(true, alpn.alpn_ids.iter().map(|id| id.0.as_slice()), f)?; + escaping::encode_value_list(alpn.alpn_ids.iter().map(|id| id.0.as_slice()), f)?; if alpn.no_default_alpn { write!(f, " no-default-alpn")?; } @@ -1237,7 +1237,7 @@ mod test_vectors { 0x6f, 0x6d, 0x00, // target 0x02, 0x9b, // key 667 0x00, 0x09, // length 9 - 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2, 0x71, 0x6f, 0x6f, // value + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2 /* \210 */, 0x71, 0x6f, 0x6f, // value ]; let value = SVCB { priority: 1, @@ -1261,7 +1261,7 @@ mod test_vectors { // we avoid writing the quotes on values where possible, so this differs from the test // vector (which is for a parser, not a formatter?) - assert_eq!(value.to_string(), "1 foo.example.com. key667=hello\\210qoo"); + assert_eq!(value.to_string(), r#"1 foo.example.com. key667=hello\210qoo"#); } #[test] @@ -1408,7 +1408,7 @@ mod test_vectors { assert_eq!( value.to_string(), - r#"16 foo.example.org. alpn=f\092oo\044bar,h2"# + r#"16 foo.example.org. alpn=f\\oo,bar,h2"# ); } @@ -1425,9 +1425,6 @@ mod test_vectors { }); // result_bin is taken directly from the binary part of the test vector assert_eq!(result, result_bin); - - // I think the test vector's presentation formats include too many backslashes. - // At one point is has three backslashes, preceding a '0'. That can't be right. assert_eq!(ValueList::parse(r#""f\\\\oo\\,bar,h2""#), result); assert_eq!(ValueList::parse(r#"f\\\092oo\092,bar,h2"#), result); } diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index e25a6fb..5770d2b 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -1,7 +1,8 @@ #![allow(dead_code)] use core::fmt::{self, Display}; -use std::io::{Cursor, Read}; +use std::borrow::Cow; +use std::iter::FromIterator; /// Parameters to a SVCB/HTTPS record can be multi-valued. /// This is a fancy comma-separated list, where escaped commas \, and \044 do not separate @@ -15,11 +16,37 @@ pub struct ValueList { pub values: Vec>, } +impl>> FromIterator for ValueList { + fn from_iter>(iter: I) -> Self { + let values = iter.into_iter().map(|x| x.into()).collect(); + Self { values } + } +} +impl>> From> for ValueList { + fn from(vec: Vec) -> Self { + let values = vec.into_iter().map(|x| x.into()).collect(); + Self { values } + } +} + #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct SingleValue { pub value: Vec, } +fn wrap_iresult_complete(result: IResult<&[u8], T>) -> Result { + result + .finish() + .map_err(|e| String::from_utf8_lossy(e.input).into_owned()) + .and_then(|(remain, t)| { + if remain.is_empty() { + Ok(t) + } else { + Err(String::from_utf8_lossy(remain).into_owned()) + } + }) +} + impl ValueList { /// New pub fn new() -> Self { @@ -28,73 +55,43 @@ impl ValueList { /// Parses a comma separated list with escaping as defined in Appendix A.1. This variant has /// unlimited size for each value. - pub fn parse(input: &str) -> Result { - value_list_decoding::parse(input.as_bytes()) - .finish() - .map_err(|e| std::str::from_utf8(e.input).unwrap()) - .and_then(|(remain, values)| { - if remain.is_empty() { - Ok(ValueList { values }) - } else { - Err(std::str::from_utf8(remain).unwrap()) - } - }) - } - - pub fn read_value_max( - stated_length: u16, - cursor: &mut Cursor<&[u8]>, - single_value_max: usize, - ) -> Result { - // These methods would be better with Cursor::remaining_slice if it were stable - let mut buf = vec![0u8; usize::from(stated_length)]; - cursor.read_exact(&mut buf)?; - let values = value_list_decoding::parse(&buf).no_remaining()?; - if values.iter().any(|val| val.len() > single_value_max) { - return Err(WireError::IO); - } else { - Ok(Self { values }) - } + pub fn parse(input: impl AsRef<[u8]>) -> Result { + Self::parse_inner(input.as_ref()) } - pub fn read_unlimited( - stated_length: u16, - cursor: &mut Cursor<&[u8]>, - ) -> Result { - // These methods would be better with Cursor::remaining_slice if it were stable - let mut buf = vec![0u8; usize::from(stated_length)]; - cursor.read_exact(&mut buf)?; - let values = value_list_decoding::parse(&buf).no_remaining()?; - Ok(Self { values }) + fn parse_inner(input: &[u8]) -> Result { + let cow = wrap_iresult_complete(char_string_decoding::parse(input.as_ref()))?; + let val = wrap_iresult_complete(value_list_decoding::parse(&cow))?; + Ok(ValueList { values: val }) } } impl SingleValue { - pub fn read(stated_length: u16, cursor: &mut Cursor<&[u8]>) -> Result { - // These methods would be better with Cursor::remaining_slice if it were stable - let mut buf = vec![0u8; usize::from(stated_length)]; - cursor.read_exact(&mut buf)?; - let value = char_string::parse(&buf).no_remaining()?; - Ok(Self { value }) + pub fn parse(input: impl AsRef<[u8]>) -> Result { + Self::parse_inner(input.as_ref()) + } + fn parse_inner(input: &[u8]) -> Result { + let value = wrap_iresult_complete(char_string_decoding::parse(&input))?; + Ok(Self { + value: value.into_owned(), + }) } } -trait NoRemaining: Finish { - fn no_remaining(self) -> Result; -} - -impl NoRemaining for T -where - T: Finish, -{ - fn no_remaining(self) -> Result { - let (i, o) = self.finish().map_err(|_| WireError::IO)?; - if i.input_len() == 0 { - Ok(o) - } else { - Err(WireError::IO) - } - } +#[test] +fn rfc_example() { + let one = br#""part1,part2,part3\\,part4\\\\""#; + let two = br#"part1\,\p\a\r\t2\044part3\092,part4\092\\"#; + let expected_string = br"part1,part2,part3\,part4\\".to_vec(); + let expected_values = vec![ + br"part1".to_vec(), + br"part2".to_vec(), + br"part3,part4\".to_vec(), + ]; + assert_eq!(SingleValue::parse(one).unwrap().value, expected_string); + assert_eq!(SingleValue::parse(two).unwrap().value, expected_string); + assert_eq!(ValueList::parse(one).unwrap().values, expected_values); + assert_eq!(ValueList::parse(two).unwrap().values, expected_values); } use nom::branch::alt; @@ -103,9 +100,7 @@ use nom::combinator::recognize; use nom::error::ParseError; use nom::sequence::preceded; use nom::{Finish, Parser}; -use nom::{IResult, InputLength}; - -use crate::WireError; +use nom::IResult; #[cfg(test)] fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { @@ -122,20 +117,6 @@ fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { }) } -fn is_non_special(split_comma: bool, whitespace: bool) -> impl Fn(u8) -> bool { - move |c: u8| match c { - b',' if split_comma => false, - // non-special. VCHAR minus DQUOTE, ";", "(", ")" and "\" - 0x21 | 0x23..=0x27 | 0x2A..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E => true, - b' ' | b'\t' if whitespace => true, - _ => false, - } -} - -fn non_special(split_comma: bool, whitespace: bool) -> impl FnMut(&[u8]) -> IResult<&[u8], &[u8]> { - move |input| recognize(take_while1(is_non_special(split_comma, whitespace)))(input) -} - pub mod escaping { use super::*; @@ -165,32 +146,53 @@ pub mod escaping { Ok((remain, *byte)) } - fn chunk( - split_comma: bool, - whitespace: bool, - ) -> impl FnMut(&[u8]) -> IResult<&[u8], EscChunk<'_>> + Clone { - move |remain| { - non_special(split_comma, whitespace) - .map(EscChunk::Slice) - .or(single_byte.map(EscChunk::Byte)) + #[derive(Debug, Clone)] + pub enum EncodingChunk<'a> { + Slice(&'a [u8]), + Escape(&'a str), + Byte(u8), + } + + mod char_string { + use super::*; + fn chunk(remain: &[u8]) -> IResult<&[u8], EncodingChunk<'_>> { + super::super::char_string_decoding::non_special + .map(EncodingChunk::Slice) + .or(tag(br"\").map(|_| EncodingChunk::Escape(r"\\"))) + .or(tag(br",").map(|_| EncodingChunk::Escape(r"\,"))) + .or(single_byte.map(EncodingChunk::Byte)) .parse(remain) } + + pub fn emit_chunks( + input: &[u8], + ) -> impl Iterator, fmt::Error>> + Clone { + iter_parser(input, chunk) + } } - pub fn emit_chunks( - input: &[u8], - split_comma: bool, - whitespace: bool, - ) -> impl Iterator, fmt::Error>> + Clone { - iter_parser(input, chunk(split_comma, whitespace)) + mod value_list { + use super::*; + fn chunk(remain: &[u8]) -> IResult<&[u8], EncodingChunk<'_>> { + super::super::value_list_decoding::item_allowed + .map(EncodingChunk::Slice) + .or(single_byte.map(EncodingChunk::Byte)) + .parse(remain) + } + + pub fn emit_chunks( + input: &[u8], + ) -> impl Iterator, fmt::Error>> + Clone { + iter_parser(input, chunk) + } } fn format_iter<'a, I>(iter: I) -> impl fmt::Display + 'a where - I: Iterator, fmt::Error>> + Clone + 'a, + I: Iterator, fmt::Error>> + Clone + 'a, { display_utils::join_format(iter, "", |chunk, f| match chunk? { - EscChunk::Slice(slice) => { + EncodingChunk::Slice(slice) => { // Technically we know this is printable ASCII. is that utf8? let string = std::str::from_utf8(slice).map_err(|e| { log::error!("error escaping string: {}", e); @@ -199,64 +201,68 @@ pub mod escaping { f.write_str(string)?; Ok(()) } - EscChunk::Byte(byte) => { + EncodingChunk::Escape(str) => { + str.fmt(f) + } + EncodingChunk::Byte(byte) => { write!(f, "\\{:03}", byte) } }) } - pub fn format_values_iter<'a>( - split_comma: bool, - iter: impl Iterator + Clone, + pub fn escape_char_string( + string: &[u8], f: &mut fmt::Formatter<'_>, ) -> fmt::Result { - // technically this could pull up a false positive, but that's fine, quotes are always valid - let should_quote = iter - .clone() - .any(|val| val.contains(&b' ') || val.contains(&b'\t')); - if should_quote { - let joiner = if split_comma { "," } else { "" }; - let iter_fmt = display_utils::join_format(iter, joiner, |value, f| { - let iter = emit_chunks(value, split_comma, true); - format_iter(iter).fmt(f) - }); - write!(f, "\"{}\"", iter_fmt)?; + let chunks = char_string::emit_chunks(string); + let iter = format_iter(chunks); + if string.contains(&b' ') { + write!(f, "\"{}\"", iter) } else { - display_utils::join_format(iter, ",", |value, f| { - let iter = emit_chunks(value, split_comma, false); - format_iter(iter).fmt(f) - }) - .fmt(f)?; + iter.fmt(f) } - Ok(()) } - impl fmt::Display for ValueList { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - format_values_iter(true, self.values.iter().map(|val| val.as_slice()), f) + fn escape_values_join<'a>( + values: impl Iterator + Clone + 'a, + ) -> Result, fmt::Error> { + // for each value, encode `\` as `\\` and `,` as `\,`, and join all together with `,` + let mut vec = Vec::new(); + for value in values { + let chunks = value_list::emit_chunks(value); + for encoding_chunk in chunks { + match encoding_chunk? { + EncodingChunk::Slice(slice) => vec.extend_from_slice(slice), + EncodingChunk::Escape(str) => vec.extend_from_slice(str.as_bytes()), + // doesn't happen + EncodingChunk::Byte(byte) => vec.push(byte), + } + } + vec.push(b','); } + vec.pop(); + Ok(vec) } - impl fmt::Display for SingleValue { + + pub fn encode_value_list<'a>( + iter: impl Iterator + Clone + 'a, + f: &mut fmt::Formatter<'_>, + ) -> fmt::Result { + let joined = escape_values_join(iter)?; + escape_char_string(&joined, f) + } + + impl fmt::Display for ValueList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - format_values_iter(false, core::iter::once(self.value.as_slice()), f) + encode_value_list(self.values.iter().map(|val| val.as_slice()), f) } } -} -fn char_escape(input: &[u8]) -> IResult<&[u8], u8> { - let (input, _backslash) = nom::bytes::complete::tag(b"\\")(input)?; - match input { - // dec-octet, a number 0..=255 as a three digit decimal number - &[c @ b'0'..=b'2', ref rest @ ..] => { - let (remain, byte) = dec_octet(rest, c - b'0')?; - Ok((remain, byte)) + impl fmt::Display for SingleValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = self.value.as_slice(); + escape_char_string(bytes, f) } - // non-digit is VCHAR minus DIGIT - &[c @ (0x21..=0x2F | 0x3A..=0x7E), ref remain @ ..] => Ok((remain, c)), - _ => Err(nom::Err::Error(nom::error::Error::from_error_kind( - input, - nom::error::ErrorKind::Escaped, - ))), } } @@ -297,18 +303,18 @@ fn dec_octet(buf: &[u8], zero_one_or_two: u8) -> IResult<&[u8], u8> { } #[derive(Debug, Clone)] -pub enum EscChunk<'a> { +pub enum DecodingChunk<'a> { Slice(&'a [u8]), Byte(u8), } -fn chunk_1<'a, F, E>(mut inner: F) -> impl Parser<&'a [u8], Vec, E> +fn chunk_1<'a, F, E>(mut inner: F) -> impl Parser<&'a [u8], Cow<'a, [u8]>, E> where - F: Parser<&'a [u8], EscChunk<'a>, E>, + F: Parser<&'a [u8], DecodingChunk<'a>, E>, E: ParseError<&'a [u8]> + core::fmt::Debug, { move |input| { - let mut output = Vec::new(); + let mut output = Cow::Borrowed(&[][..]); let parser = |slice| inner.parse(slice); let mut iter = nom::combinator::iterator(input, parser); let i = &mut iter; @@ -316,8 +322,14 @@ where for chunk in i { success = true; match chunk { - EscChunk::Slice(slice) => output.extend_from_slice(slice), - EscChunk::Byte(byte) => output.push(byte), + DecodingChunk::Slice(new_slice) => match output { + Cow::Borrowed(ref mut slice) => { + *slice = &input[..(slice.len() + new_slice.len())] + } + Cow::Owned(ref mut vec) => vec.extend_from_slice(new_slice), + }, + // if we have any escapes at all, take ownership of the vec + DecodingChunk::Byte(byte) => output.to_mut().push(byte), } } let (remain, ()) = iter.finish()?; @@ -332,23 +344,53 @@ where } } -mod char_string { +mod char_string_decoding { use super::*; - fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { + + fn char_escape(input: &[u8]) -> IResult<&[u8], u8> { + let (input, _backslash) = nom::bytes::complete::tag(br"\")(input)?; + match input { + // dec-octet, a number 0..=255 as a three digit decimal number + &[c @ b'0'..=b'2', ref rest @ ..] => { + let (remain, byte) = dec_octet(rest, c - b'0')?; + Ok((remain, byte)) + } + // non-digit is VCHAR minus DIGIT + &[c @ (0x21..=0x2F | 0x3A..=0x7E), ref remain @ ..] => Ok((remain, c)), + _ => Err(nom::Err::Error(nom::error::Error::from_error_kind( + input, + nom::error::ErrorKind::Escaped, + ))), + } + } + + fn is_non_special(c: u8) -> bool { + match c { + // non-special. VCHAR minus DQUOTE, ";", "(", ")" and "\" + 0x21 | 0x23..=0x27 | 0x2A..=0x3A | 0x3C..=0x5B | 0x5D..=0x7E => true, + _ => false, + } + } + + pub(super) fn non_special(input: &[u8]) -> IResult<&[u8], &[u8]> { + recognize(take_while1(is_non_special))(input) + } + + fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], DecodingChunk<'_>> { alt(( - non_special(false, false).map(EscChunk::Slice), - char_escape.map(EscChunk::Byte), + non_special.map(DecodingChunk::Slice), + char_escape.map(DecodingChunk::Byte), ))(input) } - fn contiguous(input: &[u8]) -> IResult<&[u8], Vec> { + fn contiguous(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> { chunk_1(contiguous_chunk).parse(input) } - fn quoted(input: &[u8]) -> IResult<&[u8], Vec> { + fn quoted(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> { let wsc = take_while1(|b| b == b' ' || b == b'\t'); let wsc2 = take_while1(|b| b == b' ' || b == b'\t'); - let wsc_chunk = alt((wsc, preceded(tag(b"\\"), wsc2))).map(EscChunk::Slice); + let wsc_chunk = alt((wsc, preceded(tag(br"\"), wsc2))).map(DecodingChunk::Slice); let parser = chunk_1(alt((contiguous_chunk, wsc_chunk))); let mut parser = nom::sequence::delimited(tag(b"\""), parser, tag(b"\"")); parser.parse(input) @@ -357,7 +399,7 @@ mod char_string { /// A parser as defined by Appendix A of the draft, which describes RFC 1035 § 5.1 /// /// Note: Appendix A says it's not limited to 255 characters - pub fn parse(input: &[u8]) -> IResult<&[u8], Vec> { + pub fn parse(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> { quoted.or(contiguous).parse(input) } @@ -367,15 +409,15 @@ mod char_string { char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))) }; assert_eq!(mini(b"\\044"), Ok(("", ','))); - let parse = |slice| strings(contiguous(slice)); + let parse = |slice| strings(parse(slice).map(|(a, b)| (a, Cow::into_owned(b)))); assert!(parse(b"").is_err()); assert_eq!(parse(b"hello"), Ok(("", "hello".to_owned()))); assert_eq!(parse(b"hello\\044"), Ok(("", "hello,".to_owned()))); - assert_eq!(parse(b"hello\\\\"), Ok(("", "hello\\".to_owned()))); - assert_eq!(parse(b"hello\\\\\\\\"), Ok(("", "hello\\\\".to_owned()))); - assert_eq!(parse(b"hello\\*"), Ok(("", "hello*".to_owned()))); - assert_eq!(parse(b"\\,hello\\*"), Ok(("", ",hello*".to_owned()))); - assert_eq!(parse(b"\\,hello\\*("), Ok(("(", ",hello*".to_owned()))); + assert_eq!(parse(br"hello\\"), Ok(("", "hello\\".to_owned()))); + assert_eq!(parse(br"hello\\\\"), Ok(("", "hello\\\\".to_owned()))); + assert_eq!(parse(br"hello\*"), Ok(("", "hello*".to_owned()))); + assert_eq!(parse(br"\,hello\*"), Ok(("", ",hello*".to_owned()))); + assert_eq!(parse(br"\,hello\*("), Ok(("(", ",hello*".to_owned()))); assert_eq!(parse(b"*;"), Ok((";", "*".to_owned()))); assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); assert_eq!(parse(b"*\""), Ok(("\"", "*".to_owned()))); @@ -385,37 +427,53 @@ mod char_string { mod value_list_decoding { use super::*; - fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], EscChunk<'_>> { + fn is_item_allowed(c: u8) -> bool { + match c { + // item-allowed is OCTET minus "," and "\". + b',' => false, + b'\\' => false, + _ => true, + } + } + + pub fn item_allowed(input: &[u8]) -> IResult<&[u8], &[u8]> { + recognize(take_while1(is_item_allowed))(input) + } + + fn contiguous_chunk(input: &[u8]) -> IResult<&[u8], DecodingChunk<'_>> { alt(( - non_special(true, false).map(EscChunk::Slice), - char_escape.map(EscChunk::Byte), + item_allowed.map(DecodingChunk::Slice), + tag(r"\,").map(|_| DecodingChunk::Byte(b',')), + tag(r"\\").map(|_| DecodingChunk::Byte(b'\\')), ))(input) } - fn value_within_contiguous(input: &[u8]) -> IResult<&[u8], Vec> { + fn value_within_contiguous(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> { chunk_1(contiguous_chunk).parse(input) } fn values_contiguous(input: &[u8]) -> IResult<&[u8], Vec>> { - nom::multi::separated_list1(tag(b","), value_within_contiguous).parse(input) + nom::multi::separated_list1(tag(b","), value_within_contiguous.map(|x| x.to_vec())) + .parse(input) } - fn value_within_quotes(input: &[u8]) -> IResult<&[u8], Vec> { + fn value_within_quotes(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> { let wsc = take_while1(|b| b == b' ' || b == b'\t'); let wsc2 = take_while1(|b| b == b' ' || b == b'\t'); - let wsc_chunk = alt((wsc, preceded(tag(b"\\"), wsc2))).map(EscChunk::Slice); + let wsc_chunk = alt((wsc, preceded(tag(b"\\"), wsc2))).map(DecodingChunk::Slice); let mut parser = chunk_1(alt((contiguous_chunk, wsc_chunk))); parser.parse(input) } fn values_quoted(input: &[u8]) -> IResult<&[u8], Vec>> { - let values = nom::multi::separated_list1(tag(b","), value_within_quotes); + let values = + nom::multi::separated_list1(tag(b","), value_within_quotes.map(|x| x.to_vec())); let mut parser = nom::sequence::delimited(tag(b"\""), values, tag(b"\"")); parser.parse(input) } - pub fn parse(input: &[u8]) -> IResult<&[u8], Vec>> { - values_quoted.or(values_contiguous).parse(input) + pub fn parse(char_decoded: &[u8]) -> IResult<&[u8], Vec>> { + values_quoted.or(values_contiguous).parse(char_decoded) } #[cfg(test)] @@ -442,35 +500,37 @@ mod value_list_decoding { #[test] fn test_escaping() { - let mini = |slice| { - char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))) - }; - assert_eq!(mini(b"\\044"), Ok(("", ','))); - - let parse = |slice| strings(values_contiguous(slice)); + let parse = |slice: &[u8]| ValueList::parse(slice); assert!(parse(b"").is_err()); - assert_eq!(parse(b"hello"), Ok(("", vec!["hello".to_owned()]))); - assert_eq!(parse(b"hello\\044"), Ok(("", vec!["hello,".to_owned()]))); assert_eq!( - parse(b"hello\\\\\\044"), - Ok(("", vec!["hello\\,".to_owned()])) + parse(br"hello"), + Ok([b"hello"].iter().map(|x| x.to_vec()).collect()) + ); + assert_eq!( + parse(br"hello\044hello"), + Ok(vec![br"hello".to_vec(), br"hello".to_vec()].into()) + ); + assert_eq!( + parse(br"hello\\\044hello"), + Ok(vec![br"hello,hello".to_vec()].into()) ); assert_eq!( - parse(b"hello,\\\\\\044"), - Ok(("", vec!["hello".to_owned(), "\\,".to_owned()])) + parse(br"hello\\\\044"), + Ok(vec![br"hello\044".to_vec()].into()) ); assert_eq!( - parse(b"hello\\\\\\\\,"), - Ok((",", vec!["hello\\\\".to_owned()])) + parse(br"hello,\\\044"), + Ok(vec![br"hello".to_vec(), br",".to_vec()].into()) ); - assert_eq!(parse(b"hello\\*"), Ok(("", vec!["hello*".to_owned()]))); - assert_eq!(parse(b"\\,hello\\*"), Ok(("", vec![",hello*".to_owned()]))); + assert_eq!(parse(br"hello\\\\,"), Err(",".into()),); + assert_eq!(parse(br"hello\*"), Ok(vec![b"hello*".to_vec()].into())); assert_eq!( - parse(b"\\,hello\\*("), - Ok(("(", vec![",hello*".to_owned()])) + parse(br"hi\,hello\*"), + Ok(vec![b"hi".to_vec(), b"hello*".to_vec()].into()) ); - assert_eq!(parse(b"*;"), Ok((";", vec!["*".to_owned()]))); - assert_eq!(parse(b"*\""), Ok(("\"", vec!["*".to_owned()]))); - assert_eq!(parse(b"*\""), Ok(("\"", vec!["*".to_owned()]))); + assert_eq!(parse(b"\\,hello\\*("), Err("(".into())); + assert_eq!(parse(b"*;"), Err(";".into())); + assert_eq!(parse(b"*\""), Err("\"".into())); + assert_eq!(parse(b"*\""), Err("\"".into())); } } From 7595462e10e8ea7cd8b581b10a6d48d36a842298 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 06:12:14 +1000 Subject: [PATCH 10/26] Parse ECH from crypto.cloudflare.com --- dns/src/record/svcb_https.rs | 239 +++++++++++++++++++++++------------ 1 file changed, 156 insertions(+), 83 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index b3948ed..2d83471 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -127,6 +127,12 @@ macro_rules! u16_enum { #[derive(Debug, Clone, PartialEq)] pub struct Opaque(/* u16 len */ Vec); +impl From> for Opaque { + fn from(vec: Vec) -> Self { + Self(vec) + } +} + impl fmt::Display for Opaque { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { escaping::escape_char_string(&self.0[..], f) @@ -176,6 +182,13 @@ macro_rules! opaque { $crate::record::svcb_https::Opaque::read_from(cursor).map(Self) } } + + impl From> for $ident { + fn from(vec: Vec) -> Self { + Self(From::from(vec)) + } + } + } } @@ -303,18 +316,18 @@ impl fmt::Display for SvcParams { write!(f, "port={}", port)?; after_first = true; } - if let Some(ech) = ech { + if !ipv4hint.is_empty() { if after_first { f.write_str(" ")?; } - write!(f, "ech={}", ech.base64)?; + write!(f, "ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; after_first = true; } - if !ipv4hint.is_empty() { + if let Some(ech) = ech { if after_first { f.write_str(" ")?; } - write!(f, "ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; + write!(f, "ech={}", ech.base64)?; after_first = true; } if !ipv6hint.is_empty() { @@ -500,7 +513,7 @@ mod ech { use super::{CursorExt, Opaque, Opaque1, ReadFromCursor}; - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone)] pub struct ECHConfigList { configs: Vec, @@ -508,15 +521,31 @@ mod ech { pub base64: String, } + impl PartialEq for ECHConfigList { + fn eq(&self, other: &Self) -> bool { + self.configs == other.configs + } + } + + impl From> for ECHConfigList { + fn from(configs: Vec) -> Self { + Self { + configs, + base64: String::from("base64 not computed"), + } + } + } + impl ReadFromCursor for ECHConfigList { fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { let mut configs = Vec::new(); let configs_length = cursor.read_u16::()?; + log::trace!("ECHConfigList length = {}", configs_length); let buf = &cursor.std_remaining_slice()[..configs_length.into()]; let base64 = base64::encode(buf); - for _ in 0..configs_length { + while cursor.std_remaining_slice().len() > 0 { let config = ECHConfig::read_from(cursor)?; configs.push(config); } @@ -527,18 +556,31 @@ mod ech { impl ReadFromCursor for ECHConfig { fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { let version = cursor.read_u16::()?; + log::trace!("ECHConfig version = 0x{:04x}", version); let length = cursor.read_u16::()?; + log::trace!("ECHConfig length = {}", length); match version { 0xfe0d => cursor.with_truncated(u64::from(length), |cursor, _len_hint| { let key_config = tls13::HpkeKeyConfig::read_from(cursor)?; + log::trace!("key_config = {:?}", key_config); let maximum_name_length = cursor.read_u8()?; + log::trace!("maximum_name_length = {}", maximum_name_length); let public_name = PublicName::read_from(cursor)?; let mut extensions = Vec::new(); - while let ext = tls13::Extension::read_from(cursor)? { - extensions.push(ext); - } + let extensions_len = cursor.read_u16::()?; + log::trace!("extensions: len = {}", extensions_len); + cursor.with_truncated( + extensions_len as u64, + |cursor, _| -> io::Result<()> { + while cursor.std_remaining_slice().len() > 0 { + let ext = tls13::Extension::read_from(cursor)?; + extensions.push(ext); + } + Ok(()) + }, + )?; Ok(Self::EchConfigContents { key_config, @@ -557,13 +599,11 @@ mod ech { } #[derive(Clone, PartialEq)] - pub struct PublicName { - inner: Vec, - } + pub struct PublicName(pub Vec); impl fmt::Debug for PublicName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let bytes = &self.inner[..]; + let bytes = &self.0[..]; f.write_str(&String::from_utf8_lossy(bytes)) } } @@ -571,7 +611,7 @@ mod ech { impl ReadFromCursor for PublicName { fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { let len = cursor.read_u8()?; - log::trace!("read ECHConfig.public_name length = {}", len); + log::trace!("PublicName length = {}", len); if len == 0 || len > 254 { return Err(io::Error::new( io::ErrorKind::Other, @@ -579,8 +619,9 @@ mod ech { )); } let mut vec = vec![0u8; usize::from(len)]; - cursor.read_exact(&mut vec[..])?; - Ok(Self { inner: vec }) + cursor.read_exact(&mut vec)?; + log::trace!("PublicName = {:?}", std::str::from_utf8(&vec)); + Ok(Self(vec)) } } @@ -648,14 +689,12 @@ mod ech { outer: Vec, } - mod tls13 { + pub mod tls13 { + use crate::record::svcb_https::{CursorExt, Opaque, ReadFromCursor}; use byteorder::{BigEndian, ReadBytesExt}; + use std::io::{self, Read}; - use crate::record::svcb_https::ReadFromCursor; - - // const MANDATORY: u16 = 0x1 << 15; - - // We will implement the mandatory-to-implement extensions only from RFC8446 + // mandatory-to-implement extensions from RFC8446 // // - Supported Versions ("supported_versions"; Section 4.2.1) // - Cookie ("cookie"; Section 4.2.2) @@ -698,14 +737,34 @@ mod ech { impl ReadFromCursor for Extension { fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { let ty: ExtensionType = cursor.read_u16::()?.into(); - match ty { - // ExtensionType::ServerName => Extension::ServerName(ServerName::read_) - _ => Ok(Extension::Other(ty, UnknownExtension::read_from(cursor)?)), - } + log::trace!("TLS extension: {:?}", ty); + let remain = cursor.std_remaining_slice(); + log::trace!("TLS remaining: {:?}", remain); + let len = cursor.read_u16::()?; + log::trace!("TLS extension length: {:?}", len); + cursor.with_truncated(len as u64, |cursor, len_hint| { + log::trace!("TLS extension length hint: {:?}", len_hint); + match ty { + // ExtensionType::ServerName => Extension::ServerName(ServerName::read_) + _ => Ok(Extension::Other( + ty, + UnknownExtension::read_len(cursor, len_hint as u64)?, + )), + } + }) } } - opaque!(pub struct UnknownExtension); + #[derive(Debug, Clone, PartialEq)] + pub struct UnknownExtension(Opaque); + + impl UnknownExtension { + fn read_len(cursor: &mut io::Cursor<&[u8]>, len: u64) -> io::Result { + let mut vec = vec![0u8; len as usize]; + cursor.read_exact(&mut vec)?; + Ok(Self(Opaque::from(vec))) + } + } #[derive(Debug, Clone, PartialEq)] pub enum ServerName { @@ -722,7 +781,7 @@ mod ech { /// Draft RFC /// 7.1. Key Encapsulation Mechanisms (KEMs) #[allow(non_camel_case_types)] - enum HpkeKemId { + pub enum HpkeKemId { Reserved = 0x0000, DHKEM_P256_HKDF_SHA256 = 0x0010, DHKEM_P384_HKDF_SHA384 = 0x0011, @@ -737,7 +796,7 @@ mod ech { /// Draft RFC /// 7.2. Key Derivation Functions (KDFs) #[allow(non_camel_case_types)] - enum HpkeKdfId { + pub enum HpkeKdfId { Reserved = 0, HKDF_SHA256 = 1, HKDF_SHA384 = 2, @@ -750,7 +809,7 @@ mod ech { /// Draft RFC /// 7.3. Authenticated Encryption with Associated Data (AEAD) Functions #[allow(non_camel_case_types)] - enum HpkeAeadId { + pub enum HpkeAeadId { Reserved = 0, AES_128_GCM = 1, AES_256_GCM = 2, @@ -762,13 +821,13 @@ mod ech { #[test] fn test_hpke() { - let hpke = HpkeAeadId::from(0x0003); + let _hpke = HpkeAeadId::from(0x0003); } #[derive(Debug, Clone, PartialEq)] pub struct HpkeSymmetricCipherSuite { - kdf_id: HpkeKdfId, - aead_id: HpkeAeadId, + pub kdf_id: HpkeKdfId, + pub aead_id: HpkeAeadId, } impl ReadFromCursor for HpkeSymmetricCipherSuite { @@ -782,21 +841,31 @@ mod ech { #[derive(Debug, Clone, PartialEq)] pub struct HpkeKeyConfig { - config_id: u8, - kem_id: HpkeKemId, - public_key: HpkePublicKey, + pub config_id: u8, + pub kem_id: HpkeKemId, + pub public_key: HpkePublicKey, // u16 len - cipher_suites: Vec, + pub cipher_suites: Vec, } impl ReadFromCursor for HpkeKeyConfig { fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { let config_id = cursor.read_u8()?; + log::trace!("config_id = {:?}", config_id); let kem_id = cursor.read_u16::()?.into(); + log::trace!("kem_id = {:?}", kem_id); let public_key = HpkePublicKey::read_from(cursor)?; + log::trace!("public_key (len) = {:?}", (public_key.0).0.len()); let cs_len = cursor.read_u16::()?; - let n_cipher_suites = - cs_len as usize / core::mem::size_of::(); + log::trace!("cs_len = {:?}", cs_len); + if cs_len < 4 || cs_len as u32 > 2u32 << 16 - 4 { + return Err(io::Error::new( + io::ErrorKind::Other, + "cipher_suites length field invalid", + )); + } + let n_cipher_suites = cs_len as usize / 4; + log::trace!("n_cipher_suites = {:?}", n_cipher_suites); let mut cipher_suites = Vec::with_capacity(n_cipher_suites); for _ in 0..n_cipher_suites { let suite = HpkeSymmetricCipherSuite::read_from(cursor)?; @@ -817,46 +886,9 @@ mod ech { pub struct SupportedVersions { // min length 2 (otherwise implied by TLS version field), max length 254 // len is a u8 i suppose? - versions: Vec, + pub versions: Vec, } - // #[derive(Debug, Clone, PartialEq)] - // pub struct NamedGroupList(/* u16 len */ Vec); - - // u16_enum! { - // pub enum NamedGroup { - // /* Elliptic Curve Groups (ECDHE) */ - // Secp256r1 = 0x0017, - // Secp384r1 = 0x0018, - // Secp521r1 = 0x0019, - // X25519 = 0x001D, - // X448 = 0x001E, - // /* Finite Field Groups (DHE) */ - // Ffdhe2048 = 0x0100, - // Ffdhe3072 = 0x0101, - // Ffdhe4096 = 0x0102, - // Ffdhe6144 = 0x0103, - // Ffdhe8192 = 0x0104, - // /* Reserved Code Points */ - // @unknown - // /// ffdhe_private_use(0x01FC..0x01FF), - // /// ecdhe_private_use(0xFE00..0xFEFF), - // PrivateUse(u16), - // } - // } - - // #[derive(Debug, Clone, PartialEq)] - // struct KeyShareEntry { - // group: NamedGroup, - // key_exchange: Vec, - // } - - // #[derive(Debug, Clone, PartialEq)] - // struct KeyShareClientHello { - // // u16 len - // client_shares: Vec, - // } - u16_enum! { pub enum TlsVersion { Ssl3_0 = 0x300, @@ -922,6 +954,8 @@ impl Wire for SVCB { fn read(stated_length: u16, cursor: &mut Cursor<&[u8]>) -> Result { let initial_pos = cursor.position(); + trace!("{:?}", cursor.std_remaining_slice()); + let ret = cursor.with_truncated( stated_length as _, move |cursor, _| -> Result { @@ -1237,7 +1271,8 @@ mod test_vectors { 0x6f, 0x6d, 0x00, // target 0x02, 0x9b, // key 667 0x00, 0x09, // length 9 - 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2 /* \210 */, 0x71, 0x6f, 0x6f, // value + 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xd2, /* \210 */ + 0x71, 0x6f, 0x6f, // value ]; let value = SVCB { priority: 1, @@ -1261,7 +1296,10 @@ mod test_vectors { // we avoid writing the quotes on values where possible, so this differs from the test // vector (which is for a parser, not a formatter?) - assert_eq!(value.to_string(), r#"1 foo.example.com. key667=hello\210qoo"#); + assert_eq!( + value.to_string(), + r#"1 foo.example.com. key667=hello\210qoo"# + ); } #[test] @@ -1434,13 +1472,48 @@ mod test_vectors { #[cfg(test)] mod test_ech { + use super::ech::{tls13::*, *}; + use super::*; + use pretty_assertions::assert_eq; + #[test] fn ech_param() { + init_logs(); let buf = &[ - 0x00, 0x01, // priority - 0x00, // target - 0x00, 0x05, // ech param - 0x00, 0x24, + 0, 1, // priority: = 1 + 0x00, // target: . + 0, 1, // param: alpn + 0, 3, // param: len = 3 + 2, 104, 50, // "h2" + 0, 4, // param: ipv4hint + 0, 8, // param: len = 8 + 162, 159, 135, 79, // ip 1 + 162, 159, 136, 79, // ip 2 + 0, 5, // param: ech + 0, 72, // param: len = 72 + 0, 70, // echconfiglist: len = 70 + 254, 13, // config version: 0xfe0d + 0, 66, // config len + 63, // config id + 0, 32, 0, 32, // hpke stuff + 40, 38, 25, 12, 212, 168, 183, 42, 218, 32, 41, 154, 44, 61, 152, 136, 131, 114, 86, + 111, 194, 66, 154, 114, 231, 170, 205, 83, 72, 105, 105, 119, // public_key + 0, 4, // cipher suites len + 0, 1, 0, 1, // cipher suites + 0, 19, // public name + 99, 108, 111, 117, 100, 102, 108, 97, 114, 101, 45, 101, 115, 110, 105, 46, 99, 111, + 109, // cloudflare-esni.com + 0, 0, // extensions len + 0, 6, // param: ipv6hints + 0, 32, 38, 6, 71, 0, 0, 7, 0, 0, 0, 0, 0, 0, 162, 159, 135, 79, // ipv6 1 + 38, 6, 71, 0, 0, 7, 0, 0, 0, 0, 0, 0, 162, 159, 136, 79, // ipv6 2 ]; + let parsed = SVCB::read(buf.len() as u16, &mut Cursor::new(buf)); + assert_eq!( + parsed.map(|x| x.to_string()).as_deref(), + Ok( + r#"1 . alpn=h2 ipv4hint=162.159.135.79,162.159.136.79 ech=AEb+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA ipv6hint=2606:4700:7::a29f:874f,2606:4700:7::a29f:884f"# + ) + ); } } From 21c5a27ccb592bc55dd380ee6d22de4bdb653e5f Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 06:13:05 +1000 Subject: [PATCH 11/26] Fix missing length field in of ech base64 encoding --- dns/src/record/svcb_https.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 2d83471..c705478 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -539,12 +539,17 @@ mod ech { impl ReadFromCursor for ECHConfigList { fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { let mut configs = Vec::new(); - let configs_length = cursor.read_u16::()?; - log::trace!("ECHConfigList length = {}", configs_length); - let buf = &cursor.std_remaining_slice()[..configs_length.into()]; + // write a base64 string that _includes the length field_ + let mut buf = cursor.std_remaining_slice(); + let mut throwaway = io::Cursor::new(buf); + let configs_length = throwaway.read_u16::()?; + buf = &buf[..configs_length as usize + 2]; let base64 = base64::encode(buf); + let configs_length = cursor.read_u16::()?; + log::trace!("ECHConfigList length = {}", configs_length); + while cursor.std_remaining_slice().len() > 0 { let config = ECHConfig::read_from(cursor)?; configs.push(config); From d5d73169fc2ddac443a0817d4207deb20ad3f4ad Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 06:13:50 +1000 Subject: [PATCH 12/26] Redo presentation format spaces --- dns/src/record/svcb_https.rs | 50 ++++++++++-------------------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index c705478..2c7e26e 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -289,61 +289,36 @@ impl fmt::Display for SvcParams { ipv6hint, other, } = self; - let mut after_first = false; if !mandatory.is_empty() { write!( f, - "mandatory={}", + " mandatory={}", display_utils::join(mandatory.iter(), ",") )?; - after_first = true; } if let Some(alpn) = alpn { - if after_first { - f.write_str(" ")?; - } - f.write_str("alpn=")?; + f.write_str(" alpn=")?; escaping::encode_value_list(alpn.alpn_ids.iter().map(|id| id.0.as_slice()), f)?; if alpn.no_default_alpn { write!(f, " no-default-alpn")?; } - after_first = true; } if let &Some(port) = port { - if after_first { - f.write_str(" ")?; - } - write!(f, "port={}", port)?; - after_first = true; + write!(f, " port={}", port)?; } if !ipv4hint.is_empty() { - if after_first { - f.write_str(" ")?; - } - write!(f, "ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; - after_first = true; + write!(f, " ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; } if let Some(ech) = ech { - if after_first { - f.write_str(" ")?; - } - write!(f, "ech={}", ech.base64)?; - after_first = true; + write!(f, " ech={}", ech.base64)?; } if !ipv6hint.is_empty() { - if after_first { - f.write_str(" ")?; - } - write!(f, "ipv6hint={}", display_utils::join(ipv6hint.iter(), ","))?; - after_first = true; + write!(f, " ipv6hint={}", display_utils::join(ipv6hint.iter(), ","))?; } if !other.is_empty() { - if after_first { - f.write_str(" ")?; - } - display_utils::join_format(other.iter(), " ", |(k, v), f| write!(f, "{}={}", k, v)) - .fmt(f)?; - // after_first = true; + other + .iter() + .try_for_each(|(k, v)| write!(f, " {}={}", k, v))?; } Ok(()) } @@ -959,7 +934,10 @@ impl Wire for SVCB { fn read(stated_length: u16, cursor: &mut Cursor<&[u8]>) -> Result { let initial_pos = cursor.position(); - trace!("{:?}", cursor.std_remaining_slice()); + trace!( + "{:?}", + &cursor.std_remaining_slice()[..stated_length as usize] + ); let ret = cursor.with_truncated( stated_length as _, @@ -1020,7 +998,7 @@ impl fmt::Display for SVCB { write!(f, "{} {}", priority, target)?; if let Some(params) = parameters { - write!(f, "{}{}", if target.len() > 0 { " " } else { "" }, params)?; + write!(f, "{}", params)?; } Ok(()) } From 6a0240ad82f176b71233a43035b392af292051cb Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 17:01:15 +1000 Subject: [PATCH 13/26] Fix value list encoding again, make Display impls --- dns/src/lib.rs | 2 +- dns/src/record/svcb_https.rs | 21 +++++--- dns/src/value_list.rs | 94 +++++++++++++++++++++++++++--------- src/output.rs | 11 ++++- 4 files changed, 93 insertions(+), 35 deletions(-) diff --git a/dns/src/lib.rs b/dns/src/lib.rs index cb49a85..1cc5b08 100644 --- a/dns/src/lib.rs +++ b/dns/src/lib.rs @@ -39,7 +39,7 @@ mod strings; pub use self::strings::Labels; mod value_list; -pub use self::value_list::ValueList; +pub use self::value_list::escaping; mod wire; pub use self::wire::{Wire, WireError, MandatedLength}; diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 2c7e26e..5892e02 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -135,7 +135,7 @@ impl From> for Opaque { impl fmt::Display for Opaque { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - escaping::escape_char_string(&self.0[..], f) + escaping::EscapeCharString(&self.0).fmt(f) } } @@ -298,7 +298,7 @@ impl fmt::Display for SvcParams { } if let Some(alpn) = alpn { f.write_str(" alpn=")?; - escaping::encode_value_list(alpn.alpn_ids.iter().map(|id| id.0.as_slice()), f)?; + escaping::EscapeValueList(alpn.alpn_ids.iter().map(|id| id.0.as_slice())).fmt(f)?; if alpn.no_default_alpn { write!(f, " no-default-alpn")?; } @@ -1149,7 +1149,7 @@ mod test { /// See the draft RFC #[cfg(test)] mod test_vectors { - use crate::ValueList; + use crate::value_list::ValueList; use super::*; use pretty_assertions::assert_eq; @@ -1416,7 +1416,9 @@ mod test_vectors { target: Labels::encode("foo.example.org.").unwrap(), parameters: Some(SvcParams { alpn: Some(Alpn { - alpn_ids: vec!["f\\oo,bar".into(), "h2".into()], + // here, it's a single \ because there's only one 0x5c and only a single 0x2c + // comma, neither of which need escaping in binary + alpn_ids: vec![r"f\oo,bar".into(), "h2".into()], no_default_alpn: false, }), ..Default::default() @@ -1427,10 +1429,13 @@ mod test_vectors { Ok(&value) ); - assert_eq!( - value.to_string(), - r#"16 foo.example.org. alpn=f\\oo,bar,h2"# - ); + // here, we have two levels of escaping applied, + // - initial: [br"f\oo,bar", br"h2"] + // - value-list encoding => f\\oo\,bar,h2 + // this joins the values with commas, and escapes value-internal backslashes and commas + // - char-string encoding => f\\\\oo\\,bar,h2 + let presentation = r#"16 foo.example.org. alpn=f\\\\oo\\,bar,h2"#; + assert_eq!(value.to_string(), presentation); } #[test] diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index 5770d2b..2934647 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -29,6 +29,7 @@ impl>> From> for ValueList { } } +/// Nice #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct SingleValue { pub value: Vec, @@ -99,8 +100,8 @@ use nom::bytes::complete::{tag, take_while1}; use nom::combinator::recognize; use nom::error::ParseError; use nom::sequence::preceded; -use nom::{Finish, Parser}; use nom::IResult; +use nom::{Finish, Parser}; #[cfg(test)] fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { @@ -147,7 +148,7 @@ pub mod escaping { } #[derive(Debug, Clone)] - pub enum EncodingChunk<'a> { + enum EncodingChunk<'a> { Slice(&'a [u8]), Escape(&'a str), Byte(u8), @@ -158,13 +159,18 @@ pub mod escaping { fn chunk(remain: &[u8]) -> IResult<&[u8], EncodingChunk<'_>> { super::super::char_string_decoding::non_special .map(EncodingChunk::Slice) + // special treatment for these few, not matched by is_non_special .or(tag(br"\").map(|_| EncodingChunk::Escape(r"\\"))) - .or(tag(br",").map(|_| EncodingChunk::Escape(r"\,"))) + .or(tag(b";").map(|_| EncodingChunk::Escape(r"\;"))) + .or(tag(b"\"").map(|_| EncodingChunk::Escape("\\\""))) + .or(tag(b"(").map(|_| EncodingChunk::Escape("\\("))) + .or(tag(b")").map(|_| EncodingChunk::Escape("\\)"))) + // or any other byte .or(single_byte.map(EncodingChunk::Byte)) .parse(remain) } - pub fn emit_chunks( + pub(super) fn emit_chunks( input: &[u8], ) -> impl Iterator, fmt::Error>> + Clone { iter_parser(input, chunk) @@ -175,12 +181,16 @@ pub mod escaping { use super::*; fn chunk(remain: &[u8]) -> IResult<&[u8], EncodingChunk<'_>> { super::super::value_list_decoding::item_allowed + // .or(tag(br"\\")) + // .or(tag(br"\,")) .map(EncodingChunk::Slice) - .or(single_byte.map(EncodingChunk::Byte)) + // .or(single_byte.map(EncodingChunk::Byte)) + .or(tag(br"\").map(|_| EncodingChunk::Escape(r"\\"))) + .or(tag(br",").map(|_| EncodingChunk::Escape(r"\,"))) .parse(remain) } - pub fn emit_chunks( + pub(super) fn emit_chunks( input: &[u8], ) -> impl Iterator, fmt::Error>> + Clone { iter_parser(input, chunk) @@ -201,19 +211,63 @@ pub mod escaping { f.write_str(string)?; Ok(()) } - EncodingChunk::Escape(str) => { - str.fmt(f) - } + EncodingChunk::Escape(str) => str.fmt(f), EncodingChunk::Byte(byte) => { write!(f, "\\{:03}", byte) } }) } - pub fn escape_char_string( - string: &[u8], - f: &mut fmt::Formatter<'_>, - ) -> fmt::Result { + /// Display implementation that escapes a string + pub struct EscapeCharString>(pub A); + + impl> Display for EscapeCharString { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let string = self.0.as_ref(); + let chunks = char_string::emit_chunks(string); + let iter = format_iter(chunks); + if string.contains(&b' ') || string.contains(&b'\t') || string.contains(&b'"') { + write!(f, "\"{}\"", iter) + } else { + iter.fmt(f) + } + } + } + + #[test] + fn test_escape_char_string() { + assert_eq!(EscapeCharString(br"\").to_string(), r"\\"); + } + + pub struct EscapeValueList<'a, I: IntoIterator + Clone>(pub I); + + impl<'a, I> fmt::Display for EscapeValueList<'a, I> + where + I: IntoIterator + Clone, + I::IntoIter: Clone, + { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let values = self.0.clone().into_iter(); + // a temporary buffer to do EscapeCharString on. + let mut buf = Vec::::new(); + for value in values { + let chunks = value_list::emit_chunks(value); + for encoding_chunk in chunks { + match encoding_chunk? { + EncodingChunk::Slice(slice) => buf.extend_from_slice(slice), + EncodingChunk::Escape(str) => buf.extend_from_slice(str.as_bytes()), + // doesn't happen + EncodingChunk::Byte(byte) => buf.push(byte), + } + } + buf.push(b','); + } + buf.pop(); + EscapeCharString(buf).fmt(f) + } + } + + fn escape_char_string(string: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result { let chunks = char_string::emit_chunks(string); let iter = format_iter(chunks); if string.contains(&b' ') { @@ -244,24 +298,16 @@ pub mod escaping { Ok(vec) } - pub fn encode_value_list<'a>( - iter: impl Iterator + Clone + 'a, - f: &mut fmt::Formatter<'_>, - ) -> fmt::Result { - let joined = escape_values_join(iter)?; - escape_char_string(&joined, f) - } - impl fmt::Display for ValueList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - encode_value_list(self.values.iter().map(|val| val.as_slice()), f) + EscapeValueList(self.values.iter().map(|val| val.as_slice())).fmt(f) } } impl fmt::Display for SingleValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bytes = self.value.as_slice(); - escape_char_string(bytes, f) + EscapeCharString(bytes).fmt(f) } } } @@ -404,7 +450,7 @@ mod char_string_decoding { } #[test] - fn test_escaping() { + fn test_char_string_decoding() { let mini = |slice| { char_escape(slice).map(|(a, b)| (std::str::from_utf8(a).unwrap(), char::from(b))) }; diff --git a/src/output.rs b/src/output.rs index abb2714..9c6cb88 100644 --- a/src/output.rs +++ b/src/output.rs @@ -614,7 +614,11 @@ struct Ascii<'a>(&'a [u8]); impl fmt::Display for Ascii<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "\"")?; + let contains_spaces = self.0.contains(&b' ') || self.0.contains(&b'\t') || self.0.contains(&b'"'); + // let contains_spaces = true; + if contains_spaces { + write!(f, "\"")?; + } for byte in self.0.iter().copied() { if byte < 32 || byte >= 128 { @@ -631,7 +635,10 @@ impl fmt::Display for Ascii<'_> { } } - write!(f, "\"") + if contains_spaces { + write!(f, "\"")?; + } + Ok(()) } } From 9f234a796dadd4eba60701c36585c4e85b6d15e1 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 18:04:01 +1000 Subject: [PATCH 14/26] EscapeValueList no longer allocates. + new error type DecodingError --- dns/src/lib.rs | 2 +- dns/src/record/svcb_https.rs | 10 +- dns/src/value_list.rs | 171 +++++++++++++++++------------------ 3 files changed, 89 insertions(+), 94 deletions(-) diff --git a/dns/src/lib.rs b/dns/src/lib.rs index 1cc5b08..f1d9fb4 100644 --- a/dns/src/lib.rs +++ b/dns/src/lib.rs @@ -39,7 +39,7 @@ mod strings; pub use self::strings::Labels; mod value_list; -pub use self::value_list::escaping; +pub use self::value_list::encoding; mod wire; pub use self::wire::{Wire, WireError, MandatedLength}; diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 5892e02..af48932 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -10,7 +10,7 @@ use log::*; use crate::strings::{Labels, ReadLabels}; use crate::wire::*; -use crate::value_list::escaping; +use crate::value_list::encoding; /// A kinda hacky but alright way to avoid copying tons of data trait CursorExt { @@ -135,7 +135,7 @@ impl From> for Opaque { impl fmt::Display for Opaque { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - escaping::EscapeCharString(&self.0).fmt(f) + encoding::EscapeCharString(&self.0).fmt(f) } } @@ -298,7 +298,7 @@ impl fmt::Display for SvcParams { } if let Some(alpn) = alpn { f.write_str(" alpn=")?; - escaping::EscapeValueList(alpn.alpn_ids.iter().map(|id| id.0.as_slice())).fmt(f)?; + encoding::EscapeValueList(alpn.alpn_ids.iter().map(|id| id.0.as_slice())).fmt(f)?; if alpn.no_default_alpn { write!(f, " no-default-alpn")?; } @@ -1451,8 +1451,8 @@ mod test_vectors { }); // result_bin is taken directly from the binary part of the test vector assert_eq!(result, result_bin); - assert_eq!(ValueList::parse(r#""f\\\\oo\\,bar,h2""#), result); - assert_eq!(ValueList::parse(r#"f\\\092oo\092,bar,h2"#), result); + assert_eq!(ValueList::parse(br#""f\\\\oo\\,bar,h2""#), result); + assert_eq!(ValueList::parse(br#"f\\\092oo\092,bar,h2"#), result); } // the failure case is not useful, because we don't parse the presentation format. diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index 2934647..4231f6b 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -35,19 +35,39 @@ pub struct SingleValue { pub value: Vec, } -fn wrap_iresult_complete(result: IResult<&[u8], T>) -> Result { +fn wrap_iresult_complete(result: IResult<&[u8], T>) -> Result { result .finish() - .map_err(|e| String::from_utf8_lossy(e.input).into_owned()) + .map_err(|e| DecodingError::new(e.input)) .and_then(|(remain, t)| { if remain.is_empty() { Ok(t) } else { - Err(String::from_utf8_lossy(remain).into_owned()) + Err(DecodingError::new(remain)) } }) } +#[derive(Debug, Clone, PartialEq)] +pub struct DecodingError { + input: Vec, +} + +impl DecodingError { + fn new(input: &[u8]) -> Self { + Self { + input: input.to_vec(), + } + } +} + +impl std::error::Error for DecodingError {} +impl fmt::Display for DecodingError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "error decoding, occurred at: {:x?}", self.input) + } +} + impl ValueList { /// New pub fn new() -> Self { @@ -56,11 +76,7 @@ impl ValueList { /// Parses a comma separated list with escaping as defined in Appendix A.1. This variant has /// unlimited size for each value. - pub fn parse(input: impl AsRef<[u8]>) -> Result { - Self::parse_inner(input.as_ref()) - } - - fn parse_inner(input: &[u8]) -> Result { + pub fn parse(input: &[u8]) -> Result { let cow = wrap_iresult_complete(char_string_decoding::parse(input.as_ref()))?; let val = wrap_iresult_complete(value_list_decoding::parse(&cow))?; Ok(ValueList { values: val }) @@ -68,10 +84,8 @@ impl ValueList { } impl SingleValue { - pub fn parse(input: impl AsRef<[u8]>) -> Result { - Self::parse_inner(input.as_ref()) - } - fn parse_inner(input: &[u8]) -> Result { + /// Parse with char-string decoding + pub fn parse(input: &[u8]) -> Result { let value = wrap_iresult_complete(char_string_decoding::parse(&input))?; Ok(Self { value: value.into_owned(), @@ -118,25 +132,32 @@ fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { }) } -pub mod escaping { +pub mod encoding { use super::*; + pub use super::DecodingError; + pub use super::SingleValue; + pub use super::ValueList; + + /// Iterates a parser over an input. Expects it does not fail or return Incomplete. It stops + /// parsing when the parser returns nom::Err::Error (which should occur at Eof). fn iter_parser( input: I, mut parser: impl Parser + Clone, - ) -> impl Iterator> + Clone { + ) -> impl Iterator + Clone { let mut remain = input; core::iter::from_fn(move || match parser.parse(remain) { Ok((rest, chunk)) => { remain = rest; - Some(Ok(chunk)) + Some(chunk) } Err(nom::Err::Error(..)) => None, - Err(nom::Err::Failure(..)) => Some(Err(fmt::Error)), - Err(nom::Err::Incomplete(..)) => Some(Err(fmt::Error)), + Err(nom::Err::Failure(..)) => panic!("iter_parser encountered nom::Err::Failure"), + Err(nom::Err::Incomplete(..)) => panic!("iter_parser encountered nom::Err::Incomplete"), }) } + /// Pops a single byte off the front of the input fn single_byte(input: &[u8]) -> IResult<&[u8], u8> { let (byte, remain) = input.split_first().ok_or_else(|| { nom::Err::Error(ParseError::from_error_kind( @@ -170,9 +191,7 @@ pub mod escaping { .parse(remain) } - pub(super) fn emit_chunks( - input: &[u8], - ) -> impl Iterator, fmt::Error>> + Clone { + pub(super) fn emit_chunks(input: &[u8]) -> impl Iterator> + Clone { iter_parser(input, chunk) } } @@ -190,18 +209,16 @@ pub mod escaping { .parse(remain) } - pub(super) fn emit_chunks( - input: &[u8], - ) -> impl Iterator, fmt::Error>> + Clone { + pub(super) fn emit_chunks(input: &[u8]) -> impl Iterator> + Clone { iter_parser(input, chunk) } } fn format_iter<'a, I>(iter: I) -> impl fmt::Display + 'a where - I: Iterator, fmt::Error>> + Clone + 'a, + I: Iterator> + Clone + 'a, { - display_utils::join_format(iter, "", |chunk, f| match chunk? { + display_utils::join_format(iter, "", |chunk, f| match chunk { EncodingChunk::Slice(slice) => { // Technically we know this is printable ASCII. is that utf8? let string = std::str::from_utf8(slice).map_err(|e| { @@ -239,6 +256,8 @@ pub mod escaping { assert_eq!(EscapeCharString(br"\").to_string(), r"\\"); } + /// Takes an iterator of `&[u8]` and implements Display, writing in value-list (escaped + /// comma-separated) encoding for presentation of lists of strings. pub struct EscapeValueList<'a, I: IntoIterator + Clone>(pub I); impl<'a, I> fmt::Display for EscapeValueList<'a, I> @@ -248,56 +267,23 @@ pub mod escaping { { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let values = self.0.clone().into_iter(); - // a temporary buffer to do EscapeCharString on. - let mut buf = Vec::::new(); - for value in values { + let chunk_iter = values.map(|value| { let chunks = value_list::emit_chunks(value); - for encoding_chunk in chunks { - match encoding_chunk? { - EncodingChunk::Slice(slice) => buf.extend_from_slice(slice), - EncodingChunk::Escape(str) => buf.extend_from_slice(str.as_bytes()), - // doesn't happen - EncodingChunk::Byte(byte) => buf.push(byte), - } - } - buf.push(b','); - } - buf.pop(); - EscapeCharString(buf).fmt(f) + let char_escaped_chunks = chunks + .map(|encoding_chunk| match encoding_chunk { + EncodingChunk::Slice(slice) => slice, + EncodingChunk::Escape(str) => str.as_bytes(), + EncodingChunk::Byte(_byte) => unreachable!( + "encountered EncodingChunk::Byte, not used in value_list::emit_chunks" + ), + }) + .map(EscapeCharString); + display_utils::concat(char_escaped_chunks) + }); + display_utils::join(chunk_iter, ",").fmt(f) } } - fn escape_char_string(string: &[u8], f: &mut fmt::Formatter<'_>) -> fmt::Result { - let chunks = char_string::emit_chunks(string); - let iter = format_iter(chunks); - if string.contains(&b' ') { - write!(f, "\"{}\"", iter) - } else { - iter.fmt(f) - } - } - - fn escape_values_join<'a>( - values: impl Iterator + Clone + 'a, - ) -> Result, fmt::Error> { - // for each value, encode `\` as `\\` and `,` as `\,`, and join all together with `,` - let mut vec = Vec::new(); - for value in values { - let chunks = value_list::emit_chunks(value); - for encoding_chunk in chunks { - match encoding_chunk? { - EncodingChunk::Slice(slice) => vec.extend_from_slice(slice), - EncodingChunk::Escape(str) => vec.extend_from_slice(str.as_bytes()), - // doesn't happen - EncodingChunk::Byte(byte) => vec.push(byte), - } - } - vec.push(b','); - } - vec.pop(); - Ok(vec) - } - impl fmt::Display for ValueList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { EscapeValueList(self.values.iter().map(|val| val.as_slice())).fmt(f) @@ -545,38 +531,47 @@ mod value_list_decoding { } #[test] - fn test_escaping() { - let parse = |slice: &[u8]| ValueList::parse(slice); - assert!(parse(b"").is_err()); + fn test_parsing() { + // value lists must be non-empty + assert!(ValueList::parse(b"").is_err()); assert_eq!( - parse(br"hello"), - Ok([b"hello"].iter().map(|x| x.to_vec()).collect()) + ValueList::parse(br"hello"), + Ok(vec![b"hello".to_vec()].into()) ); assert_eq!( - parse(br"hello\044hello"), - Ok(vec![br"hello".to_vec(), br"hello".to_vec()].into()) + ValueList::parse(br"hello\044hello"), + Ok(vec![b"hello".to_vec(), b"hello".to_vec()].into()) ); assert_eq!( - parse(br"hello\\\044hello"), + ValueList::parse(br"hello\\\044hello"), Ok(vec![br"hello,hello".to_vec()].into()) ); assert_eq!( - parse(br"hello\\\\044"), + ValueList::parse(br"hello\\\\044"), Ok(vec![br"hello\044".to_vec()].into()) ); assert_eq!( - parse(br"hello,\\\044"), + ValueList::parse(br"hello,\\\044"), Ok(vec![br"hello".to_vec(), br",".to_vec()].into()) ); - assert_eq!(parse(br"hello\\\\,"), Err(",".into()),); - assert_eq!(parse(br"hello\*"), Ok(vec![b"hello*".to_vec()].into())); assert_eq!( - parse(br"hi\,hello\*"), + ValueList::parse(br"hello\\\\,"), + Err(DecodingError::new(b",")), + ); + assert_eq!( + ValueList::parse(br"hello\*"), + Ok(vec![b"hello*".to_vec()].into()) + ); + assert_eq!( + ValueList::parse(br"hi\,hello\*"), Ok(vec![b"hi".to_vec(), b"hello*".to_vec()].into()) ); - assert_eq!(parse(b"\\,hello\\*("), Err("(".into())); - assert_eq!(parse(b"*;"), Err(";".into())); - assert_eq!(parse(b"*\""), Err("\"".into())); - assert_eq!(parse(b"*\""), Err("\"".into())); + assert_eq!( + ValueList::parse(b"\\,hello\\*("), + Err(DecodingError::new(b"(")) + ); + assert_eq!(ValueList::parse(b"*;"), Err(DecodingError::new(b";"))); + assert_eq!(ValueList::parse(b"*\""), Err(DecodingError::new(b"\""))); + assert_eq!(ValueList::parse(b"*\""), Err(DecodingError::new(b"\""))); } } From a41e8456fb1ecf588ba70a4be1ac771d8532db5d Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 18:04:47 +1000 Subject: [PATCH 15/26] disable single_use_lifetimes lint sometimes rustc warns but if you go back to implicit, then it errors out. make up your mind, rustc --- dns/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/dns/src/lib.rs b/dns/src/lib.rs index f1d9fb4..e90431b 100644 --- a/dns/src/lib.rs +++ b/dns/src/lib.rs @@ -5,7 +5,6 @@ #![warn(nonstandard_style)] #![warn(rust_2018_compatibility)] #![warn(rust_2018_idioms)] -#![warn(single_use_lifetimes)] #![warn(trivial_casts, trivial_numeric_casts)] #![warn(unused)] From 156032aed6604156ead431d69315c94bf855f9f4 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 20:14:35 +1000 Subject: [PATCH 16/26] HTTPS wraps SVCB better --- dns/src/record/svcb_https.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index af48932..87e4ff3 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -192,8 +192,8 @@ macro_rules! opaque { } } -/// A **SVCB** record, which contains an IP address as well as a port number, -/// for specifying the location of services more precisely. +/// A **SVCB** (*service binding*) record, which holds information needed to make connections to +/// network services, such as for HTTPS origins. /// /// # References /// @@ -201,13 +201,27 @@ macro_rules! opaque { /// specifying the location of services (February 2000) #[derive(PartialEq, Debug)] pub struct SVCB { - priority: u16, - target: Labels, + /// The priority of this record (relative to others, with lower values preferred). A value of 0 + /// indicates AliasMode. + pub priority: u16, + /// The domain name of either the alias target (for AliasMode) or the alternative endpoint (for + /// ServiceMode). + pub target: Labels, parameters: Option, } +/// An **HTTPS** record, which is the HTTPS incarnation of **SVCB**. #[derive(PartialEq, Debug)] -pub struct HTTPS(SVCB); +pub struct HTTPS { + /// The underlying SVCB record + pub svcb: SVCB, +} + +impl HTTPS { + pub fn new(svcb: SVCB) -> Self { + Self { svcb } + } +} u16_enum! { /// 14.3.2. Initial contents (subject to IANA additions) @@ -922,7 +936,7 @@ impl Wire for HTTPS { #[cfg_attr(feature = "with_mutagen", ::mutagen::mutate)] fn read(stated_length: u16, c: &mut Cursor<&[u8]>) -> Result { // TODO: default mandatory fields? something like that? - SVCB::read(stated_length, c).map(HTTPS) + SVCB::read(stated_length, c).map(HTTPS::new) } } @@ -984,7 +998,7 @@ impl Wire for SVCB { impl fmt::Display for HTTPS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + self.svcb.fmt(f) } } @@ -1048,7 +1062,7 @@ mod test { let result = HTTPS::read(buf.len() as _, &mut Cursor::new(buf)).unwrap(); assert_eq!( result, - HTTPS(SVCB { + HTTPS::new(SVCB { priority: 1, target: Labels::root(), parameters: Some(SvcParams { From 614117fc64afe2930a5c00dbf06df430eb36259a Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 20:14:57 +1000 Subject: [PATCH 17/26] Move ech decoding to another crate --- Cargo.lock | 24 +- Cargo.toml | 1 + dns/src/record/svcb_https.rs | 463 +------------------------------ ech-config/Cargo.toml | 22 ++ ech-config/src/cursor_ext.rs | 119 ++++++++ ech-config/src/lib.rs | 509 +++++++++++++++++++++++++++++++++++ ech-config/src/macros.rs | 76 ++++++ 7 files changed, 755 insertions(+), 459 deletions(-) create mode 100644 ech-config/Cargo.toml create mode 100644 ech-config/src/cursor_ext.rs create mode 100644 ech-config/src/lib.rs create mode 100644 ech-config/src/macros.rs diff --git a/Cargo.lock b/Cargo.lock index 1ad37e5..6c04bd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,6 +187,18 @@ dependencies = [ "rand", ] +[[package]] +name = "ech-config" +version = "0.1.0" +dependencies = [ + "base64", + "byteorder", + "env_logger", + "log", + "pretty_assertions", + "serde", +] + [[package]] name = "env_logger" version = "0.9.0" @@ -487,9 +499,9 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "pretty_assertions" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f297542c27a7df8d45de2b0e620308ab883ad232d06c14b76ac3e144bda50184" +checksum = "1cab0e7c02cf376875e9335e0ba1da535775beb5450d21e1dffca068818ed98b" dependencies = [ "ansi_term", "ctor", @@ -643,18 +655,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.125" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.125" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 2a40fb4..238e6a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ doctest = false members = [ "dns", "dns-transport", + "ech-config", ] diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 87e4ff3..9739d37 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -188,7 +188,6 @@ macro_rules! opaque { Self(From::from(vec)) } } - } } @@ -255,7 +254,7 @@ impl fmt::Display for SvcParam { Self::NoDefaultAlpn => f.write_str("no-default-alpn")?, Self::Port => f.write_str("port")?, Self::Ipv4Hint => f.write_str("ipv4hint")?, - Self::Ech => f.write_str("echconfig")?, + Self::Ech => f.write_str("ech")?, Self::Ipv6Hint => f.write_str("ipv6hint")?, Self::KeyNNNNN(n) => write!(f, "key{}", n)?, Self::InvalidKey => f.write_str("[invalid key]")?, @@ -285,7 +284,7 @@ struct SvcParams { /// /// Wire format, the value of the parameter is an ECHConfigList [ECH], including the redundant length prefix. /// Presentation format, the value is a single ECHConfigList encoded in Base64 [base64]. - ech: Option, + ech: Option>, ipv6hint: Vec, /// For any unrecognised keys. BTreeMap, because keys are sorted this way @@ -324,7 +323,11 @@ impl fmt::Display for SvcParams { write!(f, " ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; } if let Some(ech) = ech { - write!(f, " ech={}", ech.base64)?; + write!( + f, + " ech={}", + base64::display::Base64Display::with_config(ech, base64::STANDARD) + )?; } if !ipv6hint.is_empty() { write!(f, " ipv6hint={}", display_utils::join(ipv6hint.iter(), ","))?; @@ -381,8 +384,9 @@ impl SvcParams { ipv4hint = read_convert(cursor, len_hint, |c| c.read_u32::())?; } SvcParam::Ech => { - let parsed = ech::ECHConfigList::read_from(cursor)?; - ech = Some(parsed); + let mut vec = vec![0u8; len_hint]; + cursor.read_exact(&mut vec)?; + ech = Some(vec); } SvcParam::Ipv6Hint => { ipv6hint = read_convert(cursor, len_hint, |c| c.read_u128::())?; @@ -490,445 +494,6 @@ impl fmt::Debug for AlpnId { } } -/// ECH RFC draft 13 section 4 -mod ech { - use core::fmt; - use std::{ - convert::TryInto, - io::{self, Read}, - }; - - use byteorder::{BigEndian, ReadBytesExt}; - - use super::{CursorExt, Opaque, Opaque1, ReadFromCursor}; - - #[derive(Debug, Clone)] - pub struct ECHConfigList { - configs: Vec, - - /// Need a copy of the whole thing to encode as base64 - pub base64: String, - } - - impl PartialEq for ECHConfigList { - fn eq(&self, other: &Self) -> bool { - self.configs == other.configs - } - } - - impl From> for ECHConfigList { - fn from(configs: Vec) -> Self { - Self { - configs, - base64: String::from("base64 not computed"), - } - } - } - - impl ReadFromCursor for ECHConfigList { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { - let mut configs = Vec::new(); - - // write a base64 string that _includes the length field_ - let mut buf = cursor.std_remaining_slice(); - let mut throwaway = io::Cursor::new(buf); - let configs_length = throwaway.read_u16::()?; - buf = &buf[..configs_length as usize + 2]; - let base64 = base64::encode(buf); - - let configs_length = cursor.read_u16::()?; - log::trace!("ECHConfigList length = {}", configs_length); - - while cursor.std_remaining_slice().len() > 0 { - let config = ECHConfig::read_from(cursor)?; - configs.push(config); - } - Ok(Self { configs, base64 }) - } - } - - impl ReadFromCursor for ECHConfig { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { - let version = cursor.read_u16::()?; - log::trace!("ECHConfig version = 0x{:04x}", version); - let length = cursor.read_u16::()?; - log::trace!("ECHConfig length = {}", length); - match version { - 0xfe0d => cursor.with_truncated(u64::from(length), |cursor, _len_hint| { - let key_config = tls13::HpkeKeyConfig::read_from(cursor)?; - log::trace!("key_config = {:?}", key_config); - let maximum_name_length = cursor.read_u8()?; - log::trace!("maximum_name_length = {}", maximum_name_length); - let public_name = PublicName::read_from(cursor)?; - - let mut extensions = Vec::new(); - - let extensions_len = cursor.read_u16::()?; - log::trace!("extensions: len = {}", extensions_len); - cursor.with_truncated( - extensions_len as u64, - |cursor, _| -> io::Result<()> { - while cursor.std_remaining_slice().len() > 0 { - let ext = tls13::Extension::read_from(cursor)?; - extensions.push(ext); - } - Ok(()) - }, - )?; - - Ok(Self::EchConfigContents { - key_config, - maximum_name_length, - public_name, - extensions, - }) - }), - _ => { - let mut vec = vec![0u8; usize::from(length)]; - cursor.read_exact(&mut vec)?; - Ok(ECHConfig::UnknownECHVersion(version, Opaque(vec))) - } - } - } - } - - #[derive(Clone, PartialEq)] - pub struct PublicName(pub Vec); - - impl fmt::Debug for PublicName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let bytes = &self.0[..]; - f.write_str(&String::from_utf8_lossy(bytes)) - } - } - - impl ReadFromCursor for PublicName { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { - let len = cursor.read_u8()?; - log::trace!("PublicName length = {}", len); - if len == 0 || len > 254 { - return Err(io::Error::new( - io::ErrorKind::Other, - "length of opaque field was zero, but must be at least 1", - )); - } - let mut vec = vec![0u8; usize::from(len)]; - cursor.read_exact(&mut vec)?; - log::trace!("PublicName = {:?}", std::str::from_utf8(&vec)); - Ok(Self(vec)) - } - } - - #[derive(Debug, Clone, PartialEq)] - pub enum ECHConfig { - // if version == 0xfe0d - EchConfigContents { - key_config: tls13::HpkeKeyConfig, - maximum_name_length: u8, - // min len 1, max len 255 - public_name: PublicName, - - // any length up to 65535 - // - // each is a TLS 1.3 Extension, defined in RFC8446 section 4.2 - // - // > extensions MAY appear in any order, but - // > there MUST NOT be more than one extension of the same type in the - // > extensions block. An extension can be tagged as mandatory by using - // > an extension type codepoint with the high order bit set to 1. - - // > Clients MUST parse the extension list and check for unsupported - // > mandatory extensions. If an unsupported mandatory extension is - // > present, clients MUST ignore the "ECHConfig". - extensions: Vec, - }, - UnknownECHVersion(u16, Opaque), - } - - #[derive(Debug, Clone, PartialEq)] - pub enum EncryptedClientHello { - Outer { - cipher_suite: tls13::HpkeSymmetricCipherSuite, - config_id: u8, - enc: Opaque, - payload: Opaque1, - }, - Inner, - } - - u16_enum! { - pub enum ECHClientHelloType { - Outer = 0, - Inner = 1, - } - } - - impl ReadFromCursor for EncryptedClientHello { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { - let ty: ECHClientHelloType = cursor.read_u16::()?.try_into()?; - Ok(match ty { - ECHClientHelloType::Inner => EncryptedClientHello::Inner, - ECHClientHelloType::Outer => EncryptedClientHello::Outer { - cipher_suite: ReadFromCursor::read_from(cursor)?, - config_id: cursor.read_u8()?, - enc: super::Opaque::read_from(cursor)?, - payload: super::Opaque1::read_from(cursor)?, - }, - }) - } - } - - #[derive(Debug, Clone, PartialEq)] - pub struct EchOuterExtensions { - outer: Vec, - } - - pub mod tls13 { - use crate::record::svcb_https::{CursorExt, Opaque, ReadFromCursor}; - use byteorder::{BigEndian, ReadBytesExt}; - use std::io::{self, Read}; - - // mandatory-to-implement extensions from RFC8446 - // - // - Supported Versions ("supported_versions"; Section 4.2.1) - // - Cookie ("cookie"; Section 4.2.2) - // - Signature Algorithms ("signature_algorithms"; Section 4.2.3) - // - Signature Algorithms Certificate ("signature_algorithms_cert"; Section 4.2.3) - // - Negotiated Groups ("supported_groups"; Section 4.2.7) - // - Key Share ("key_share"; Section 4.2.8) - // - Server Name Indication ("server_name"; Section 3 of [RFC6066]) - // - #[derive(Debug, Clone, PartialEq)] - pub enum Extension { - /// `encrypted_client_hello` - EncryptedClientHello(super::EncryptedClientHello), - /// `ech_outer_extensions` - EchOuterExtensions(super::EchOuterExtensions), - - /// `server_name`. This is the SNI field. - /// - /// This probably shouldn't appear in an ECH config! The whole point is to avoid it! - /// So we'll parse it to show if it's being used - ServerName(ServerName), - - /// `supported_versions` (TLS version negotiation) - SupportedVersions(SupportedVersions), - - // /// `supported_groups` - // SupportedGroups(NamedGroupList), - - // /// `cookie` - // Cookie(Cookie), - - // /// `key_share` - // /// - // /// We assume a KeyShareClientHello version of this structure, because these - // /// extensions are for adding to a client hello message - // KeyShare(KeyShareClientHello), - Other(ExtensionType, UnknownExtension), - } - - impl ReadFromCursor for Extension { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { - let ty: ExtensionType = cursor.read_u16::()?.into(); - log::trace!("TLS extension: {:?}", ty); - let remain = cursor.std_remaining_slice(); - log::trace!("TLS remaining: {:?}", remain); - let len = cursor.read_u16::()?; - log::trace!("TLS extension length: {:?}", len); - cursor.with_truncated(len as u64, |cursor, len_hint| { - log::trace!("TLS extension length hint: {:?}", len_hint); - match ty { - // ExtensionType::ServerName => Extension::ServerName(ServerName::read_) - _ => Ok(Extension::Other( - ty, - UnknownExtension::read_len(cursor, len_hint as u64)?, - )), - } - }) - } - } - - #[derive(Debug, Clone, PartialEq)] - pub struct UnknownExtension(Opaque); - - impl UnknownExtension { - fn read_len(cursor: &mut io::Cursor<&[u8]>, len: u64) -> io::Result { - let mut vec = vec![0u8; len as usize]; - cursor.read_exact(&mut vec)?; - Ok(Self(Opaque::from(vec))) - } - } - - #[derive(Debug, Clone, PartialEq)] - pub enum ServerName { - // name type 0x0000 - HostName(HostName), - Unknown(UnknownNameType), - } - pub type NameType = u16; - - opaque!(pub struct HostName); - opaque!(pub struct UnknownNameType); - - u16_enum! { - /// Draft RFC - /// 7.1. Key Encapsulation Mechanisms (KEMs) - #[allow(non_camel_case_types)] - pub enum HpkeKemId { - Reserved = 0x0000, - DHKEM_P256_HKDF_SHA256 = 0x0010, - DHKEM_P384_HKDF_SHA384 = 0x0011, - DHKEM_P512_HKDF_SHA512 = 0x0012, - DHKEM_X25519_HKDF_SHA512 = 0x0020, - DHKEM_X448_HKDF_SHA512 = 0x0021, - @unknown Unknown(u16), - } - } - - u16_enum! { - /// Draft RFC - /// 7.2. Key Derivation Functions (KDFs) - #[allow(non_camel_case_types)] - pub enum HpkeKdfId { - Reserved = 0, - HKDF_SHA256 = 1, - HKDF_SHA384 = 2, - HKDF_SHA512 = 3, - @unknown Unknown(u16), - } - } - - u16_enum! { - /// Draft RFC - /// 7.3. Authenticated Encryption with Associated Data (AEAD) Functions - #[allow(non_camel_case_types)] - pub enum HpkeAeadId { - Reserved = 0, - AES_128_GCM = 1, - AES_256_GCM = 2, - ChaCha20Poly1305 = 3, - @unknown Unknown(u16), - ExportOnly = 0xffff, - } - } - - #[test] - fn test_hpke() { - let _hpke = HpkeAeadId::from(0x0003); - } - - #[derive(Debug, Clone, PartialEq)] - pub struct HpkeSymmetricCipherSuite { - pub kdf_id: HpkeKdfId, - pub aead_id: HpkeAeadId, - } - - impl ReadFromCursor for HpkeSymmetricCipherSuite { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { - Ok(Self { - kdf_id: cursor.read_u16::()?.into(), - aead_id: cursor.read_u16::()?.into(), - }) - } - } - - #[derive(Debug, Clone, PartialEq)] - pub struct HpkeKeyConfig { - pub config_id: u8, - pub kem_id: HpkeKemId, - pub public_key: HpkePublicKey, - // u16 len - pub cipher_suites: Vec, - } - - impl ReadFromCursor for HpkeKeyConfig { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { - let config_id = cursor.read_u8()?; - log::trace!("config_id = {:?}", config_id); - let kem_id = cursor.read_u16::()?.into(); - log::trace!("kem_id = {:?}", kem_id); - let public_key = HpkePublicKey::read_from(cursor)?; - log::trace!("public_key (len) = {:?}", (public_key.0).0.len()); - let cs_len = cursor.read_u16::()?; - log::trace!("cs_len = {:?}", cs_len); - if cs_len < 4 || cs_len as u32 > 2u32 << 16 - 4 { - return Err(io::Error::new( - io::ErrorKind::Other, - "cipher_suites length field invalid", - )); - } - let n_cipher_suites = cs_len as usize / 4; - log::trace!("n_cipher_suites = {:?}", n_cipher_suites); - let mut cipher_suites = Vec::with_capacity(n_cipher_suites); - for _ in 0..n_cipher_suites { - let suite = HpkeSymmetricCipherSuite::read_from(cursor)?; - cipher_suites.push(suite); - } - Ok(Self { - config_id, - kem_id, - public_key, - cipher_suites, - }) - } - } - - opaque!(pub struct HpkePublicKey); - - #[derive(Debug, Clone, PartialEq)] - pub struct SupportedVersions { - // min length 2 (otherwise implied by TLS version field), max length 254 - // len is a u8 i suppose? - pub versions: Vec, - } - - u16_enum! { - pub enum TlsVersion { - Ssl3_0 = 0x300, - Tls1_0 = 0x301, - Tls1_1 = 0x302, - Tls1_2 = 0x303, - Tls1_3 = 0x304, - @unknown Other(u16), - } - } - - // opaque!(pub struct Cookie); - - u16_enum! { - pub enum ExtensionType { - ServerName = 0, /* RFC 6066 */ - MaxFragmentLength = 1, /* RFC 6066 */ - StatusRequest = 5, /* RFC 6066 */ - SupportedGroups = 10, /* RFC 8422, 7919 */ - SignatureAlgorithms = 13, /* RFC 8446 */ - UseSrtp = 14, /* RFC 5764 */ - Heartbeat = 15, /* RFC 6520 */ - ApplicationLayerProtocolNegotiation = 16, /* RFC 7301 */ - SignedCertificateTimestamp = 18, /* RFC 6962 */ - ClientCertificateType = 19, /* RFC 7250 */ - ServerCertificateType = 20, /* RFC 7250 */ - Padding = 21, /* RFC 7685 */ - PreSharedKey = 41, /* RFC 8446 */ - EarlyData = 42, /* RFC 8446 */ - SupportedVersions = 43, /* RFC 8446 */ - Cookie = 44, /* RFC 8446 */ - PskKeyExchangeModes = 45, /* RFC 8446 */ - CertificateAuthorities = 47, /* RFC 8446 */ - OidFilters = 48, /* RFC 8446 */ - PostHandshakeAuth = 49, /* RFC 8446 */ - SignatureAlgorithmsCert = 50, /* RFC 8446 */ - KeyShare = 51, /* RFC 8446 */ - // This is the ECH extension - EncryptedClientHello = 0xfe0d, - EchOuterExtensions = 0xfd00, - @unknown Other(u16), - } - } - } -} - impl Wire for HTTPS { const NAME: &'static str = "HTTPS"; const RR_TYPE: u16 = 65; @@ -948,11 +513,6 @@ impl Wire for SVCB { fn read(stated_length: u16, cursor: &mut Cursor<&[u8]>) -> Result { let initial_pos = cursor.position(); - trace!( - "{:?}", - &cursor.std_remaining_slice()[..stated_length as usize] - ); - let ret = cursor.with_truncated( stated_length as _, move |cursor, _| -> Result { @@ -984,8 +544,6 @@ impl Wire for SVCB { "Length is incorrect (stated length {:?}, fields plus target length {:?})", stated_length, total_read ); - let remain = cursor.std_remaining_slice(); - warn!("remaining: {:?}", remain); Err(WireError::WrongLabelLength { stated_length, length_after_labels: total_read, @@ -1474,7 +1032,6 @@ mod test_vectors { #[cfg(test)] mod test_ech { - use super::ech::{tls13::*, *}; use super::*; use pretty_assertions::assert_eq; diff --git a/ech-config/Cargo.toml b/ech-config/Cargo.toml new file mode 100644 index 0000000..a876cdd --- /dev/null +++ b/ech-config/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ech-config" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# logging +log = "0.4" + +# protocol parsing helper +byteorder = "1.3" + +# printing of certain packets +base64 = "0.13.0" +pretty_assertions = "0.7.2" + +[dev-dependencies] +pretty_assertions = "0.7" +env_logger = "0.9.0" + diff --git a/ech-config/src/cursor_ext.rs b/ech-config/src/cursor_ext.rs new file mode 100644 index 0000000..20cdd28 --- /dev/null +++ b/ech-config/src/cursor_ext.rs @@ -0,0 +1,119 @@ +use byteorder::{BigEndian, ReadBytesExt}; +use std::convert::TryFrom; +use std::fmt; +use std::io::{self, Cursor, Read, Seek, SeekFrom}; +use std::ops::RangeInclusive; + +/// A kinda hacky but alright way to avoid copying tons of data +pub(crate) trait CursorExt { + /// Replace this when #[feature(cursor_remaining)] is stabilised + fn std_remaining_slice(&self) -> &[u8]; + + /// Convenience + fn truncated(&self, length: u64) -> Self; + fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T; +} + +impl CursorExt for Cursor<&[u8]> { + fn std_remaining_slice(&self) -> &[u8] { + let inner = self.get_ref(); + let len = self.position().min(inner.as_ref().len() as u64); + &inner[(len as usize)..] + } + fn truncated(&self, to_length: u64) -> Self { + let inner = self.get_ref(); + let len = inner.len() as u64; + let start = self.position().min(len); + let end = (start + to_length).min(len); + let trunc = &inner[(start as usize)..(end as usize)]; + Cursor::new(trunc) + } + fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T { + let mut trunc = self.truncated(length); + let len_hint = trunc.get_ref().len(); + let ret = f(&mut trunc, len_hint); + self.seek(SeekFrom::Current(trunc.position() as i64)) + .unwrap(); + ret + } +} + +pub(crate) trait ReadFromCursor: Sized { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result; +} + +impl TryFrom> for Opaque { + type Error = usize; + fn try_from(vec: Vec) -> Result { + if !(MIN as usize..=MAX as usize).contains(&vec.len()) { + Err(vec.len()) + } else { + Ok(Self(vec)) + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Opaque(pub Vec); + +impl Opaque { + pub(crate) fn read_known_len(cursor: &mut Cursor<&[u8]>, len: u16) -> io::Result { + let vec = read_vec_of_len(cursor, MIN..=MAX, len)?; + Ok(Self(vec)) + } +} + +pub fn read_vec_of_len( + cursor: &mut Cursor<&[u8]>, + limit: RangeInclusive, + len: u16, +) -> io::Result> { + if !limit.contains(&len) { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("invalid length {}: must be within {:?}", len, limit), + )); + } + let mut vec = vec![0u8; usize::from(len)]; + cursor.read_exact(&mut vec[..])?; + Ok(vec) +} + +pub fn read_vec(cursor: &mut Cursor<&[u8]>, limit: RangeInclusive) -> io::Result> { + let len = cursor.read_u16::()?; + log::trace!("read opaque length = {}", len); + read_vec_of_len(cursor, limit, len) +} + +impl ReadFromCursor for Opaque { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { + let vec = read_vec(cursor, MIN..=MAX)?; + Ok(Opaque(vec)) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Ascii(pub Vec); + +impl From> for Ascii { + fn from(vec: Vec) -> Self { + Self(vec) + } +} + +impl fmt::Display for Ascii { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0 + .iter() + .copied() + .map(std::ascii::escape_default) + .try_for_each(|esc| esc.fmt(f)) + } +} + +impl ReadFromCursor for Ascii { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { + let vec = read_vec(cursor, 0..=u16::MAX)?; + Ok(Ascii(vec)) + } +} diff --git a/ech-config/src/lib.rs b/ech-config/src/lib.rs new file mode 100644 index 0000000..4c7ecdd --- /dev/null +++ b/ech-config/src/lib.rs @@ -0,0 +1,509 @@ +//! ECH RFC draft 13 section 4 + +use core::fmt; +use std::{ + convert::TryInto, + io::{self, Read}, +}; + +use byteorder::{BigEndian, ReadBytesExt}; + +#[macro_use] +mod macros; +mod cursor_ext; + +use cursor_ext::{CursorExt, Opaque, ReadFromCursor}; + +#[derive(Debug, Clone, PartialEq)] +pub struct ECHConfigList { + configs: Vec, +} + +impl ECHConfigList { + fn from_base64(base: &str) -> io::Result { + let buffer = base64::decode_config(base, base64::STANDARD) + .map_err(|de| io::Error::new(io::ErrorKind::Other, format!("{}", de)))?; + log::trace!("{:?}", buffer); + + let mut cursor = io::Cursor::new(&buffer[..]); + let ret = Self::read_from(&mut cursor)?; + let remain = cursor.std_remaining_slice(); + if remain.is_empty() { + Ok(ret) + } else { + println!("parsed but had bytes leftover: {:?}", ret); + Err(io::Error::new( + io::ErrorKind::Other, + format!("base64 string had leftover bytes: {:?}", remain), + )) + } + } +} + +impl From> for ECHConfigList { + fn from(configs: Vec) -> Self { + Self { configs } + } +} + +impl ReadFromCursor for ECHConfigList { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { + let mut configs = Vec::new(); + + let configs_length = cursor.read_u16::()?; + log::trace!("ECHConfigList length = {}", configs_length); + + cursor.with_truncated(configs_length.into(), |cursor, _| { + while cursor.std_remaining_slice().len() > 0 { + let config = ECHConfig::read_from(cursor)?; + configs.push(config); + } + Ok(Self { configs }) + }) + } +} + +impl ReadFromCursor for ECHConfig { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { + let version = cursor.read_u16::()?; + log::trace!("ECHConfig version = 0x{:04x}", version); + let length = cursor.read_u16::()?; + log::trace!("ECHConfig length = {}", length); + match version { + 0xfe0d => cursor.with_truncated(u64::from(length), |cursor, _len_hint| { + let key_config = tls13::HpkeKeyConfig::read_from(cursor)?; + log::trace!("key_config = {:?}", key_config); + let maximum_name_length = cursor.read_u8()?; + log::trace!("maximum_name_length = {}", maximum_name_length); + let public_name = PublicName::read_from(cursor)?; + + let mut extensions = Vec::new(); + + let extensions_len = cursor.read_u16::()?; + log::trace!("extensions: len = {}", extensions_len); + cursor.with_truncated(extensions_len as u64, |cursor, _| -> io::Result<()> { + while cursor.std_remaining_slice().len() > 0 { + let ext = tls13::Extension::read_from(cursor)?; + extensions.push(ext); + } + Ok(()) + })?; + + Ok(Self::EchConfigContents { + key_config, + maximum_name_length, + public_name, + extensions, + }) + }), + _ => { + let opq = Opaque::read_known_len(cursor, length)?; + Ok(ECHConfig::UnknownECHVersion(version, opq)) + } + } + } +} + +#[derive(Clone, PartialEq)] +pub struct PublicName(pub Vec); + +impl fmt::Debug for PublicName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = &self.0[..]; + f.write_str(&String::from_utf8_lossy(bytes)) + } +} + +impl ReadFromCursor for PublicName { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> io::Result { + let len = cursor.read_u8()?; + log::trace!("PublicName length = {}", len); + if len == 0 || len > 254 { + return Err(io::Error::new( + io::ErrorKind::Other, + "length of opaque field was zero, but must be at least 1", + )); + } + let mut vec = vec![0u8; usize::from(len)]; + cursor.read_exact(&mut vec)?; + log::trace!("PublicName = {:?}", std::str::from_utf8(&vec)); + Ok(Self(vec)) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ECHConfig { + // if version == 0xfe0d + EchConfigContents { + key_config: tls13::HpkeKeyConfig, + maximum_name_length: u8, + // min len 1, max len 255 + public_name: PublicName, + + // any length up to 65535 + // + // each is a TLS 1.3 Extension, defined in RFC8446 section 4.2 + // + // > extensions MAY appear in any order, but + // > there MUST NOT be more than one extension of the same type in the + // > extensions block. An extension can be tagged as mandatory by using + // > an extension type codepoint with the high order bit set to 1. + + // > Clients MUST parse the extension list and check for unsupported + // > mandatory extensions. If an unsupported mandatory extension is + // > present, clients MUST ignore the "ECHConfig". + extensions: Vec, + }, + UnknownECHVersion(u16, Opaque<0, { u16::MAX }>), +} + +#[derive(Debug, Clone, PartialEq)] +pub enum EncryptedClientHello { + Outer { + cipher_suite: tls13::HpkeSymmetricCipherSuite, + config_id: u8, + enc: Opaque<0, { u16::MAX }>, + payload: Opaque<1, { u16::MAX }>, + }, + Inner, +} + +u16_enum! { + pub enum ECHClientHelloType { + Outer = 0, + Inner = 1, + } +} + +impl ReadFromCursor for EncryptedClientHello { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + let ty: ECHClientHelloType = cursor.read_u16::()?.try_into()?; + Ok(match ty { + ECHClientHelloType::Inner => EncryptedClientHello::Inner, + ECHClientHelloType::Outer => EncryptedClientHello::Outer { + cipher_suite: ReadFromCursor::read_from(cursor)?, + config_id: cursor.read_u8()?, + enc: Opaque::read_from(cursor)?, + payload: Opaque::read_from(cursor)?, + }, + }) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct EchOuterExtensions { + outer: Vec, +} + +pub mod tls13 { + use crate::cursor_ext::{Ascii, CursorExt, Opaque, ReadFromCursor}; + use byteorder::{BigEndian, ReadBytesExt}; + use std::io; + + // mandatory-to-implement extensions from RFC8446 + // + // - Supported Versions ("supported_versions"; Section 4.2.1) + // - Cookie ("cookie"; Section 4.2.2) + // - Signature Algorithms ("signature_algorithms"; Section 4.2.3) + // - Signature Algorithms Certificate ("signature_algorithms_cert"; Section 4.2.3) + // - Negotiated Groups ("supported_groups"; Section 4.2.7) + // - Key Share ("key_share"; Section 4.2.8) + // - Server Name Indication ("server_name"; Section 3 of [RFC6066]) + // + #[derive(Debug, Clone, PartialEq)] + pub enum Extension { + /// `encrypted_client_hello` + EncryptedClientHello(super::EncryptedClientHello), + /// `ech_outer_extensions` + EchOuterExtensions(super::EchOuterExtensions), + + /// `server_name`. This is the SNI field. + /// + /// This probably shouldn't appear in an ECH config! The whole point is to avoid it! + /// So we'll parse it to show if it's being used + ServerName(ServerName), + + /// `supported_versions` (TLS version negotiation) + SupportedVersions(SupportedVersions), + + // /// `supported_groups` + // SupportedGroups(NamedGroupList), + + // /// `cookie` + // Cookie(Cookie), + + // /// `key_share` + // /// + // /// We assume a KeyShareClientHello version of this structure, because these + // /// extensions are for adding to a client hello message + // KeyShare(KeyShareClientHello), + Other(ExtensionType, UnknownExtension), + } + + impl ReadFromCursor for Extension { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + let ty: ExtensionType = cursor.read_u16::()?.into(); + log::trace!("TLS extension: {:?}", ty); + let remain = cursor.std_remaining_slice(); + log::trace!("TLS remaining: {:?}", remain); + let len = cursor.read_u16::()?; + log::trace!("TLS extension length: {:?}", len); + cursor.with_truncated(len as u64, |cursor, len_hint| { + log::trace!("TLS extension length hint: {:?}", len_hint); + match ty { + // ExtensionType::ServerName => Extension::ServerName(ServerName::read_) + _ => Ok(Extension::Other( + ty, + UnknownExtension::read_len(cursor, len)?, + )), + } + }) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct UnknownExtension(Opaque<0, { u16::MAX }>); + + impl UnknownExtension { + fn read_len(cursor: &mut io::Cursor<&[u8]>, len: u16) -> io::Result { + let vec = crate::cursor_ext::read_vec_of_len(cursor, 0..=u16::MAX, len)?; + Ok(Self(Opaque(vec))) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub enum ServerName { + // name type 0x0000 + HostName(HostName), + Unknown(UnknownNameType), + } + pub type NameType = u16; + + pub type HostName = Ascii; + pub type UnknownNameType = Opaque<0, { u16::MAX }>; + + u16_enum! { + /// Draft RFC + /// 7.1. Key Encapsulation Mechanisms (KEMs) + #[allow(non_camel_case_types)] + pub enum HpkeKemId { + Reserved = 0x0000, + DHKEM_P256_HKDF_SHA256 = 0x0010, + DHKEM_P384_HKDF_SHA384 = 0x0011, + DHKEM_P512_HKDF_SHA512 = 0x0012, + DHKEM_X25519_HKDF_SHA512 = 0x0020, + DHKEM_X448_HKDF_SHA512 = 0x0021, + @unknown Unknown(u16), + } + } + + u16_enum! { + /// Draft RFC + /// 7.2. Key Derivation Functions (KDFs) + #[allow(non_camel_case_types)] + pub enum HpkeKdfId { + Reserved = 0, + HKDF_SHA256 = 1, + HKDF_SHA384 = 2, + HKDF_SHA512 = 3, + @unknown Unknown(u16), + } + } + + u16_enum! { + /// Draft RFC + /// 7.3. Authenticated Encryption with Associated Data (AEAD) Functions + #[allow(non_camel_case_types)] + pub enum HpkeAeadId { + Reserved = 0, + AES_128_GCM = 1, + AES_256_GCM = 2, + ChaCha20Poly1305 = 3, + @unknown Unknown(u16), + ExportOnly = 0xffff, + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct HpkeSymmetricCipherSuite { + pub kdf_id: HpkeKdfId, + pub aead_id: HpkeAeadId, + } + + impl ReadFromCursor for HpkeSymmetricCipherSuite { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + Ok(Self { + kdf_id: cursor.read_u16::()?.into(), + aead_id: cursor.read_u16::()?.into(), + }) + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct HpkeKeyConfig { + pub config_id: u8, + pub kem_id: HpkeKemId, + pub public_key: HpkePublicKey, + // u16 len + pub cipher_suites: Vec, + } + + impl ReadFromCursor for HpkeKeyConfig { + fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { + let config_id = cursor.read_u8()?; + log::trace!("config_id = {:?}", config_id); + let kem_id = cursor.read_u16::()?.into(); + log::trace!("kem_id = {:?}", kem_id); + let public_key = HpkePublicKey::read_from(cursor)?; + log::trace!("public_key (len) = {:?}", public_key.0.len()); + let cs_len = cursor.read_u16::()?; + log::trace!("cs_len = {:?}", cs_len); + if cs_len < 4 || cs_len as u32 > 2u32 << 16 - 4 { + return Err(io::Error::new( + io::ErrorKind::Other, + "cipher_suites length field invalid", + )); + } + let n_cipher_suites = cs_len as usize / 4; + log::trace!("n_cipher_suites = {:?}", n_cipher_suites); + let mut cipher_suites = Vec::with_capacity(n_cipher_suites); + for _ in 0..n_cipher_suites { + let suite = HpkeSymmetricCipherSuite::read_from(cursor)?; + cipher_suites.push(suite); + } + Ok(Self { + config_id, + kem_id, + public_key, + cipher_suites, + }) + } + } + + // opaque!(pub struct HpkePublicKey<1, {u16::MAX}>); + pub type HpkePublicKey = Opaque<1, { u16::MAX }>; + + #[derive(Debug, Clone, PartialEq)] + pub struct SupportedVersions { + // min length 2 (otherwise implied by TLS version field), max length 254 + // len is a u8 i suppose? + pub versions: Vec, + } + + u16_enum! { + pub enum TlsVersion { + Ssl3_0 = 0x300, + Tls1_0 = 0x301, + Tls1_1 = 0x302, + Tls1_2 = 0x303, + Tls1_3 = 0x304, + @unknown Other(u16), + } + } + + // opaque!(pub struct Cookie); + + u16_enum! { + pub enum ExtensionType { + ServerName = 0, /* RFC 6066 */ + MaxFragmentLength = 1, /* RFC 6066 */ + StatusRequest = 5, /* RFC 6066 */ + SupportedGroups = 10, /* RFC 8422, 7919 */ + SignatureAlgorithms = 13, /* RFC 8446 */ + UseSrtp = 14, /* RFC 5764 */ + Heartbeat = 15, /* RFC 6520 */ + ApplicationLayerProtocolNegotiation = 16, /* RFC 7301 */ + SignedCertificateTimestamp = 18, /* RFC 6962 */ + ClientCertificateType = 19, /* RFC 7250 */ + ServerCertificateType = 20, /* RFC 7250 */ + Padding = 21, /* RFC 7685 */ + PreSharedKey = 41, /* RFC 8446 */ + EarlyData = 42, /* RFC 8446 */ + SupportedVersions = 43, /* RFC 8446 */ + Cookie = 44, /* RFC 8446 */ + PskKeyExchangeModes = 45, /* RFC 8446 */ + CertificateAuthorities = 47, /* RFC 8446 */ + OidFilters = 48, /* RFC 8446 */ + PostHandshakeAuth = 49, /* RFC 8446 */ + SignatureAlgorithmsCert = 50, /* RFC 8446 */ + KeyShare = 51, /* RFC 8446 */ + // This is the ECH extension + EncryptedClientHello = 0xfe0d, + EchOuterExtensions = 0xfd00, + @unknown Other(u16), + } + } +} + +#[cfg(test)] +mod test { + use super::tls13::*; + use super::*; + use pretty_assertions::assert_eq; + #[test] + fn cloudflare() { + init_logs(); + // from crypto.cloudflare.com + let public_key: [u8; 32] = [ + 40, 38, 25, 12, 212, 168, 183, 42, 218, 32, 41, 154, 44, 61, 152, 136, 131, 114, 86, + 111, 194, 66, 154, 114, 231, 170, 205, 83, 72, 105, 105, 119, + ]; + let buf = &[ + 0, 70, // echconfiglist: len = 70 + 254, 13, // config version: 0xfe0d + 0, 66, // config len + 63, // config id + 0, 32, 0, 32, // hpke stuff + 40, 38, 25, 12, 212, 168, 183, 42, 218, 32, 41, 154, 44, 61, 152, 136, 131, 114, 86, + 111, 194, 66, 154, 114, 231, 170, 205, 83, 72, 105, 105, 119, // public_key + 0, 4, // cipher suites len + 0, 1, 0, 1, // cipher suites + 0, 19, // public name + 99, 108, 111, 117, 100, 102, 108, 97, 114, 101, 45, 101, 115, 110, 105, 46, 99, 111, + 109, // cloudflare-esni.com + 0, 0, // extensions len + ]; + let expected = ECHConfigList { + configs: vec![ECHConfig::EchConfigContents { + key_config: HpkeKeyConfig { + config_id: 63, + kem_id: HpkeKemId::DHKEM_X25519_HKDF_SHA512, + cipher_suites: vec![HpkeSymmetricCipherSuite { + kdf_id: HpkeKdfId::HKDF_SHA256, + aead_id: HpkeAeadId::AES_128_GCM, + }], + public_key: public_key.to_vec().try_into().unwrap(), + }, + maximum_name_length: 0, + public_name: PublicName(b"cloudflare-esni.com".to_vec()), + extensions: vec![], + }], + }; + + assert_eq!( + ECHConfigList::read_from(&mut io::Cursor::new(buf)) + .map_err(|e| e.to_string()) + .as_ref(), + Ok(&expected) + ); + // this is what google returned for HTTPS crypto.cloudflare.com on 2021-09-26 + let base = "AEb+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"; + assert_eq!(base64::encode(buf), base); + + assert_eq!( + ECHConfigList::from_base64(base) + .map_err(|e| e.to_string()) + .as_ref(), + Ok(&expected), + ); + } +} + +#[cfg(test)] +fn init_logs() { + use std::sync::Once; + static LOG_INIT: Once = Once::new(); + LOG_INIT.call_once(|| { + env_logger::init(); + }); +} diff --git a/ech-config/src/macros.rs b/ech-config/src/macros.rs new file mode 100644 index 0000000..f994fd9 --- /dev/null +++ b/ech-config/src/macros.rs @@ -0,0 +1,76 @@ +macro_rules! u16_enum { + { + $(#[$attr:meta])* + $vis:vis enum $name:ident { + $( + $(#[$vattr:meta])* + $variant:ident = $lit:literal,)+ + } + } => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + #[repr(u16)] + $vis enum $name { + $( + $(#[$vattr])* + $variant = $lit,)+ + } + impl core::convert::TryFrom for $name { + type Error = std::io::Error; + + fn try_from(int: u16) -> Result { + match int { + $($lit => Ok(Self::$variant),)+ + _ => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("invalid value for {}: {:04x}", stringify!($name), int) + ) + ) + } + } + } + }; + { + $(#[$attr:meta])* + $vis:vis enum $name:ident { + $( + $(#[$vattr:meta])* + $variant:ident = $lit:literal,)+ + $( + @unknown + $(#[$uattr:meta])* + $unknown:ident (u16), + $( + $(#[$vattr2:meta])* + $variant2:ident = $lit2:literal,)* + )? + } + } => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + #[repr(u16)] + $vis enum $name { + $( + $(#[$vattr])* + $variant = $lit,)+ + $( + $(#[$uattr])* + $unknown(u16), + $( + $(#[$vattr2])* + $variant2 = $lit2,)* + )? + } + impl From for $name { + fn from(int: u16) -> Self { + match int { + $($lit => Self::$variant,)+ + $( + $($lit2 => Self::$variant2,)* + _ => Self::$unknown(int), + )? + } + } + } + }; +} From 23e184134c2b3c9c94dbe39fc879aa4f55111361 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 20:15:20 +1000 Subject: [PATCH 18/26] update cargo-fuzz lockfile --- dns/fuzz/Cargo.lock | 74 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/dns/fuzz/Cargo.lock b/dns/fuzz/Cargo.lock index 48caf6e..45af730 100644 --- a/dns/fuzz/Cargo.lock +++ b/dns/fuzz/Cargo.lock @@ -1,62 +1,106 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "arbitrary" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0922a3e746b5a44e111e5603feb6704e5cc959116f66737f50bb5cbd264e9d87" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "byteorder" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" [[package]] name = "cc" version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" [[package]] name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "display_utils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cabb9d45c20801e16b8483e998bd1ebe4a07bc56406135ded5b4e3ad140c30" [[package]] name = "dns" -version = "0.1.0" +version = "0.2.0-pre" dependencies = [ - "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "base64", + "byteorder", + "display_utils", + "log", + "nom", ] [[package]] name = "dns-fuzz" version = "0.0.1" dependencies = [ - "dns 0.1.0", - "libfuzzer-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "dns", + "libfuzzer-sys", ] [[package]] name = "libfuzzer-sys" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8c42ab62f43795ed77a965ed07994c5584cdc94fd0ebf14b22ac1524077acc" dependencies = [ - "arbitrary 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "cc 1.0.60 (registry+https://github.com/rust-lang/crates.io-index)", + "arbitrary", + "cc", ] [[package]] name = "log" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "minimal-lexical" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c835948974f68e0bd58636fc6c5b1fbff7b297e3046f11b3b3c18bbac012c6d" + +[[package]] +name = "nom" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" dependencies = [ - "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr", + "minimal-lexical", + "version_check", ] -[metadata] -"checksum arbitrary 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0922a3e746b5a44e111e5603feb6704e5cc959116f66737f50bb5cbd264e9d87" -"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" -"checksum cc 1.0.60 (registry+https://github.com/rust-lang/crates.io-index)" = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c" -"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" -"checksum libfuzzer-sys 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ee8c42ab62f43795ed77a965ed07994c5584cdc94fd0ebf14b22ac1524077acc" -"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" From f090a20fa57786d3bf7f334156cd03d6d1f83e5b Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 21:14:29 +1000 Subject: [PATCH 19/26] ech: Serialize as json --- Cargo.lock | 96 +++++++++++++++-- ech-config/Cargo.toml | 7 ++ ech-config/bin/ech_config.rs | 12 +++ ech-config/src/cursor_ext.rs | 10 +- ech-config/src/lib.rs | 161 ++++++++++++++++++---------- ech-config/src/serde_with_base64.rs | 141 ++++++++++++++++++++++++ 6 files changed, 361 insertions(+), 66 deletions(-) create mode 100644 ech-config/bin/ech_config.rs create mode 100644 ech-config/src/serde_with_base64.rs diff --git a/Cargo.lock b/Cargo.lock index 6c04bd1..c6c91a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,6 +122,41 @@ dependencies = [ "syn", ] +[[package]] +name = "darling" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "757c0ded2af11d8e739c4daea1ac623dd1624b06c844cf3f5a39f1bdbd99bb12" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c34d8efb62d0c2d7f60ece80f75e5c63c1588ba68032740494b0b9a996466e3" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade7bff147130fe5e6d39f089c6bd49ec0250f35d70b2eebf72afdfc919f15cc" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "datetime" version = "0.5.1" @@ -197,6 +232,8 @@ dependencies = [ "log", "pretty_assertions", "serde", + "serde_json", + "serde_with", ] [[package]] @@ -234,6 +271,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -296,6 +339,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "ipconfig" version = "0.2.2" @@ -511,9 +560,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] @@ -614,6 +663,12 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +[[package]] +name = "rustversion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b3909d758bb75c79f23d4736fac9433868679d3ad2ea7a61e3c25cfda9a088" + [[package]] name = "ryu" version = "1.0.5" @@ -675,15 +730,38 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_with" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "062b87e45d8f26714eacfaef0ed9a583e2bfd50ebd96bdd3c200733bd5758e2c" +dependencies = [ + "rustversion", + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c1fcca18d55d1763e1c16873c4bde0ac3ef75179a28c7b372917e0494625be" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.3.19" @@ -695,11 +773,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" -version = "1.0.65" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a1d708c221c5a612956ef9f75b37e454e88d1f7b899fbd3a18d4252012d663" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" dependencies = [ "proc-macro2", "quote", diff --git a/ech-config/Cargo.toml b/ech-config/Cargo.toml index a876cdd..11b8c77 100644 --- a/ech-config/Cargo.toml +++ b/ech-config/Cargo.toml @@ -5,6 +5,10 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "ech-config" +path = "bin/ech_config.rs" + [dependencies] # logging log = "0.4" @@ -15,6 +19,9 @@ byteorder = "1.3" # printing of certain packets base64 = "0.13.0" pretty_assertions = "0.7.2" +serde = { version = "1.0.130", features = ["derive"] } +serde_with = { version = "1.10.0", features = [] } +serde_json = "1.0.68" [dev-dependencies] pretty_assertions = "0.7" diff --git a/ech-config/bin/ech_config.rs b/ech-config/bin/ech_config.rs new file mode 100644 index 0000000..a86bce0 --- /dev/null +++ b/ech-config/bin/ech_config.rs @@ -0,0 +1,12 @@ +use ech_config::ECHConfigList; +use serde_json; + +fn main() { + let base = "AEb+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"; + let configs = ECHConfigList::from_base64(base).unwrap(); + println!("{}", serde_json::to_string_pretty(&configs).unwrap()); + let unknown_version = "AEc+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"; + let configs = ECHConfigList::from_base64(unknown_version).unwrap(); + println!("{}", serde_json::to_string_pretty(&configs).unwrap()); + assert!(false); +} diff --git a/ech-config/src/cursor_ext.rs b/ech-config/src/cursor_ext.rs index 20cdd28..8399a57 100644 --- a/ech-config/src/cursor_ext.rs +++ b/ech-config/src/cursor_ext.rs @@ -1,4 +1,5 @@ use byteorder::{BigEndian, ReadBytesExt}; +use serde::{Serialize, Deserialize}; use std::convert::TryFrom; use std::fmt; use std::io::{self, Cursor, Read, Seek, SeekFrom}; @@ -53,8 +54,11 @@ impl TryFrom> for Opaque { } } -#[derive(Debug, Clone, PartialEq)] -pub struct Opaque(pub Vec); +#[serde_with::serde_as] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Opaque( + #[serde_as(as = "crate::serde_with_base64::Base64")] pub Vec, +); impl Opaque { pub(crate) fn read_known_len(cursor: &mut Cursor<&[u8]>, len: u16) -> io::Result { @@ -92,7 +96,7 @@ impl ReadFromCursor for Opaque { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Ascii(pub Vec); impl From> for Ascii { diff --git a/ech-config/src/lib.rs b/ech-config/src/lib.rs index 4c7ecdd..309c2ff 100644 --- a/ech-config/src/lib.rs +++ b/ech-config/src/lib.rs @@ -7,20 +7,23 @@ use std::{ }; use byteorder::{BigEndian, ReadBytesExt}; +use serde::{Deserialize, Serialize}; #[macro_use] mod macros; mod cursor_ext; +mod serde_with_base64; use cursor_ext::{CursorExt, Opaque, ReadFromCursor}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] pub struct ECHConfigList { configs: Vec, } impl ECHConfigList { - fn from_base64(base: &str) -> io::Result { + pub fn from_base64(base: &str) -> io::Result { let buffer = base64::decode_config(base, base64::STANDARD) .map_err(|de| io::Error::new(io::ErrorKind::Other, format!("{}", de)))?; log::trace!("{:?}", buffer); @@ -69,48 +72,73 @@ impl ReadFromCursor for ECHConfig { log::trace!("ECHConfig version = 0x{:04x}", version); let length = cursor.read_u16::()?; log::trace!("ECHConfig length = {}", length); - match version { - 0xfe0d => cursor.with_truncated(u64::from(length), |cursor, _len_hint| { - let key_config = tls13::HpkeKeyConfig::read_from(cursor)?; - log::trace!("key_config = {:?}", key_config); - let maximum_name_length = cursor.read_u8()?; - log::trace!("maximum_name_length = {}", maximum_name_length); - let public_name = PublicName::read_from(cursor)?; - - let mut extensions = Vec::new(); - - let extensions_len = cursor.read_u16::()?; - log::trace!("extensions: len = {}", extensions_len); - cursor.with_truncated(extensions_len as u64, |cursor, _| -> io::Result<()> { - while cursor.std_remaining_slice().len() > 0 { - let ext = tls13::Extension::read_from(cursor)?; - extensions.push(ext); - } - Ok(()) - })?; - - Ok(Self::EchConfigContents { - key_config, - maximum_name_length, - public_name, - extensions, - }) - }), + let contents = match version { + 0xfe0d => cursor.with_truncated( + u64::from(length), + |cursor, _len_hint| -> io::Result { + let key_config = tls13::HpkeKeyConfig::read_from(cursor)?; + log::trace!("key_config = {:?}", key_config); + let maximum_name_length = cursor.read_u8()?; + log::trace!("maximum_name_length = {}", maximum_name_length); + let public_name = PublicName::read_from(cursor)?; + + let mut extensions = Vec::new(); + + let extensions_len = cursor.read_u16::()?; + log::trace!("extensions: len = {}", extensions_len); + cursor.with_truncated( + extensions_len as u64, + |cursor, _| -> io::Result<()> { + while cursor.std_remaining_slice().len() > 0 { + let ext = tls13::Extension::read_from(cursor)?; + extensions.push(ext); + } + Ok(()) + }, + )?; + + Ok(ECHConfigContents::Version0xfe0d { + key_config, + maximum_name_length, + public_name, + extensions, + }) + }, + )?, _ => { let opq = Opaque::read_known_len(cursor, length)?; - Ok(ECHConfig::UnknownECHVersion(version, opq)) + ECHConfigContents::UnknownECHVersion(opq) } - } + }; + Ok(Self { version, contents }) } } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Deserialize, Serialize)] pub struct PublicName(pub Vec); impl fmt::Debug for PublicName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bytes = &self.0[..]; - f.write_str(&String::from_utf8_lossy(bytes)) + String::from_utf8_lossy(bytes).fmt(f) + } +} + +impl fmt::Display for PublicName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = &self.0[..]; + String::from_utf8_lossy(bytes).fmt(f) + } +} + +impl std::str::FromStr for PublicName { + type Err = &'static str; + fn from_str(s: &str) -> Result { + if (1..=254).contains(&s.len()) { + Ok(Self(s.as_bytes().to_vec())) + } else { + Err("string length not in range 1..=254") + } } } @@ -131,13 +159,21 @@ impl ReadFromCursor for PublicName { } } -#[derive(Debug, Clone, PartialEq)] -pub enum ECHConfig { +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub struct ECHConfig { + pub version: u16, + pub contents: ECHConfigContents, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ECHConfigContents { // if version == 0xfe0d - EchConfigContents { + Version0xfe0d { key_config: tls13::HpkeKeyConfig, maximum_name_length: u8, // min len 1, max len 255 + #[serde(with = "serde_with::rust::display_fromstr")] public_name: PublicName, // any length up to 65535 @@ -154,10 +190,10 @@ pub enum ECHConfig { // > present, clients MUST ignore the "ECHConfig". extensions: Vec, }, - UnknownECHVersion(u16, Opaque<0, { u16::MAX }>), + UnknownECHVersion(Opaque<0, { u16::MAX }>), } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum EncryptedClientHello { Outer { cipher_suite: tls13::HpkeSymmetricCipherSuite, @@ -169,6 +205,7 @@ pub enum EncryptedClientHello { } u16_enum! { + #[derive(Deserialize, Serialize)] pub enum ECHClientHelloType { Outer = 0, Inner = 1, @@ -190,7 +227,7 @@ impl ReadFromCursor for EncryptedClientHello { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct EchOuterExtensions { outer: Vec, } @@ -198,6 +235,7 @@ pub struct EchOuterExtensions { pub mod tls13 { use crate::cursor_ext::{Ascii, CursorExt, Opaque, ReadFromCursor}; use byteorder::{BigEndian, ReadBytesExt}; + use serde::{Deserialize, Serialize}; use std::io; // mandatory-to-implement extensions from RFC8446 @@ -210,7 +248,7 @@ pub mod tls13 { // - Key Share ("key_share"; Section 4.2.8) // - Server Name Indication ("server_name"; Section 3 of [RFC6066]) // - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum Extension { /// `encrypted_client_hello` EncryptedClientHello(super::EncryptedClientHello), @@ -261,7 +299,7 @@ pub mod tls13 { } } - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct UnknownExtension(Opaque<0, { u16::MAX }>); impl UnknownExtension { @@ -271,7 +309,7 @@ pub mod tls13 { } } - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub enum ServerName { // name type 0x0000 HostName(HostName), @@ -286,6 +324,7 @@ pub mod tls13 { /// Draft RFC /// 7.1. Key Encapsulation Mechanisms (KEMs) #[allow(non_camel_case_types)] + #[derive(Deserialize, Serialize)] pub enum HpkeKemId { Reserved = 0x0000, DHKEM_P256_HKDF_SHA256 = 0x0010, @@ -301,6 +340,7 @@ pub mod tls13 { /// Draft RFC /// 7.2. Key Derivation Functions (KDFs) #[allow(non_camel_case_types)] + #[derive(Deserialize, Serialize)] pub enum HpkeKdfId { Reserved = 0, HKDF_SHA256 = 1, @@ -314,6 +354,7 @@ pub mod tls13 { /// Draft RFC /// 7.3. Authenticated Encryption with Associated Data (AEAD) Functions #[allow(non_camel_case_types)] + #[derive(Deserialize, Serialize)] pub enum HpkeAeadId { Reserved = 0, AES_128_GCM = 1, @@ -324,7 +365,7 @@ pub mod tls13 { } } - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct HpkeSymmetricCipherSuite { pub kdf_id: HpkeKdfId, pub aead_id: HpkeAeadId, @@ -339,7 +380,7 @@ pub mod tls13 { } } - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct HpkeKeyConfig { pub config_id: u8, pub kem_id: HpkeKemId, @@ -383,7 +424,7 @@ pub mod tls13 { // opaque!(pub struct HpkePublicKey<1, {u16::MAX}>); pub type HpkePublicKey = Opaque<1, { u16::MAX }>; - #[derive(Debug, Clone, PartialEq)] + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] pub struct SupportedVersions { // min length 2 (otherwise implied by TLS version field), max length 254 // len is a u8 i suppose? @@ -391,6 +432,7 @@ pub mod tls13 { } u16_enum! { + #[derive(Deserialize, Serialize)] pub enum TlsVersion { Ssl3_0 = 0x300, Tls1_0 = 0x301, @@ -404,6 +446,7 @@ pub mod tls13 { // opaque!(pub struct Cookie); u16_enum! { + #[derive(Deserialize, Serialize)] pub enum ExtensionType { ServerName = 0, /* RFC 6066 */ MaxFragmentLength = 1, /* RFC 6066 */ @@ -464,19 +507,22 @@ mod test { 0, 0, // extensions len ]; let expected = ECHConfigList { - configs: vec![ECHConfig::EchConfigContents { - key_config: HpkeKeyConfig { - config_id: 63, - kem_id: HpkeKemId::DHKEM_X25519_HKDF_SHA512, - cipher_suites: vec![HpkeSymmetricCipherSuite { - kdf_id: HpkeKdfId::HKDF_SHA256, - aead_id: HpkeAeadId::AES_128_GCM, - }], - public_key: public_key.to_vec().try_into().unwrap(), + configs: vec![ECHConfig { + version: 0xfe0d, + contents: ECHConfigContents::Version0xfe0d { + key_config: HpkeKeyConfig { + config_id: 63, + kem_id: HpkeKemId::DHKEM_X25519_HKDF_SHA512, + cipher_suites: vec![HpkeSymmetricCipherSuite { + kdf_id: HpkeKdfId::HKDF_SHA256, + aead_id: HpkeAeadId::AES_128_GCM, + }], + public_key: public_key.to_vec().try_into().unwrap(), + }, + maximum_name_length: 0, + public_name: PublicName(b"cloudflare-esni.com".to_vec()), + extensions: vec![], }, - maximum_name_length: 0, - public_name: PublicName(b"cloudflare-esni.com".to_vec()), - extensions: vec![], }], }; @@ -497,6 +543,7 @@ mod test { Ok(&expected), ); } + } #[cfg(test)] diff --git a/ech-config/src/serde_with_base64.rs b/ech-config/src/serde_with_base64.rs new file mode 100644 index 0000000..51603e0 --- /dev/null +++ b/ech-config/src/serde_with_base64.rs @@ -0,0 +1,141 @@ +//! De/Serialization of hexadecimal encoded bytes +//! +//! This modules is only available when using the `hex` feature of the crate. + +use serde_with::de::DeserializeAs; +use serde_with::ser::SerializeAs; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serializer}; +use std::borrow::Cow; +use std::convert::{TryFrom, TryInto}; +use std::marker::PhantomData; + +/// Serialize bytes as a base64 string +/// +/// The type serializes a sequence of bytes as a base64 string. +/// It works on any type implementing `AsRef<[u8]>` for serialization and `From>` for deserialization. +/// +/// # Example +/// +/// ```rust +/// # #[cfg(feature = "macros")] { +/// # use serde_derive::{Deserialize, Serialize}; +/// # use serde_json::json; +/// # use serde_with::serde_as; +/// # +/// #[serde_as] +/// # #[derive(Debug, PartialEq, Eq)] +/// #[derive(Deserialize, Serialize)] +/// struct BytesLowercase( +/// // Equivalent to serde_with_base64::Base64 +/// #[serde_as(as = "serde_with_base64::Base64")] +/// Vec +/// ); +/// +/// #[serde_as] +/// # #[derive(Debug, PartialEq, Eq)] +/// #[derive(Deserialize, Serialize)] +/// struct BytesUppercase( +/// #[serde_as(as = "serde_with_base64::Base64")] +/// Vec +/// ); +/// +/// let b = b"Hello World!"; +/// +/// // Base64 with lowercase letters +/// assert_eq!( +/// json!("48656c6c6f20576f726c6421"), +/// serde_json::to_value(BytesLowercase(b.to_vec())).unwrap() +/// ); +/// // Base64 with uppercase letters +/// assert_eq!( +/// json!("48656C6C6F20576F726C6421"), +/// serde_json::to_value(BytesUppercase(b.to_vec())).unwrap() +/// ); +/// +/// // Serialization always work from lower- and uppercase characters, even mixed case. +/// assert_eq!( +/// BytesLowercase(vec![0x00, 0xaa, 0xbc, 0x99, 0xff]), +/// serde_json::from_value(json!("00aAbc99FF")).unwrap() +/// ); +/// assert_eq!( +/// BytesUppercase(vec![0x00, 0xaa, 0xbc, 0x99, 0xff]), +/// serde_json::from_value(json!("00aAbc99FF")).unwrap() +/// ); +/// +/// ///////////////////////////////////// +/// // Arrays are supported in Rust 1.48+ +/// +/// # #[rustversion::since(1.48)] +/// # fn test_array() { +/// #[serde_as] +/// # #[derive(Debug, PartialEq, Eq)] +/// #[derive(Deserialize, Serialize)] +/// struct ByteArray( +/// #[serde_as(as = "serde_with_base64::Base64")] +/// [u8; 12] +/// ); +/// +/// let b = b"Hello World!"; +/// +/// assert_eq!( +/// json!("48656c6c6f20576f726c6421"), +/// serde_json::to_value(ByteArray(b.clone())).unwrap() +/// ); +/// +/// // Serialization always work from lower- and uppercase characters, even mixed case. +/// assert_eq!( +/// ByteArray([0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0xaa, 0xbc, 0x99, 0xff]), +/// serde_json::from_value(json!("0011223344556677aAbc99FF")).unwrap() +/// ); +/// +/// // Remember that the conversion may fail. (The following errors are specific to fixed-size arrays) +/// let error_result: Result = serde_json::from_value(json!("42")); // Too short +/// error_result.unwrap_err(); +/// +/// let error_result: Result = +/// serde_json::from_value(json!("000000000000000000000000000000")); // Too long +/// error_result.unwrap_err(); +/// # }; +/// # #[rustversion::before(1.48)] +/// # fn test_array() {} +/// # test_array(); +/// # } +/// ``` +#[derive(Copy, Clone, Debug, Default)] +pub struct Base64(PhantomData<()>); + +impl SerializeAs for Base64 +where + T: AsRef<[u8]>, +{ + fn serialize_as(source: &T, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&base64::encode(source)) + } +} + +impl<'de, T> DeserializeAs<'de, T> for Base64 +where + T: TryFrom>, +{ + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + as Deserialize<'de>>::deserialize(deserializer) + .and_then(|s| base64::decode(&*s).map_err(Error::custom)) + .and_then(|vec: Vec| { + let length = vec.len(); + vec.try_into().map_err(|_e: T::Error| { + Error::custom(format!( + "Can't convert a Byte Vector of length {} to the output type.", + length + )) + }) + }) + } +} + From f6c3ec0dbf90039664e3e053a90f01750858a4b3 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 22:06:04 +1000 Subject: [PATCH 20/26] ech: dog => SVCB record => json => base64 => ECHConfigList => json cargo run -q -p dog -- HTTPS crypto.cloudflare.com --json \ | jq '.responses[] .answers[].data.params.ech' -r \ | cargo run -q -p ech-config \ | jq --- Cargo.lock | 1 + Cargo.toml | 1 + dns/src/record/mod.rs | 4 ++ dns/src/record/svcb_https.rs | 128 +++++++++++++++++++---------------- dns/src/value_list.rs | 4 ++ ech-config/bin/ech_config.rs | 25 +++++-- src/output.rs | 32 ++++++++- 7 files changed, 128 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6c91a4..a65cd20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,7 @@ version = "0.2.0-pre" dependencies = [ "ansi_term", "atty", + "base64", "datetime", "dns", "dns-transport", diff --git a/Cargo.toml b/Cargo.toml index 238e6a5..1ee3c5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ json = "0.12" # logging log = "0.4" +base64 = "0.13.0" # windows default nameserver determination [target.'cfg(windows)'.dependencies] diff --git a/dns/src/record/mod.rs b/dns/src/record/mod.rs index 0a6f34c..30551fe 100644 --- a/dns/src/record/mod.rs +++ b/dns/src/record/mod.rs @@ -55,6 +55,10 @@ pub use self::srv::SRV; mod svcb_https; pub use self::svcb_https::{HTTPS, SVCB}; +/// Helper objects for the [SVCB] record +pub mod svcb { + pub use super::svcb_https::{Alpn, AlpnId, Opaque, SvcParam, SvcParams,}; +} mod tlsa; pub use self::tlsa::TLSA; diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 9739d37..811cc03 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -124,8 +124,9 @@ macro_rules! u16_enum { } // TODO: reimplement debug and use ... to truncate (base64?) output +/// An opaque piece of data, e.g. [SvcParams::ech] #[derive(Debug, Clone, PartialEq)] -pub struct Opaque(/* u16 len */ Vec); +pub struct Opaque(Vec); impl From> for Opaque { fn from(vec: Vec) -> Self { @@ -141,7 +142,7 @@ impl fmt::Display for Opaque { /// Same as [Opaque] but min length is 1 #[derive(Debug, Clone, PartialEq)] -pub struct Opaque1(/* u16 len */ Vec); +pub(crate) struct Opaque1(Vec); trait ReadFromCursor: Sized { fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result; @@ -173,24 +174,6 @@ impl ReadFromCursor for Opaque1 { } } -macro_rules! opaque { - ($vis:vis struct $ident:ident) => { - #[derive(Debug, Clone, PartialEq)] - $vis struct $ident($crate::record::svcb_https::Opaque); - impl $crate::record::svcb_https::ReadFromCursor for $ident { - fn read_from(cursor: &mut std::io::Cursor<&[u8]>) -> std::io::Result { - $crate::record::svcb_https::Opaque::read_from(cursor).map(Self) - } - } - - impl From> for $ident { - fn from(vec: Vec) -> Self { - Self(From::from(vec)) - } - } - } -} - /// A **SVCB** (*service binding*) record, which holds information needed to make connections to /// network services, such as for HTTPS origins. /// @@ -206,7 +189,8 @@ pub struct SVCB { /// The domain name of either the alias target (for AliasMode) or the alternative endpoint (for /// ServiceMode). pub target: Labels, - parameters: Option, + /// The SvcParams + pub params: Option, } /// An **HTTPS** record, which is the HTTPS incarnation of **SVCB**. @@ -217,6 +201,7 @@ pub struct HTTPS { } impl HTTPS { + /// Constructor pub fn new(svcb: SVCB) -> Self { Self { svcb } } @@ -225,18 +210,25 @@ impl HTTPS { u16_enum! { /// 14.3.2. Initial contents (subject to IANA additions) #[derive(Copy, Eq, PartialOrd, Ord, Hash)] - enum SvcParam { + pub enum SvcParam { /// `mandatory` Mandatory = 0, /// `alpn` Alpn = 1, /// `no-default-alpn` NoDefaultAlpn = 2, + /// `port` Port = 3, + /// `ipv4hint` Ipv4Hint = 4, + /// `ech` Ech = 5, + /// `ipv6hint` Ipv6Hint = 6, - @unknown KeyNNNNN(u16), + @unknown + /// `keyNNNNN` + KeyNNNNN(u16), + /// Invalid. InvalidKey = 65535, } } @@ -262,33 +254,43 @@ impl fmt::Display for SvcParam { } } +/// The SvcParams section of a [SVCB] record #[derive(Debug, Clone, PartialEq, Default)] -struct SvcParams { +pub struct SvcParams { /// List of keys that must be understood by a client to use the RR properly. /// /// Wire format: list of u16 network endian svcparam values - /// Presentation format: a comma-separated [ValueList] - mandatory: Vec, + /// Presentation format: a comma-separated [crate::value_list::ValueList] + pub mandatory: Vec, /// Draft 7 section 6.1 /// /// Wire format: /// Presentation format: comma-separated list of alpn-id, 1-255 characters each /// (also, "Zone file implementations MAY disallow" commas/backslash escapes, use \002 (ascii /// 0x02 STX (start text) character). That's a TODO - alpn: Option, - port: Option, - ipv4hint: Vec, + pub alpn: Option, + /// > The "port" SvcParamKey defines the TCP or UDP port that should be used to reach this + /// alternative endpoint. If this key is not present, clients SHALL use the authority + /// endpoint's port number. + pub port: Option, + /// > The "ipv4hint" and "ipv6hint" keys convey IP addresses that clients MAY use to reach the + /// service. If A and AAAA records for TargetName are locally available, the client SHOULD + /// ignore these hints. + pub ipv4hint: Vec, /// An ECHConfigList from the [ECH RFC][ech-rfc] /// /// [ech-rfc]: https://datatracker.ietf.org/doc/draft-ietf-tls-esni/13/ /// - /// Wire format, the value of the parameter is an ECHConfigList [ECH], including the redundant length prefix. - /// Presentation format, the value is a single ECHConfigList encoded in Base64 [base64]. - ech: Option>, - ipv6hint: Vec, + /// Wire format: the value of the parameter is an ECHConfigList, including the redundant length prefix. + /// Presentation format: the value is a single ECHConfigList encoded in Base64. + pub ech: Option>, + /// > The "ipv4hint" and "ipv6hint" keys convey IP addresses that clients MAY use to reach the + /// service. If A and AAAA records for TargetName are locally available, the client SHOULD + /// ignore these hints. + pub ipv6hint: Vec, /// For any unrecognised keys. BTreeMap, because keys are sorted this way - other: BTreeMap, + pub other: BTreeMap, } impl fmt::Display for SvcParams { @@ -311,7 +313,7 @@ impl fmt::Display for SvcParams { } if let Some(alpn) = alpn { f.write_str(" alpn=")?; - encoding::EscapeValueList(alpn.alpn_ids.iter().map(|id| id.0.as_slice())).fmt(f)?; + encoding::EscapeValueList(alpn.ids.iter().map(|id| id.0.as_slice())).fmt(f)?; if alpn.no_default_alpn { write!(f, " no-default-alpn")?; } @@ -428,7 +430,7 @@ impl SvcParams { None } else { Some(Alpn { - alpn_ids, + ids: alpn_ids, no_default_alpn, }) }; @@ -460,15 +462,20 @@ fn read_convert>( Ok(collector) } +/// The ALPN configuration, covering the `alpn` and `no-default-alpn` parameters. #[derive(Debug, Clone, PartialEq)] -struct Alpn { - alpn_ids: Vec, - no_default_alpn: bool, +pub struct Alpn { + /// The `alpn` field + pub ids: Vec, + /// The `no-default-alpn` field + /// + /// > To determine the set of protocol suites supported by an endpoint (the "SVCB ALPN set"), the client adds the default set to the list of alpn-ids unless the "no-default-alpn" SvcParamKey is present. + pub no_default_alpn: bool, } +/// An ALPN id, like "h2" or "h3-19" #[derive(Clone, PartialEq)] -#[repr(transparent)] -struct AlpnId(Vec); +pub struct AlpnId(Vec); impl From<&str> for AlpnId { fn from(s: &str) -> Self { @@ -487,6 +494,13 @@ impl ReadFromCursor for AlpnId { } } +impl fmt::Display for AlpnId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = &self.0[..]; + String::from_utf8_lossy(bytes).fmt(f) + } +} + impl fmt::Debug for AlpnId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bytes = &self.0[..]; @@ -532,7 +546,7 @@ impl Wire for SVCB { let ret = Self { priority, target, - parameters, + params: parameters, }; Ok(ret) }, @@ -565,7 +579,7 @@ impl fmt::Display for SVCB { let Self { priority, target, - parameters, + params: parameters, } = self; write!(f, "{} {}", priority, target)?; @@ -623,10 +637,10 @@ mod test { HTTPS::new(SVCB { priority: 1, target: Labels::root(), - parameters: Some(SvcParams { + params: Some(SvcParams { mandatory: vec![], alpn: Some(Alpn { - alpn_ids: vec![ + ids: vec![ "h3".into(), "h3-29".into(), "h3-28".into(), @@ -696,7 +710,7 @@ mod test { Ok(SVCB { priority: 0, target: Labels::root(), - parameters: None, + params: None, }) ); } @@ -733,7 +747,7 @@ mod test_vectors { let value = SVCB { priority: 0, target: Labels::encode("foo.example.com").unwrap(), - parameters: None, + params: None, }; assert_eq!( SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), @@ -749,7 +763,7 @@ mod test_vectors { let value = SVCB { priority: 1, target: Labels::encode(".").unwrap(), - parameters: Some(SvcParams::default()), + params: Some(SvcParams::default()), }; assert_eq!( SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), @@ -772,7 +786,7 @@ mod test_vectors { let value = SVCB { priority: 16, target: Labels::encode("foo.example.com.").unwrap(), - parameters: Some(SvcParams { + params: Some(SvcParams { port: Some(53), ..SvcParams::default() }), @@ -799,7 +813,7 @@ mod test_vectors { let value = SVCB { priority: 1, target: Labels::encode("foo.example.com.").unwrap(), - parameters: Some(SvcParams { + params: Some(SvcParams { other: { let mut map = BTreeMap::new(); map.insert( @@ -832,7 +846,7 @@ mod test_vectors { let value = SVCB { priority: 1, target: Labels::encode("foo.example.com.").unwrap(), - parameters: Some(SvcParams { + params: Some(SvcParams { other: { let mut map = BTreeMap::new(); map.insert( @@ -873,7 +887,7 @@ mod test_vectors { let value = SVCB { priority: 1, target: Labels::encode("foo.example.com.").unwrap(), - parameters: Some(SvcParams { + params: Some(SvcParams { ipv6hint: vec![ "2001:db8::1".parse().unwrap(), "2001:db8::53:1".parse().unwrap(), @@ -908,7 +922,7 @@ mod test_vectors { let value = SVCB { priority: 1, target: Labels::encode("foo.example.com.").unwrap(), - parameters: Some(SvcParams { + params: Some(SvcParams { ipv6hint: vec!["::ffff:198.51.100.100".parse().unwrap()], ..Default::default() }), @@ -949,10 +963,10 @@ mod test_vectors { let value = SVCB { priority: 16, target: Labels::encode("foo.example.org.").unwrap(), - parameters: Some(SvcParams { + params: Some(SvcParams { mandatory: vec![SvcParam::Alpn, SvcParam::Ipv4Hint], alpn: Some(Alpn { - alpn_ids: vec!["h2".into(), "h3-19".into()], + ids: vec!["h2".into(), "h3-19".into()], no_default_alpn: false, }), ipv4hint: vec!["192.0.2.1".parse().unwrap()], @@ -986,11 +1000,11 @@ mod test_vectors { let value = SVCB { priority: 16, target: Labels::encode("foo.example.org.").unwrap(), - parameters: Some(SvcParams { + params: Some(SvcParams { alpn: Some(Alpn { // here, it's a single \ because there's only one 0x5c and only a single 0x2c // comma, neither of which need escaping in binary - alpn_ids: vec![r"f\oo,bar".into(), "h2".into()], + ids: vec![r"f\oo,bar".into(), "h2".into()], no_default_alpn: false, }), ..Default::default() diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index 4231f6b..1276192 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -13,6 +13,7 @@ use std::iter::FromIterator; /// [Draft RFC](https://tools.ietf.org/id/draft-ietf-dnsop-svcb-https-02.html#name-the-svcb-record-type), section A.1 #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct ValueList { + /// The parsed values pub values: Vec>, } @@ -32,6 +33,7 @@ impl>> From> for ValueList { /// Nice #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] pub struct SingleValue { + /// The value pub value: Vec, } @@ -48,6 +50,7 @@ fn wrap_iresult_complete(result: IResult<&[u8], T>) -> Result, @@ -132,6 +135,7 @@ fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { }) } +/// Tools for encoding and decoding value-list and char-string pub mod encoding { use super::*; diff --git a/ech-config/bin/ech_config.rs b/ech-config/bin/ech_config.rs index a86bce0..6e95cdb 100644 --- a/ech-config/bin/ech_config.rs +++ b/ech-config/bin/ech_config.rs @@ -1,12 +1,23 @@ use ech_config::ECHConfigList; use serde_json; +use std::io::{self, Read}; + fn main() { - let base = "AEb+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"; - let configs = ECHConfigList::from_base64(base).unwrap(); - println!("{}", serde_json::to_string_pretty(&configs).unwrap()); - let unknown_version = "AEc+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"; - let configs = ECHConfigList::from_base64(unknown_version).unwrap(); - println!("{}", serde_json::to_string_pretty(&configs).unwrap()); - assert!(false); + let stdin = io::stdin(); + let stdout = io::stdout(); + let mut buf = String::new(); + stdin.lock().take(u16::MAX.into()).read_to_string(&mut buf).unwrap(); + let configs = ECHConfigList::from_base64(&buf.trim()).unwrap(); + serde_json::to_writer(stdout.lock(), &configs).unwrap(); + + // println!("{}", serde_json::to_string_pretty(&configs).unwrap()); + // + // let base = "AEb+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"; + // let configs = ECHConfigList::from_base64(base).unwrap(); + // println!("{}", serde_json::to_string_pretty(&configs).unwrap()); + // let unknown_version = "AEc+DQBCPwAgACAoJhkM1Ki3KtogKZosPZiIg3JWb8JCmnLnqs1TSGlpdwAEAAEAAQATY2xvdWRmbGFyZS1lc25pLmNvbQAA"; + // let configs = ECHConfigList::from_base64(unknown_version).unwrap(); + // println!("{}", serde_json::to_string_pretty(&configs).unwrap()); + // assert!(false); } diff --git a/src/output.rs b/src/output.rs index 9c6cb88..fa9e11a 100644 --- a/src/output.rs +++ b/src/output.rs @@ -4,7 +4,7 @@ use std::fmt; use std::time::Duration; use dns::{Response, Query, Answer, QClass, ErrorCode, WireError, MandatedLength}; -use dns::record::{Record, RecordType, UnknownQtype, OPT}; +use dns::record::{OPT, Record, RecordType, SVCB, UnknownQtype}; use dns_transport::Error as TransportError; use json::{object, JsonValue}; @@ -599,8 +599,34 @@ fn json_record_data(record: Record) -> JsonValue { "bytes": bytes, } } - Record::HTTPS(_https) => todo!(), - Record::SVCB(_svcb) => todo!(), + Record::HTTPS(ref https) => svcb_json(&https.svcb), + Record::SVCB(ref svcb) => svcb_json(svcb), + } +} + +fn svcb_json(svcb: &SVCB) -> JsonValue { + use dns::record::svcb::*; + let SVCB { priority, target, params } = svcb; + let params = params.as_ref().map(|params| { + let SvcParams { mandatory, alpn, port, ipv4hint, ech, ipv6hint, other } = params; + let mut obj = object! { + "mandatory": mandatory.iter().map(|x| x.to_string()).collect::>(), + "alpn": alpn.as_ref().map(|x| x.ids.iter().map(|id| id.to_string()).collect::>()), + "port": *port, + "no-default-alpn": alpn.as_ref().map(|x| x.no_default_alpn).unwrap_or(false), + "ipv4hint": ipv4hint.iter().map(|x| x.to_string()).collect::>(), + "ech": ech.as_ref().map(|x| base64::encode(x)), + "ipv6hint": ipv6hint.iter().map(|x| x.to_string()).collect::>(), + }; + other.iter().map(|(k, v)| (k.to_string(), v.to_string())).for_each(|(k, v)| { + obj[k] = v.into(); + }); + obj + }); + object! { + "priority": *priority, + "target": target.to_string(), + "params": params, } } From fb37b8a69018ebc16417cf776170093aae76c15e Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 22:16:35 +1000 Subject: [PATCH 21/26] Change `ech` param to use Opaque; improve opaque --- dns/src/record/svcb_https.rs | 65 ++++++++++++++++++------------------ src/output.rs | 2 +- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index 811cc03..b7802f1 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -4,6 +4,7 @@ use core::fmt; use std::collections::BTreeMap; use std::io::{self, Seek, SeekFrom}; use std::net::{Ipv4Addr, Ipv6Addr}; +use std::ops::RangeInclusive; use log::*; @@ -136,41 +137,46 @@ impl From> for Opaque { impl fmt::Display for Opaque { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - encoding::EscapeCharString(&self.0).fmt(f) + base64::display::Base64Display::with_config(&self.0, base64::STANDARD).fmt(f) } } -/// Same as [Opaque] but min length is 1 -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct Opaque1(Vec); - trait ReadFromCursor: Sized { fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result; } impl ReadFromCursor for Opaque { fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { - let len = cursor.read_u16::()?; - log::trace!("read opaque length = {}", len); - let mut vec = vec![0u8; usize::from(len)]; - cursor.read_exact(&mut vec[..])?; - Ok(Opaque(vec)) + read_vec(cursor, 0..=u16::MAX).map(Self) } } -impl ReadFromCursor for Opaque1 { - fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { - let len = cursor.read_u16::()?; - log::trace!("read opaque1 length = {}", len); - if len == 0 { - return Err(io::Error::new( - io::ErrorKind::Other, - "length of opaque field was zero, but must be at least 1", - )); - } - let mut vec = vec![0u8; usize::from(len)]; - cursor.read_exact(&mut vec[..])?; - Ok(Opaque1(vec)) +fn read_vec_of_len( + cursor: &mut Cursor<&[u8]>, + limit: RangeInclusive, + len: u16, +) -> io::Result> { + if !limit.contains(&len) { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("invalid length {}: must be within {:?}", len, limit), + )); + } + let mut vec = vec![0u8; usize::from(len)]; + cursor.read_exact(&mut vec[..])?; + Ok(vec) +} + +fn read_vec(cursor: &mut Cursor<&[u8]>, limit: RangeInclusive) -> io::Result> { + let len = cursor.read_u16::()?; + log::trace!("read opaque length = {}", len); + read_vec_of_len(cursor, limit, len) +} + +impl Opaque { + pub(crate) fn read_known_len(cursor: &mut Cursor<&[u8]>, len: u16) -> io::Result { + let vec = read_vec_of_len(cursor, 0..=u16::MAX, len)?; + Ok(Self(vec)) } } @@ -283,7 +289,7 @@ pub struct SvcParams { /// /// Wire format: the value of the parameter is an ECHConfigList, including the redundant length prefix. /// Presentation format: the value is a single ECHConfigList encoded in Base64. - pub ech: Option>, + pub ech: Option, /// > The "ipv4hint" and "ipv6hint" keys convey IP addresses that clients MAY use to reach the /// service. If A and AAAA records for TargetName are locally available, the client SHOULD /// ignore these hints. @@ -325,11 +331,7 @@ impl fmt::Display for SvcParams { write!(f, " ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; } if let Some(ech) = ech { - write!( - f, - " ech={}", - base64::display::Base64Display::with_config(ech, base64::STANDARD) - )?; + write!(f, " ech={}", ech)?; } if !ipv6hint.is_empty() { write!(f, " ipv6hint={}", display_utils::join(ipv6hint.iter(), ","))?; @@ -386,9 +388,8 @@ impl SvcParams { ipv4hint = read_convert(cursor, len_hint, |c| c.read_u32::())?; } SvcParam::Ech => { - let mut vec = vec![0u8; len_hint]; - cursor.read_exact(&mut vec)?; - ech = Some(vec); + let opaque = Opaque::read_known_len(cursor, param_length)?; + ech = Some(opaque); } SvcParam::Ipv6Hint => { ipv6hint = read_convert(cursor, len_hint, |c| c.read_u128::())?; diff --git a/src/output.rs b/src/output.rs index fa9e11a..527ad67 100644 --- a/src/output.rs +++ b/src/output.rs @@ -615,7 +615,7 @@ fn svcb_json(svcb: &SVCB) -> JsonValue { "port": *port, "no-default-alpn": alpn.as_ref().map(|x| x.no_default_alpn).unwrap_or(false), "ipv4hint": ipv4hint.iter().map(|x| x.to_string()).collect::>(), - "ech": ech.as_ref().map(|x| base64::encode(x)), + "ech": ech.as_ref().map(|x| x.to_string()), "ipv6hint": ipv6hint.iter().map(|x| x.to_string()).collect::>(), }; other.iter().map(|(k, v)| (k.to_string(), v.to_string())).for_each(|(k, v)| { From b45b49b44774b867b2fe278f934fb88ad84ddd14 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 22:24:47 +1000 Subject: [PATCH 22/26] a little doc --- dns/src/record/svcb_https.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index b7802f1..c2581ad 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -448,6 +448,7 @@ impl SvcParams { } } +/// Read a bunch of specific endian integers and convert them to an enum, for example fn read_convert>( cursor: &mut Cursor<&[u8]>, len: usize, From a8d67ed6a8d5d29bf73720284ec8c90d85a684c5 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 22:51:17 +1000 Subject: [PATCH 23/26] Remove value-list encoding stuff from public API --- dns/Cargo.toml | 2 +- dns/src/lib.rs | 1 - dns/src/record/svcb_https.rs | 3 +-- dns/src/value_list.rs | 14 +++++++------- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/dns/Cargo.toml b/dns/Cargo.toml index 393a876..a53e3ec 100644 --- a/dns/Cargo.toml +++ b/dns/Cargo.toml @@ -19,13 +19,13 @@ nom = "7.0.0" # printing of certain packets base64 = "0.13.0" +display_utils = "0.4.0" # idna encoding unic-idna = { version = "0.9.0", optional = true } # mutation testing mutagen = { git = "https://github.com/llogiq/mutagen", optional = true } -display_utils = "0.4.0" [dev-dependencies] pretty_assertions = "0.7" diff --git a/dns/src/lib.rs b/dns/src/lib.rs index e90431b..dfbb5b1 100644 --- a/dns/src/lib.rs +++ b/dns/src/lib.rs @@ -38,7 +38,6 @@ mod strings; pub use self::strings::Labels; mod value_list; -pub use self::value_list::encoding; mod wire; pub use self::wire::{Wire, WireError, MandatedLength}; diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index c2581ad..c3b4948 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -124,7 +124,6 @@ macro_rules! u16_enum { }; } -// TODO: reimplement debug and use ... to truncate (base64?) output /// An opaque piece of data, e.g. [SvcParams::ech] #[derive(Debug, Clone, PartialEq)] pub struct Opaque(Vec); @@ -266,7 +265,7 @@ pub struct SvcParams { /// List of keys that must be understood by a client to use the RR properly. /// /// Wire format: list of u16 network endian svcparam values - /// Presentation format: a comma-separated [crate::value_list::ValueList] + /// Presentation format: a comma-separated value-list pub mandatory: Vec, /// Draft 7 section 6.1 /// diff --git a/dns/src/value_list.rs b/dns/src/value_list.rs index 1276192..7eefd98 100644 --- a/dns/src/value_list.rs +++ b/dns/src/value_list.rs @@ -5,14 +5,15 @@ use std::borrow::Cow; use std::iter::FromIterator; /// Parameters to a SVCB/HTTPS record can be multi-valued. +/// /// This is a fancy comma-separated list, where escaped commas \, and \044 do not separate /// values. /// /// # References: /// -/// [Draft RFC](https://tools.ietf.org/id/draft-ietf-dnsop-svcb-https-02.html#name-the-svcb-record-type), section A.1 +/// [Draft RFC](https://tools.ietf.org/id/draft-ietf-dnsop-svcb-https-07.html), section A.1 #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] -pub struct ValueList { +pub(crate) struct ValueList { /// The parsed values pub values: Vec>, } @@ -30,9 +31,8 @@ impl>> From> for ValueList { } } -/// Nice #[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] -pub struct SingleValue { +pub(crate) struct SingleValue { /// The value pub value: Vec, } @@ -136,12 +136,12 @@ fn strings(x: IResult<&[u8], Vec>) -> IResult<&str, String> { } /// Tools for encoding and decoding value-list and char-string -pub mod encoding { +pub(crate) mod encoding { use super::*; pub use super::DecodingError; - pub use super::SingleValue; - pub use super::ValueList; + // pub use super::SingleValue; + // pub use super::ValueList; /// Iterates a parser over an input. Expects it does not fail or return Incomplete. It stops /// parsing when the parser returns nom::Err::Error (which should occur at Eof). From 3eaedfa49e8a0456bfc192e18344c73f3a48906f Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 23:00:53 +1000 Subject: [PATCH 24/26] Move some code out of svcb_https --- dns/src/record/mod.rs | 7 +- dns/src/record/svcb_https.rs | 192 +---------------------------------- dns/src/record/utils.rs | 188 ++++++++++++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 189 deletions(-) create mode 100644 dns/src/record/utils.rs diff --git a/dns/src/record/mod.rs b/dns/src/record/mod.rs index 30551fe..dec27ba 100644 --- a/dns/src/record/mod.rs +++ b/dns/src/record/mod.rs @@ -2,6 +2,9 @@ use crate::wire::*; +#[macro_use] +mod utils; + mod a; pub use self::a::A; @@ -57,9 +60,11 @@ mod svcb_https; pub use self::svcb_https::{HTTPS, SVCB}; /// Helper objects for the [SVCB] record pub mod svcb { - pub use super::svcb_https::{Alpn, AlpnId, Opaque, SvcParam, SvcParams,}; + pub use super::svcb_https::{Alpn, AlpnId, SvcParam, SvcParams}; } +pub use utils::Opaque; + mod tlsa; pub use self::tlsa::TLSA; diff --git a/dns/src/record/svcb_https.rs b/dns/src/record/svcb_https.rs index c3b4948..adab1b4 100644 --- a/dns/src/record/svcb_https.rs +++ b/dns/src/record/svcb_https.rs @@ -1,183 +1,15 @@ //! The format of both SVCB and HTTPS RRs is identical. use core::fmt; +use log::*; use std::collections::BTreeMap; -use std::io::{self, Seek, SeekFrom}; +use std::io; use std::net::{Ipv4Addr, Ipv6Addr}; -use std::ops::RangeInclusive; - -use log::*; +use crate::record::utils::{read_convert, CursorExt, Opaque, ReadFromCursor}; use crate::strings::{Labels, ReadLabels}; -use crate::wire::*; - use crate::value_list::encoding; - -/// A kinda hacky but alright way to avoid copying tons of data -trait CursorExt { - /// Replace this when #[feature(cursor_remaining)] is stabilised - fn std_remaining_slice(&self) -> &[u8]; - - /// Convenience - fn truncated(&self, length: u64) -> Self; - fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T; -} - -impl CursorExt for Cursor<&[u8]> { - fn std_remaining_slice(&self) -> &[u8] { - let inner = self.get_ref(); - let len = self.position().min(inner.as_ref().len() as u64); - &inner[(len as usize)..] - } - fn truncated(&self, to_length: u64) -> Self { - let inner = self.get_ref(); - let len = inner.len() as u64; - let start = self.position().min(len); - let end = (start + to_length).min(len); - let trunc = &inner[(start as usize)..(end as usize)]; - Cursor::new(trunc) - } - fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T { - let mut trunc = self.truncated(length); - let len_hint = trunc.get_ref().len(); - let ret = f(&mut trunc, len_hint); - self.seek(SeekFrom::Current(trunc.position() as i64)) - .unwrap(); - ret - } -} - -macro_rules! u16_enum { - { - $(#[$attr:meta])* - $vis:vis enum $name:ident { - $( - $(#[$vattr:meta])* - $variant:ident = $lit:literal,)+ - } - } => { - $(#[$attr])* - #[derive(Debug, Clone, PartialEq)] - #[repr(u16)] - $vis enum $name { - $( - $(#[$vattr])* - $variant = $lit,)+ - } - impl core::convert::TryFrom for $name { - type Error = std::io::Error; - - fn try_from(int: u16) -> Result { - match int { - $($lit => Ok(Self::$variant),)+ - _ => Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("invalid value for {}: {:04x}", stringify!($name), int) - ) - ) - } - } - } - }; - { - $(#[$attr:meta])* - $vis:vis enum $name:ident { - $( - $(#[$vattr:meta])* - $variant:ident = $lit:literal,)+ - $( - @unknown - $(#[$uattr:meta])* - $unknown:ident (u16), - $( - $(#[$vattr2:meta])* - $variant2:ident = $lit2:literal,)* - )? - } - } => { - $(#[$attr])* - #[derive(Debug, Clone, PartialEq)] - #[repr(u16)] - $vis enum $name { - $( - $(#[$vattr])* - $variant = $lit,)+ - $( - $(#[$uattr])* - $unknown(u16), - $( - $(#[$vattr2])* - $variant2 = $lit2,)* - )? - } - impl From for $name { - fn from(int: u16) -> Self { - match int { - $($lit => Self::$variant,)+ - $( - $($lit2 => Self::$variant2,)* - _ => Self::$unknown(int), - )? - } - } - } - }; -} - -/// An opaque piece of data, e.g. [SvcParams::ech] -#[derive(Debug, Clone, PartialEq)] -pub struct Opaque(Vec); - -impl From> for Opaque { - fn from(vec: Vec) -> Self { - Self(vec) - } -} - -impl fmt::Display for Opaque { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - base64::display::Base64Display::with_config(&self.0, base64::STANDARD).fmt(f) - } -} - -trait ReadFromCursor: Sized { - fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result; -} - -impl ReadFromCursor for Opaque { - fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { - read_vec(cursor, 0..=u16::MAX).map(Self) - } -} - -fn read_vec_of_len( - cursor: &mut Cursor<&[u8]>, - limit: RangeInclusive, - len: u16, -) -> io::Result> { - if !limit.contains(&len) { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("invalid length {}: must be within {:?}", len, limit), - )); - } - let mut vec = vec![0u8; usize::from(len)]; - cursor.read_exact(&mut vec[..])?; - Ok(vec) -} - -fn read_vec(cursor: &mut Cursor<&[u8]>, limit: RangeInclusive) -> io::Result> { - let len = cursor.read_u16::()?; - log::trace!("read opaque length = {}", len); - read_vec_of_len(cursor, limit, len) -} - -impl Opaque { - pub(crate) fn read_known_len(cursor: &mut Cursor<&[u8]>, len: u16) -> io::Result { - let vec = read_vec_of_len(cursor, 0..=u16::MAX, len)?; - Ok(Self(vec)) - } -} +use crate::wire::*; /// A **SVCB** (*service binding*) record, which holds information needed to make connections to /// network services, such as for HTTPS origins. @@ -447,22 +279,6 @@ impl SvcParams { } } -/// Read a bunch of specific endian integers and convert them to an enum, for example -fn read_convert>( - cursor: &mut Cursor<&[u8]>, - len: usize, - mut f: impl FnMut(&mut Cursor<&[u8]>) -> io::Result, -) -> Result, WireError> { - let size = core::mem::size_of::(); - if len % size != 0 { - return Err(WireError::IO); - } - let mut collector = Vec::with_capacity(len / size); - let reader = core::iter::from_fn(|| f(cursor).ok().map(Nice::from)); - collector.extend(reader); - Ok(collector) -} - /// The ALPN configuration, covering the `alpn` and `no-default-alpn` parameters. #[derive(Debug, Clone, PartialEq)] pub struct Alpn { diff --git a/dns/src/record/utils.rs b/dns/src/record/utils.rs new file mode 100644 index 0000000..c2701a3 --- /dev/null +++ b/dns/src/record/utils.rs @@ -0,0 +1,188 @@ +use std::fmt; +use std::io::{self, Cursor, Read, Seek, SeekFrom}; +use std::ops::RangeInclusive; + +use crate::wire::*; + +/// A kinda hacky but alright way to avoid copying tons of data +pub(crate) trait CursorExt { + /// Replace this when #[feature(cursor_remaining)] is stabilised + fn std_remaining_slice(&self) -> &[u8]; + + /// Convenience + fn truncated(&self, length: u64) -> Self; + fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T; +} + +impl CursorExt for Cursor<&[u8]> { + fn std_remaining_slice(&self) -> &[u8] { + let inner = self.get_ref(); + let len = self.position().min(inner.as_ref().len() as u64); + &inner[(len as usize)..] + } + fn truncated(&self, to_length: u64) -> Self { + let inner = self.get_ref(); + let len = inner.len() as u64; + let start = self.position().min(len); + let end = (start + to_length).min(len); + let trunc = &inner[(start as usize)..(end as usize)]; + Cursor::new(trunc) + } + fn with_truncated(&mut self, length: u64, f: impl FnOnce(&mut Self, usize) -> T) -> T { + let mut trunc = self.truncated(length); + let len_hint = trunc.get_ref().len(); + let ret = f(&mut trunc, len_hint); + self.seek(SeekFrom::Current(trunc.position() as i64)) + .unwrap(); + ret + } +} + +/// Read a bunch of specific endian integers and convert them to an enum, for example +pub(crate) fn read_convert>( + cursor: &mut Cursor<&[u8]>, + len: usize, + mut f: impl FnMut(&mut Cursor<&[u8]>) -> io::Result, +) -> Result, WireError> { + let size = core::mem::size_of::(); + if len % size != 0 { + return Err(WireError::IO); + } + let mut collector = Vec::with_capacity(len / size); + let reader = core::iter::from_fn(|| f(cursor).ok().map(Nice::from)); + collector.extend(reader); + Ok(collector) +} + +/// An opaque piece of data, e.g. [crate::record::svcb::SvcParams::ech] +#[derive(Debug, Clone, PartialEq)] +pub struct Opaque(pub(crate) Vec); + +impl From> for Opaque { + fn from(vec: Vec) -> Self { + Self(vec) + } +} + +impl fmt::Display for Opaque { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + base64::display::Base64Display::with_config(&self.0, base64::STANDARD).fmt(f) + } +} + +pub(crate) trait ReadFromCursor: Sized { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result; +} + +impl ReadFromCursor for Opaque { + fn read_from(cursor: &mut Cursor<&[u8]>) -> io::Result { + read_vec(cursor, 0..=u16::MAX).map(Self) + } +} + +fn read_vec_of_len( + cursor: &mut Cursor<&[u8]>, + limit: RangeInclusive, + len: u16, +) -> io::Result> { + if !limit.contains(&len) { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("invalid length {}: must be within {:?}", len, limit), + )); + } + let mut vec = vec![0u8; usize::from(len)]; + cursor.read_exact(&mut vec[..])?; + Ok(vec) +} + +fn read_vec(cursor: &mut Cursor<&[u8]>, limit: RangeInclusive) -> io::Result> { + let len = cursor.read_u16::()?; + log::trace!("read opaque length = {}", len); + read_vec_of_len(cursor, limit, len) +} + +impl Opaque { + pub(crate) fn read_known_len(cursor: &mut Cursor<&[u8]>, len: u16) -> io::Result { + let vec = read_vec_of_len(cursor, 0..=u16::MAX, len)?; + Ok(Self(vec)) + } +} + +macro_rules! u16_enum { + { + $(#[$attr:meta])* + $vis:vis enum $name:ident { + $( + $(#[$vattr:meta])* + $variant:ident = $lit:literal,)+ + } + } => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + #[repr(u16)] + $vis enum $name { + $( + $(#[$vattr])* + $variant = $lit,)+ + } + impl core::convert::TryFrom for $name { + type Error = std::io::Error; + + fn try_from(int: u16) -> Result { + match int { + $($lit => Ok(Self::$variant),)+ + _ => Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!("invalid value for {}: {:04x}", stringify!($name), int) + ) + ) + } + } + } + }; + { + $(#[$attr:meta])* + $vis:vis enum $name:ident { + $( + $(#[$vattr:meta])* + $variant:ident = $lit:literal,)+ + $( + @unknown + $(#[$uattr:meta])* + $unknown:ident (u16), + $( + $(#[$vattr2:meta])* + $variant2:ident = $lit2:literal,)* + )? + } + } => { + $(#[$attr])* + #[derive(Debug, Clone, PartialEq)] + #[repr(u16)] + $vis enum $name { + $( + $(#[$vattr])* + $variant = $lit,)+ + $( + $(#[$uattr])* + $unknown(u16), + $( + $(#[$vattr2])* + $variant2 = $lit2,)* + )? + } + impl From for $name { + fn from(int: u16) -> Self { + match int { + $($lit => Self::$variant,)+ + $( + $($lit2 => Self::$variant2,)* + _ => Self::$unknown(int), + )? + } + } + } + }; +} + From cc1b5999331890c2a57e17728d6aeb2a234de071 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Mon, 27 Sep 2021 23:01:54 +1000 Subject: [PATCH 25/26] Modify output escape tests to match quoting behaviour --- src/output.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/output.rs b/src/output.rs index 527ad67..f88c208 100644 --- a/src/output.rs +++ b/src/output.rs @@ -760,7 +760,7 @@ mod test { #[test] fn escape_backslashes() { assert_eq!(Ascii(b"\\").to_string(), - "\"\\\\\""); + "\\\\"); } #[test] @@ -772,6 +772,6 @@ mod test { #[test] fn escape_highs() { assert_eq!(Ascii("pâté".as_bytes()).to_string(), - "\"p\\195\\162t\\195\\169\""); + "p\\195\\162t\\195\\169"); } } From 1d4cbd91e8b6dae257b3b93ed2d55f35ff8fc1f1 Mon Sep 17 00:00:00 2001 From: Cormac Relf Date: Tue, 28 Sep 2021 00:12:13 +1000 Subject: [PATCH 26/26] export Opaque at the crate root instead --- dns/src/lib.rs | 2 ++ dns/src/record/mod.rs | 4 +--- dns/src/record/utils.rs | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/dns/src/lib.rs b/dns/src/lib.rs index dfbb5b1..e1782e4 100644 --- a/dns/src/lib.rs +++ b/dns/src/lib.rs @@ -43,3 +43,5 @@ mod wire; pub use self::wire::{Wire, WireError, MandatedLength}; pub mod record; + +pub use record::utils::Opaque; diff --git a/dns/src/record/mod.rs b/dns/src/record/mod.rs index dec27ba..c2914a2 100644 --- a/dns/src/record/mod.rs +++ b/dns/src/record/mod.rs @@ -3,7 +3,7 @@ use crate::wire::*; #[macro_use] -mod utils; +pub(crate) mod utils; mod a; pub use self::a::A; @@ -63,8 +63,6 @@ pub mod svcb { pub use super::svcb_https::{Alpn, AlpnId, SvcParam, SvcParams}; } -pub use utils::Opaque; - mod tlsa; pub use self::tlsa::TLSA; diff --git a/dns/src/record/utils.rs b/dns/src/record/utils.rs index c2701a3..01f9711 100644 --- a/dns/src/record/utils.rs +++ b/dns/src/record/utils.rs @@ -54,9 +54,11 @@ pub(crate) fn read_convert>( Ok(collector) } -/// An opaque piece of data, e.g. [crate::record::svcb::SvcParams::ech] +/// An opaque piece of data, displayed as base64 +/// +/// e.g. [crate::record::svcb::SvcParams::ech] #[derive(Debug, Clone, PartialEq)] -pub struct Opaque(pub(crate) Vec); +pub struct Opaque(pub Vec); impl From> for Opaque { fn from(vec: Vec) -> Self {