diff --git a/Cargo.lock b/Cargo.lock index 93d4c8c..a65cd20 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" @@ -15,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" @@ -111,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" @@ -128,14 +174,23 @@ 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", + "nom", "pretty_assertions", "unic-idna", ] @@ -156,6 +211,7 @@ version = "0.2.0-pre" dependencies = [ "ansi_term", "atty", + "base64", "datetime", "dns", "dns-transport", @@ -167,6 +223,33 @@ dependencies = [ "rand", ] +[[package]] +name = "ech-config" +version = "0.1.0" +dependencies = [ + "base64", + "byteorder", + "env_logger", + "log", + "pretty_assertions", + "serde", + "serde_json", + "serde_with", +] + +[[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" @@ -189,6 +272,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" @@ -245,6 +334,18 @@ 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 = "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" @@ -296,6 +397,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 +470,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" @@ -425,9 +549,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", @@ -437,9 +561,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", ] @@ -508,6 +632,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" @@ -523,6 +664,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" @@ -564,18 +711,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", @@ -584,15 +731,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" @@ -604,11 +774,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", @@ -641,6 +817,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" @@ -762,6 +947,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" @@ -790,6 +981,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/Cargo.toml b/Cargo.toml index 2a40fb4..1ee3c5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ doctest = false members = [ "dns", "dns-transport", + "ech-config", ] @@ -57,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/Cargo.toml b/dns/Cargo.toml index 0dc7ba5..a53e3ec 100644 --- a/dns/Cargo.toml +++ b/dns/Cargo.toml @@ -15,9 +15,11 @@ log = "0.4" # protocol parsing helper byteorder = "1.3" +nom = "7.0.0" # printing of certain packets -base64 = "0.13" +base64 = "0.13.0" +display_utils = "0.4.0" # idna encoding unic-idna = { version = "0.9.0", optional = true } @@ -27,6 +29,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/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" diff --git a/dns/src/lib.rs b/dns/src/lib.rs index fe2d443..e1782e4 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)] @@ -38,7 +37,11 @@ pub use self::types::*; mod strings; pub use self::strings::Labels; +mod value_list; + 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 908fb4d..c2914a2 100644 --- a/dns/src/record/mod.rs +++ b/dns/src/record/mod.rs @@ -2,6 +2,8 @@ use crate::wire::*; +#[macro_use] +pub(crate) mod utils; mod a; pub use self::a::A; @@ -54,6 +56,13 @@ pub use self::soa::SOA; mod srv; 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, SvcParam, SvcParams}; +} + mod tlsa; pub use self::tlsa::TLSA; @@ -63,11 +72,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 +86,7 @@ pub enum Record { EUI48(EUI48), EUI64(EUI64), HINFO(HINFO), + HTTPS(HTTPS), LOC(LOC), MX(MX), NAPTR(NAPTR), @@ -89,13 +97,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 +112,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 +124,7 @@ pub enum RecordType { EUI48, EUI64, HINFO, + HTTPS, LOC, MX, NAPTR, @@ -126,6 +134,7 @@ pub enum RecordType { SSHFP, SOA, SRV, + SVCB, TLSA, TXT, URI, @@ -141,7 +150,7 @@ impl From for RecordType { if $record::RR_TYPE == type_number { return RecordType::$record; } - } + }; } try_record!(A); @@ -151,6 +160,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 +171,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 +180,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 +189,7 @@ impl RecordType { if $record::NAME.eq_ignore_ascii_case(type_name) { return Some(Self::$record); } - } + }; } try_record!(A); @@ -190,6 +199,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 +210,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 +228,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 +239,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..adab1b4 --- /dev/null +++ b/dns/src/record/svcb_https.rs @@ -0,0 +1,909 @@ +//! The format of both SVCB and HTTPS RRs is identical. + +use core::fmt; +use log::*; +use std::collections::BTreeMap; +use std::io; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use crate::record::utils::{read_convert, CursorExt, Opaque, ReadFromCursor}; +use crate::strings::{Labels, ReadLabels}; +use crate::value_list::encoding; +use crate::wire::*; + +/// A **SVCB** (*service binding*) record, which holds information needed to make connections to +/// network services, such as for HTTPS origins. +/// +/// # 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 { + /// 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, + /// The SvcParams + pub params: Option, +} + +/// An **HTTPS** record, which is the HTTPS incarnation of **SVCB**. +#[derive(PartialEq, Debug)] +pub struct HTTPS { + /// The underlying SVCB record + pub svcb: SVCB, +} + +impl HTTPS { + /// Constructor + pub fn new(svcb: SVCB) -> Self { + Self { svcb } + } +} + +u16_enum! { + /// 14.3.2. Initial contents (subject to IANA additions) + #[derive(Copy, Eq, PartialOrd, Ord, Hash)] + 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` + KeyNNNNN(u16), + /// Invalid. + 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("ech")?, + Self::Ipv6Hint => f.write_str("ipv6hint")?, + Self::KeyNNNNN(n) => write!(f, "key{}", n)?, + Self::InvalidKey => f.write_str("[invalid key]")?, + }) + } +} + +/// The SvcParams section of a [SVCB] record +#[derive(Debug, Clone, PartialEq, Default)] +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 value-list + 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 + 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, 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 + pub 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; + if !mandatory.is_empty() { + write!( + f, + " mandatory={}", + display_utils::join(mandatory.iter(), ",") + )?; + } + if let Some(alpn) = alpn { + f.write_str(" alpn=")?; + encoding::EscapeValueList(alpn.ids.iter().map(|id| id.0.as_slice())).fmt(f)?; + if alpn.no_default_alpn { + write!(f, " no-default-alpn")?; + } + } + if let &Some(port) = port { + write!(f, " port={}", port)?; + } + if !ipv4hint.is_empty() { + write!(f, " ipv4hint={}", display_utils::join(ipv4hint.iter(), ","))?; + } + if let Some(ech) = ech { + write!(f, " ech={}", ech)?; + } + if !ipv6hint.is_empty() { + write!(f, " ipv6hint={}", display_utils::join(ipv6hint.iter(), ","))?; + } + if !other.is_empty() { + other + .iter() + .try_for_each(|(k, v)| write!(f, " {}={}", k, v))?; + } + Ok(()) + } +} + +impl SvcParams { + fn read(cursor: &mut Cursor<&[u8]>) -> Result { + let mut mandatory = Default::default(); + 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 = BTreeMap::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 opaque = Opaque::read_known_len(cursor, param_length)?; + ech = Some(opaque); + } + SvcParam::Ipv6Hint => { + ipv6hint = read_convert(cursor, len_hint, |c| c.read_u128::())?; + } + SvcParam::InvalidKey => { + return Err(WireError::IO); + } + SvcParam::NoDefaultAlpn => { + no_default_alpn = true; + } + 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 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::()?); + } + } + Ok(()) + })?; + } + + if no_default_alpn && alpn_ids.is_empty() { + return Err(WireError::IO); + } + let alpn = if alpn_ids.is_empty() { + None + } else { + Some(Alpn { + ids: alpn_ids, + no_default_alpn, + }) + }; + + Ok(Self { + mandatory, + alpn, + port, + ipv4hint, + ech, + ipv6hint, + other, + }) + } +} + +/// The ALPN configuration, covering the `alpn` and `no-default-alpn` parameters. +#[derive(Debug, Clone, PartialEq)] +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)] +pub 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::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[..]; + String::from_utf8_lossy(bytes).fmt(f) + } +} + +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::new) + } +} + +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); + + // ServiceMode + let service_mode = priority > 0; + + // 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, + params: 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 + ); + Err(WireError::WrongLabelLength { + stated_length, + length_after_labels: total_read, + }) + } else { + Ok(ret) + } + } +} + +impl fmt::Display for HTTPS { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.svcb.fmt(f) + } +} + +impl fmt::Display for SVCB { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + priority, + target, + params: parameters, + } = self; + + write!(f, "{} {}", priority, target)?; + if let Some(params) = parameters { + write!(f, "{}", params)?; + } + Ok(()) + } +} + +#[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; + + #[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::new(SVCB { + priority: 1, + target: Labels::root(), + params: Some(SvcParams { + mandatory: vec![], + alpn: Some(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: BTreeMap::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 ignore_alias_mode_params() { + init_logs(); + let buf = &[ + 0, 0, // SvcPriority 0, therefore AliasMode + 0, // TargetName = . + // SvcParams + 0, 3, 0, 2, 0x01, 0xbb, // port, len 2, "443" + ]; + assert_eq!( + SVCB::read(9, &mut Cursor::new(buf)), + Ok(SVCB { + priority: 0, + target: Labels::root(), + params: None, + }) + ); + } + + #[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)); + } +} + +/// See the draft RFC +#[cfg(test)] +mod test_vectors { + use crate::value_list::ValueList; + + use super::*; + use pretty_assertions::assert_eq; + + #[test] + 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(), + params: None, + }; + assert_eq!( + 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(), + params: Some(SvcParams::default()), + }; + assert_eq!( + SVCB::read(buf.len() as u16, &mut Cursor::new(buf)).as_ref(), + Ok(&value) + ); + assert_eq!(value.to_string(), "1 ."); + } + + #[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 + ]; + let value = SVCB { + priority: 16, + target: Labels::encode("foo.example.com.").unwrap(), + params: Some(SvcParams { + port: Some(53), + ..SvcParams::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.com. port=53"); + } + + #[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 + ]; + let value = SVCB { + priority: 1, + target: Labels::encode("foo.example.com.").unwrap(), + params: 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)).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, /* \210 */ + 0x71, 0x6f, 0x6f, // value + ]; + let value = SVCB { + priority: 1, + target: Labels::encode("foo.example.com.").unwrap(), + params: 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(), + r#"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(), + params: 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(), + params: 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(), + params: Some(SvcParams { + mandatory: vec![SvcParam::Alpn, SvcParam::Ipv4Hint], + alpn: Some(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(), + 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 + ids: vec![r"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) + ); + + // 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] + 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); + 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. +} + +#[cfg(test)] +mod test_ech { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn ech_param() { + init_logs(); + let buf = &[ + 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"# + ) + ); + } +} diff --git a/dns/src/record/utils.rs b/dns/src/record/utils.rs new file mode 100644 index 0000000..01f9711 --- /dev/null +++ b/dns/src/record/utils.rs @@ -0,0 +1,190 @@ +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, displayed as base64 +/// +/// e.g. [crate::record::svcb::SvcParams::ech] +#[derive(Debug, Clone, PartialEq)] +pub struct Opaque(pub 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), + )? + } + } + } + }; +} + diff --git a/dns/src/strings.rs b/dns/src/strings.rs index fee4a43..cea074b 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. @@ -84,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)?; } @@ -205,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 new file mode 100644 index 0000000..7eefd98 --- /dev/null +++ b/dns/src/value_list.rs @@ -0,0 +1,581 @@ +#![allow(dead_code)] + +use core::fmt::{self, Display}; +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-07.html), section A.1 +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone)] +pub(crate) struct ValueList { + /// The parsed values + 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(crate) struct SingleValue { + /// The value + pub value: Vec, +} + +fn wrap_iresult_complete(result: IResult<&[u8], T>) -> Result { + result + .finish() + .map_err(|e| DecodingError::new(e.input)) + .and_then(|(remain, t)| { + if remain.is_empty() { + Ok(t) + } else { + Err(DecodingError::new(remain)) + } + }) +} + +/// An error that occurred while decoding a char-string or value-list +#[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 { + Self { values: Vec::new() } + } + + /// 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: &[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 { + /// 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(), + }) + } +} + +#[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; +use nom::bytes::complete::{tag, take_while1}; +use nom::combinator::recognize; +use nom::error::ParseError; +use nom::sequence::preceded; +use nom::IResult; +use nom::{Finish, Parser}; + +#[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) + }) + }) +} + +/// Tools for encoding and decoding value-list and char-string +pub(crate) 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 { + let mut remain = input; + core::iter::from_fn(move || match parser.parse(remain) { + Ok((rest, chunk)) => { + remain = rest; + Some(chunk) + } + Err(nom::Err::Error(..)) => None, + 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( + input, + nom::error::ErrorKind::Char, + )) + })?; + Ok((remain, *byte)) + } + + #[derive(Debug, Clone)] + 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) + // special treatment for these few, not matched by is_non_special + .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(super) fn emit_chunks(input: &[u8]) -> impl Iterator> + Clone { + iter_parser(input, chunk) + } + } + + mod value_list { + 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(tag(br"\").map(|_| EncodingChunk::Escape(r"\\"))) + .or(tag(br",").map(|_| EncodingChunk::Escape(r"\,"))) + .parse(remain) + } + + 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> + Clone + 'a, + { + 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| { + log::error!("error escaping string: {}", e); + fmt::Error + })?; + f.write_str(string)?; + Ok(()) + } + EncodingChunk::Escape(str) => str.fmt(f), + EncodingChunk::Byte(byte) => { + write!(f, "\\{:03}", byte) + } + }) + } + + /// 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"\\"); + } + + /// 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> + where + I: IntoIterator + Clone, + I::IntoIter: Clone, + { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let values = self.0.clone().into_iter(); + let chunk_iter = values.map(|value| { + let chunks = value_list::emit_chunks(value); + 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) + } + } + + 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) + } + } + + impl fmt::Display for SingleValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = self.value.as_slice(); + EscapeCharString(bytes).fmt(f) + } + } +} + +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, Clone)] +pub enum DecodingChunk<'a> { + Slice(&'a [u8]), + Byte(u8), +} + +fn chunk_1<'a, F, E>(mut inner: F) -> impl Parser<&'a [u8], Cow<'a, [u8]>, E> +where + F: Parser<&'a [u8], DecodingChunk<'a>, E>, + E: ParseError<&'a [u8]> + core::fmt::Debug, +{ + move |input| { + let mut output = Cow::Borrowed(&[][..]); + 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 { + 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()?; + if success { + Ok((remain, output)) + } else { + Err(nom::Err::Error(E::from_error_kind( + input, + nom::error::ErrorKind::Eof, + ))) + } + } +} + +mod char_string_decoding { + use super::*; + + 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.map(DecodingChunk::Slice), + char_escape.map(DecodingChunk::Byte), + ))(input) + } + + fn contiguous(input: &[u8]) -> IResult<&[u8], Cow<'_, [u8]>> { + chunk_1(contiguous_chunk).parse(input) + } + + 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(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) + } + + /// 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], Cow<'_, [u8]>> { + quoted.or(contiguous).parse(input) + } + + #[test] + fn test_char_string_decoding() { + 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(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(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()))); + } +} + +mod value_list_decoding { + use super::*; + + 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(( + 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], 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.map(|x| x.to_vec())) + .parse(input) + } + + 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(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.map(|x| x.to_vec())); + let mut parser = nom::sequence::delimited(tag(b"\""), values, tag(b"\"")); + parser.parse(input) + } + + pub fn parse(char_decoded: &[u8]) -> IResult<&[u8], Vec>> { + values_quoted.or(values_contiguous).parse(char_decoded) + } + + #[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_parsing() { + // value lists must be non-empty + assert!(ValueList::parse(b"").is_err()); + assert_eq!( + ValueList::parse(br"hello"), + Ok(vec![b"hello".to_vec()].into()) + ); + assert_eq!( + ValueList::parse(br"hello\044hello"), + Ok(vec![b"hello".to_vec(), b"hello".to_vec()].into()) + ); + assert_eq!( + ValueList::parse(br"hello\\\044hello"), + Ok(vec![br"hello,hello".to_vec()].into()) + ); + assert_eq!( + ValueList::parse(br"hello\\\\044"), + Ok(vec![br"hello\044".to_vec()].into()) + ); + assert_eq!( + ValueList::parse(br"hello,\\\044"), + Ok(vec![br"hello".to_vec(), br",".to_vec()].into()) + ); + assert_eq!( + 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!( + 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"\""))); + } +} 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/ech-config/Cargo.toml b/ech-config/Cargo.toml new file mode 100644 index 0000000..11b8c77 --- /dev/null +++ b/ech-config/Cargo.toml @@ -0,0 +1,29 @@ +[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 + +[[bin]] +name = "ech-config" +path = "bin/ech_config.rs" + +[dependencies] +# logging +log = "0.4" + +# protocol parsing helper +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" +env_logger = "0.9.0" + diff --git a/ech-config/bin/ech_config.rs b/ech-config/bin/ech_config.rs new file mode 100644 index 0000000..6e95cdb --- /dev/null +++ b/ech-config/bin/ech_config.rs @@ -0,0 +1,23 @@ +use ech_config::ECHConfigList; +use serde_json; + +use std::io::{self, Read}; + +fn main() { + 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/ech-config/src/cursor_ext.rs b/ech-config/src/cursor_ext.rs new file mode 100644 index 0000000..8399a57 --- /dev/null +++ b/ech-config/src/cursor_ext.rs @@ -0,0 +1,123 @@ +use byteorder::{BigEndian, ReadBytesExt}; +use serde::{Serialize, Deserialize}; +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)) + } + } +} + +#[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 { + 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, Serialize, Deserialize)] +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..309c2ff --- /dev/null +++ b/ech-config/src/lib.rs @@ -0,0 +1,556 @@ +//! ECH RFC draft 13 section 4 + +use core::fmt; +use std::{ + convert::TryInto, + io::{self, Read}, +}; + +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, Deserialize, Serialize)] +#[serde(transparent)] +pub struct ECHConfigList { + configs: Vec, +} + +impl ECHConfigList { + 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); + + 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); + 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)?; + ECHConfigContents::UnknownECHVersion(opq) + } + }; + Ok(Self { version, contents }) + } +} + +#[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[..]; + 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") + } + } +} + +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, 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 + 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 + // + // 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(Opaque<0, { u16::MAX }>), +} + +#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] +pub enum EncryptedClientHello { + Outer { + cipher_suite: tls13::HpkeSymmetricCipherSuite, + config_id: u8, + enc: Opaque<0, { u16::MAX }>, + payload: Opaque<1, { u16::MAX }>, + }, + Inner, +} + +u16_enum! { + #[derive(Deserialize, Serialize)] + 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, Deserialize, Serialize)] +pub struct EchOuterExtensions { + outer: Vec, +} + +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 + // + // - 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, Deserialize, Serialize)] + 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, Deserialize, Serialize)] + 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, Deserialize, Serialize)] + 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)] + #[derive(Deserialize, Serialize)] + 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)] + #[derive(Deserialize, Serialize)] + 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)] + #[derive(Deserialize, Serialize)] + pub enum HpkeAeadId { + Reserved = 0, + AES_128_GCM = 1, + AES_256_GCM = 2, + ChaCha20Poly1305 = 3, + @unknown Unknown(u16), + ExportOnly = 0xffff, + } + } + + #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] + 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, Deserialize, Serialize)] + 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, Deserialize, Serialize)] + 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! { + #[derive(Deserialize, Serialize)] + 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! { + #[derive(Deserialize, Serialize)] + 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 { + 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![], + }, + }], + }; + + 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), + )? + } + } + } + }; +} 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 + )) + }) + }) + } +} + 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..f88c208 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}; @@ -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,34 @@ fn json_record_data(record: Record) -> JsonValue { "bytes": bytes, } } + 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| 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)| { + obj[k] = v.into(); + }); + obj + }); + object! { + "priority": *priority, + "target": target.to_string(), + "params": params, } } @@ -602,7 +640,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 { @@ -619,7 +661,10 @@ impl fmt::Display for Ascii<'_> { } } - write!(f, "\"") + if contains_spaces { + write!(f, "\"")?; + } + Ok(()) } } @@ -715,7 +760,7 @@ mod test { #[test] fn escape_backslashes() { assert_eq!(Ascii(b"\\").to_string(), - "\"\\\\\""); + "\\\\"); } #[test] @@ -727,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"); } } 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()), }