1use std::{borrow::Cow, collections::BTreeMap, convert::TryInto};
2
3use crate::dns::{WireFormat, MAX_SVC_PARAM_VALUE_LENGTH};
4use crate::{CharacterString, Name};
5
6use super::RR;
7
8#[derive(Debug, PartialEq, Eq, Hash, Clone)]
11pub struct SVCB<'a> {
12 pub priority: u16,
16
17 pub target: Name<'a>,
20
21 params: BTreeMap<u16, Cow<'a, [u8]>>,
23}
24
25impl<'a> RR for SVCB<'a> {
26 const TYPE_CODE: u16 = 64;
27}
28
29impl<'a> SVCB<'a> {
30 pub const MANDATORY: u16 = 0;
32
33 pub const ALPN: u16 = 1;
35
36 pub const NO_DEFAULT_ALPN: u16 = 2;
38
39 pub const PORT: u16 = 3;
41
42 pub const IPV4HINT: u16 = 4;
44
45 pub const ECH: u16 = 5;
47
48 pub const IPV6HINT: u16 = 6;
50
51 pub fn new(priority: u16, target: Name<'a>) -> Self {
53 Self {
54 priority,
55 target,
56 params: BTreeMap::new(),
57 }
58 }
59
60 pub fn set_param<V: Into<Cow<'a, [u8]>>>(&mut self, key: u16, value: V) -> crate::Result<()> {
66 let value = value.into();
67 if value.len() > MAX_SVC_PARAM_VALUE_LENGTH {
68 return Err(crate::SimpleDnsError::InvalidDnsPacket);
69 }
70 self.params.insert(key, value);
71 Ok(())
72 }
73
74 pub fn set_mandatory<I: IntoIterator<Item = u16>>(&mut self, keys: I) -> crate::Result<()> {
78 let value = keys.into_iter().flat_map(u16::to_be_bytes).collect();
79 self.set_param(Self::MANDATORY, Cow::Owned(value))
80 }
81
82 pub fn set_alpn<'cs, I: IntoIterator<Item = CharacterString<'cs>>>(
86 &mut self,
87 alpn_ids: I,
88 ) -> crate::Result<()> {
89 let mut value = Vec::new();
90 for alpn_id in alpn_ids {
91 alpn_id.write_to(&mut value)?;
92 }
93 self.set_param(Self::ALPN, value)
94 }
95
96 pub fn set_no_default_alpn(&mut self) {
98 self.set_param(Self::NO_DEFAULT_ALPN, &b""[..]).unwrap();
99 }
100
101 pub fn set_port(&mut self, port: u16) {
103 self.set_param(Self::PORT, port.to_be_bytes().to_vec())
104 .unwrap();
105 }
106
107 pub fn set_ipv4hint<I: IntoIterator<Item = u32>>(&mut self, ips: I) -> crate::Result<()> {
111 let value = ips.into_iter().flat_map(u32::to_be_bytes).collect();
112 self.set_param(Self::IPV4HINT, Cow::Owned(value))
113 }
114
115 pub fn set_ipv6hint<I: IntoIterator<Item = u128>>(&mut self, ips: I) -> crate::Result<()> {
119 let value = ips.into_iter().flat_map(u128::to_be_bytes).collect();
120 self.set_param(Self::IPV6HINT, Cow::Owned(value))
121 }
122
123 pub fn get_param(&self, key: u16) -> Option<&[u8]> {
128 self.params.get(&key).map(|v| &**v)
129 }
130
131 pub fn iter_params(&self) -> impl Iterator<Item = (u16, &[u8])> {
133 self.params.iter().map(|(k, v)| (*k, &**v))
134 }
135
136 pub fn into_owned<'b>(self) -> SVCB<'b> {
138 SVCB {
139 priority: self.priority,
140 target: self.target.into_owned(),
141 params: self
142 .params
143 .into_iter()
144 .map(|(k, v)| (k, v.into_owned().into()))
145 .collect(),
146 }
147 }
148}
149
150impl<'a> WireFormat<'a> for SVCB<'a> {
151 fn parse(data: &'a [u8], position: &mut usize) -> crate::Result<Self>
152 where
153 Self: Sized,
154 {
155 let priority = u16::from_be_bytes(data[*position..*position + 2].try_into()?);
156 *position += 2;
157
158 let target = Name::parse(data, position)?;
159 let mut params = BTreeMap::new();
160 let mut previous_key = -1;
161 while *position < data.len() {
162 let key = u16::from_be_bytes(data[*position..*position + 2].try_into()?);
163 let value_length = usize::from(u16::from_be_bytes(
164 data[*position + 2..*position + 4].try_into()?,
165 ));
166 if i32::from(key) <= previous_key {
167 return Err(crate::SimpleDnsError::InvalidDnsPacket);
168 }
169 previous_key = i32::from(key);
170 params.insert(
171 key,
172 Cow::Borrowed(&data[*position + 4..*position + 4 + value_length]),
173 );
174 *position += 4 + value_length;
175 }
176 Ok(Self {
177 priority,
178 target,
179 params,
180 })
181 }
182
183 fn write_to<T: std::io::Write>(&self, out: &mut T) -> crate::Result<()> {
184 out.write_all(&self.priority.to_be_bytes())?;
185 self.target.write_to(out)?;
186 for (key, value) in &self.params {
187 out.write_all(&key.to_be_bytes())?;
188 let value_length = value.len() as u16;
189 out.write_all(&value_length.to_be_bytes())?;
190 out.write_all(value)?;
191 }
192 Ok(())
193 }
194
195 fn len(&self) -> usize {
199 2 + self.target.len()
200 + self
201 .params
202 .values()
203 .map(|value| value.len() + 4)
204 .sum::<usize>()
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use crate::{rdata::RData, ResourceRecord};
212
213 #[test]
214 fn parse_sample() -> Result<(), Box<dyn std::error::Error>> {
215 let sample_file = std::fs::read("samples/zonefile/HTTPS.sample")?;
217
218 let sample_rdata = match ResourceRecord::parse(&sample_file, &mut 0)?.rdata {
219 RData::HTTPS(rdata) => rdata,
220 _ => unreachable!(),
221 };
222
223 let mut expected_rdata = SVCB::new(1, Name::new_unchecked(""));
224 expected_rdata.set_alpn(["http/1.1".try_into()?, "h2".try_into()?])?;
225 expected_rdata.set_ipv4hint([0xa2_9f_89_55, 0xa2_9f_8a_55])?;
226 expected_rdata.set_param(
227 SVCB::ECH,
228 &b"\x00\x45\
229 \xfe\x0d\x00\x41\x44\x00\x20\x00\x20\x1a\xd1\x4d\x5c\xa9\x52\xda\
230 \x88\x18\xae\xaf\xd7\xc6\xc8\x7d\x47\xb4\xb3\x45\x7f\x8e\x58\xbc\
231 \x87\xb8\x95\xfc\xb3\xde\x1b\x34\x33\x00\x04\x00\x01\x00\x01\x00\
232 \x12cloudflare-ech.com\x00\x00"[..],
233 )?;
234 expected_rdata.set_ipv6hint([
235 0x2606_4700_0007_0000_0000_0000_a29f_8955,
236 0x2606_4700_0007_0000_0000_0000_a29f_8a55,
237 ])?;
238
239 assert_eq!(*sample_rdata, expected_rdata);
240
241 assert_eq!(
242 sample_rdata.get_param(SVCB::ALPN),
243 Some(&b"\x08http/1.1\x02h2"[..])
244 );
245 assert_eq!(sample_rdata.get_param(SVCB::PORT), None);
246
247 Ok(())
248 }
249
250 #[test]
251 fn parse_and_write_svcb() {
252 let tests: &[(&str, &[u8], SVCB<'_>)] = &[
255 (
256 "D.1. AliasMode",
257 b"\x00\x00\x03foo\x07example\x03com\x00",
258 SVCB::new(0, Name::new_unchecked("foo.example.com")),
259 ),
260 (
261 "D.2.3. TargetName Is '.'",
262 b"\x00\x01\x00",
263 SVCB::new(1, Name::new_unchecked("")),
264 ),
265 (
266 "D.2.4. Specified a Port",
267 b"\x00\x10\x03foo\x07example\x03com\x00\x00\x03\x00\x02\x00\x35",
268 {
269 let mut svcb = SVCB::new(16, Name::new_unchecked("foo.example.com"));
270 svcb.set_port(53);
271 svcb
272 }
273 ),
274 (
275 "D.2.6. A Generic Key and Quoted Value with a Decimal Escape",
276 b"\x00\x01\x03foo\x07example\x03com\x00\x02\x9b\x00\x09hello\xd2qoo",
277 {
278 let mut svcb = SVCB::new(1, Name::new_unchecked("foo.example.com"));
279 svcb.set_param(667, &b"hello\xd2qoo"[..]).unwrap();
280 svcb
281 }
282 ),
283 (
284 "D.2.7. Two Quoted IPv6 Hints",
285 b"\x00\x01\x03foo\x07example\x03com\x00\x00\x06\x00\x20\
286 \x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\
287 \x20\x01\x0d\xb8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x53\x00\x01",
288 {
289 let mut svcb = SVCB::new(1, Name::new_unchecked("foo.example.com"));
290 svcb.set_ipv6hint([
291 0x2001_0db8_0000_0000_0000_0000_0000_0001,
292 0x2001_0db8_0000_0000_0000_0000_0053_0001,
293 ]).unwrap();
294 svcb
295 },
296 ),
297 (
298 "D.2.10. SvcParamKey Ordering Is Arbitrary in Presentation Format but Sorted in Wire Format",
299 b"\x00\x10\x03foo\x07example\x03org\x00\
300 \x00\x00\x00\x04\x00\x01\x00\x04\
301 \x00\x01\x00\x09\x02h2\x05h3-19\
302 \x00\x04\x00\x04\xc0\x00\x02\x01",
303 {
304 let mut svcb = SVCB::new(16, Name::new_unchecked("foo.example.org"));
305 svcb.set_alpn(["h2".try_into().unwrap(), "h3-19".try_into().unwrap()]).unwrap();
306 svcb.set_mandatory([SVCB::ALPN, SVCB::IPV4HINT]).unwrap();
307 svcb.set_ipv4hint([0xc0_00_02_01]).unwrap();
308 svcb
309 },
310 ),
311 ];
312
313 for (name, expected_bytes, svcb) in tests {
314 let mut data = Vec::new();
315 svcb.write_to(&mut data).unwrap();
316 assert_eq!(expected_bytes, &data, "Test {name}");
317
318 let svcb2 = SVCB::parse(&data, &mut 0).unwrap();
319 assert_eq!(svcb, &svcb2, "Test {name}");
320 }
321 }
322}