dns_message_parser/rr/
draft_ietf_dnsop_svcb_https.rs

1use crate::rr::draft_ietf_dnsop_svcb_https::ServiceBindingMode::{Alias, Service};
2use crate::rr::{ToType, Type};
3use crate::DomainName;
4use base64::{engine::general_purpose::STANDARD as Base64Standard, Engine};
5use std::cmp::Ordering;
6use std::collections::BTreeSet;
7use std::fmt::{Display, Formatter, Result as FmtResult};
8use std::hash::{Hash, Hasher};
9use std::net::{Ipv4Addr, Ipv6Addr};
10
11/// A Service Binding record for locating alternative endpoints for a service.
12///
13#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub struct ServiceBinding {
15    pub name: DomainName,
16    pub ttl: u32,
17
18    // The class is always IN (Internet, 0x0001)
19    /// The `SvcPriority` field, a value between 0 and 65535
20    /// SVCB resource records with a smaller priority SHOULD be given priority over resource records
21    /// with a larger value.
22    pub priority: u16,
23    pub target_name: DomainName,
24    pub parameters: BTreeSet<ServiceParameter>,
25    /// Indicates whether or not this is an HTTPS record (RFC section 8)
26    pub https: bool,
27}
28
29impl ToType for ServiceBinding {
30    fn to_type(&self) -> Type {
31        if self.https {
32            Type::HTTPS
33        } else {
34            Type::SVCB
35        }
36    }
37}
38
39/// The modes inferred from the `SvcPriority` field
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum ServiceBindingMode {
42    /// "go to the target name and do another service binding query"
43    /// enables apex aliasing for participating clients
44    Alias,
45
46    /// Indicates that this record contains an arbitrary (IANA controlled) key value data store
47    /// The record contains anything the client _may_ need to know in order to connect to the server.
48    Service,
49}
50
51impl Display for ServiceBinding {
52    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
53        let record_type = if self.https { "HTTPS" } else { "SVCB" };
54        write!(
55            f,
56            "{} {} IN {} {} {}",
57            self.name, self.ttl, record_type, self.priority, self.target_name
58        )?;
59        self.parameters
60            .iter()
61            .try_for_each(|parameter| -> FmtResult {
62                write!(f, " ")?;
63                parameter.fmt(f)
64            })
65    }
66}
67
68impl ServiceBinding {
69    pub fn mode(&self) -> ServiceBindingMode {
70        if self.priority == 0 {
71            Alias
72        } else {
73            Service
74        }
75    }
76}
77
78#[derive(Debug, Clone, Eq)]
79pub enum ServiceParameter {
80    /// Mandatory keys in this resource record (service mode only)
81    MANDATORY {
82        /// the key IDs the client must support in order for this resource record to function properly
83        /// RFC section 7
84        key_ids: Vec<u16>,
85    },
86    /// Additional supported protocols
87    ALPN {
88        /// The default set of ALPNs, which SHOULD NOT be empty, e.g. "h3", "h2", "http/1.1".
89        alpn_ids: Vec<String>,
90    },
91    /// No support for default protocol
92    ///
93    /// When this is specified in a resource record, `ALPN` must also be specified in order to be
94    /// "self-consistent".
95    NO_DEFAULT_ALPN,
96    /// Port for alternative endpoint
97    PORT { port: u16 },
98    /// IPv4 address hints
99    IPV4_HINT { hints: Vec<Ipv4Addr> },
100    /// Encrypted ClientHello information (RFC Section 9)
101    ///
102    /// This conveys the ECH configuration of an alternative endpoint.
103    ECH { config_list: Vec<u8> },
104    /// IPv6 address hints
105    IPV6_HINT { hints: Vec<Ipv6Addr> },
106    /// Private use keys 65280-65534
107    PRIVATE { number: u16, wire_data: Vec<u8> },
108    /// Reserved ("Invalid key")
109    KEY_65535,
110}
111
112impl PartialEq for ServiceParameter {
113    fn eq(&self, other: &Self) -> bool {
114        self.get_registered_number()
115            .eq(&other.get_registered_number())
116    }
117}
118
119impl Hash for ServiceParameter {
120    fn hash<H: Hasher>(&self, state: &mut H) {
121        state.write_u16(self.get_registered_number())
122    }
123}
124
125impl PartialOrd for ServiceParameter {
126    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
127        Some(self.cmp(other))
128    }
129}
130
131impl Ord for ServiceParameter {
132    fn cmp(&self, other: &Self) -> Ordering {
133        self.get_registered_number()
134            .cmp(&other.get_registered_number())
135    }
136}
137
138impl ServiceParameter {
139    pub fn get_registered_number(&self) -> u16 {
140        match self {
141            ServiceParameter::MANDATORY { .. } => 0,
142            ServiceParameter::ALPN { .. } => 1,
143            ServiceParameter::NO_DEFAULT_ALPN => 2,
144            ServiceParameter::PORT { .. } => 3,
145            ServiceParameter::IPV4_HINT { .. } => 4,
146            ServiceParameter::ECH { .. } => 5,
147            ServiceParameter::IPV6_HINT { .. } => 6,
148            ServiceParameter::PRIVATE {
149                number,
150                wire_data: _,
151            } => *number,
152            ServiceParameter::KEY_65535 => 65535,
153        }
154    }
155
156    fn id_to_presentation_name(id: u16) -> String {
157        match id {
158            0 => "mandatory".to_string(),
159            1 => "alpn".to_string(),
160            2 => "no-default-alpn".to_string(),
161            3 => "port".to_string(),
162            4 => "ipv4hint".to_string(),
163            5 => "ech".to_string(),
164            6 => "ipv6hint".to_string(),
165            65535 => "reserved".to_string(),
166            number => format!("key{}", number),
167        }
168    }
169}
170
171/// Escape backslashes and commas in an ALPN ID
172fn escape_alpn(alpn: &str) -> String {
173    let mut result = String::new();
174    for char in alpn.chars() {
175        if char == '\\' {
176            result.push_str("\\\\\\");
177        } else if char == ',' {
178            result.push('\\');
179        }
180        result.push(char);
181    }
182    result
183}
184
185impl Display for ServiceParameter {
186    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
187        match self {
188            ServiceParameter::MANDATORY { key_ids } => {
189                let mut key_ids = key_ids.clone();
190                key_ids.sort_unstable();
191                let mandatory_keys = key_ids
192                    .iter()
193                    .map(|id| ServiceParameter::id_to_presentation_name(*id))
194                    .collect::<Vec<String>>()
195                    .join(",");
196
197                write!(f, "mandatory={}", mandatory_keys)
198            }
199            ServiceParameter::ALPN { alpn_ids } => {
200                let mut escape = false;
201                let mut escaped_ids = vec![];
202                for id in alpn_ids {
203                    let escaped = escape_alpn(id);
204                    if escaped != *id {
205                        escape |= true;
206                    }
207                    escaped_ids.push(escaped);
208                }
209                let value = escaped_ids.join(",");
210                if escape {
211                    write!(f, "alpn=\"{}\"", value)
212                } else {
213                    write!(f, "alpn={}", value)
214                }
215            }
216            ServiceParameter::NO_DEFAULT_ALPN => write!(f, "no-default-alpn"),
217            ServiceParameter::PORT { port } => write!(f, "port={}", port),
218            ServiceParameter::IPV4_HINT { hints } => {
219                write!(
220                    f,
221                    "ipv4hint={}",
222                    hints
223                        .iter()
224                        .map(|hint| hint.to_string())
225                        .collect::<Vec<String>>()
226                        .join(",")
227                )
228            }
229            ServiceParameter::ECH { config_list } => {
230                write!(f, "ech={}", Base64Standard.encode(config_list))
231            }
232            ServiceParameter::IPV6_HINT { hints } => {
233                write!(
234                    f,
235                    "ipv6hint=\"{}\"",
236                    hints
237                        .iter()
238                        .map(|hint| hint.to_string())
239                        .collect::<Vec<String>>()
240                        .join(",")
241                )
242            }
243            ServiceParameter::PRIVATE { number, wire_data } => {
244                let key = format!("key{}", number);
245                let value = String::from_utf8(wire_data.clone());
246                if let Ok(value) = value {
247                    write!(f, "{}={}", key, value)
248                } else {
249                    let mut escaped = vec![];
250                    for byte in wire_data {
251                        if *byte < b'0'
252                            || (*byte > b'9' && *byte < b'A')
253                            || (*byte > b'Z' && *byte < b'a')
254                            || *byte > b'z'
255                        {
256                            escaped.extend_from_slice(format!("\\{}", *byte).as_bytes());
257                        } else {
258                            escaped.push(*byte);
259                        }
260                    }
261                    if let Ok(value) = String::from_utf8(escaped) {
262                        write!(f, "{}=\"{}\"", key, value)
263                    } else {
264                        write!(f, "{}=\"{}\"", key, Base64Standard.encode(wire_data))
265                    }
266                }
267            }
268            ServiceParameter::KEY_65535 => write!(f, "reserved"),
269        }
270    }
271}