simple_dns/dns/rdata/
svcb.rs

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/// The SVCB DNS RR type is used to locate alternative endpoints for a service.
9/// [RFC 9460](https://datatracker.ietf.org/doc/html/rfc9460).
10#[derive(Debug, PartialEq, Eq, Hash, Clone)]
11pub struct SVCB<'a> {
12    /// The priority of this record (relative to others, with lower values preferred).
13    ///
14    /// A value of 0 indicates AliasMode.
15    pub priority: u16,
16
17    /// The domain name of either the alias target (for AliasMode)
18    /// or the alternative endpoint (for ServiceMode).
19    pub target: Name<'a>,
20
21    /// A list of key=value pairs describing the alternative endpoint at `target`.
22    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    /// Mandatory keys in this RR.
31    pub const MANDATORY: u16 = 0;
32
33    /// Additional supported protocols.
34    pub const ALPN: u16 = 1;
35
36    /// No support for default protocol.
37    pub const NO_DEFAULT_ALPN: u16 = 2;
38
39    /// Port for alternative endpoint.
40    pub const PORT: u16 = 3;
41
42    /// IPv4 address hints.
43    pub const IPV4HINT: u16 = 4;
44
45    /// Encrypted ClientHello (ECH) configuration.
46    pub const ECH: u16 = 5;
47
48    /// IPv6 address hints.
49    pub const IPV6HINT: u16 = 6;
50
51    /// Creates a new `SVCB` instance with no parameters.
52    pub fn new(priority: u16, target: Name<'a>) -> Self {
53        Self {
54            priority,
55            target,
56            params: BTreeMap::new(),
57        }
58    }
59
60    /// Sets an arbitrary key=value parameter.
61    ///
62    /// The format of `value` is not checked against the `key`.
63    ///
64    /// If a parameter of the given `key` already existed, the previous entry will be replaced.
65    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    /// Sets the "mandatory" parameter.
75    ///
76    /// The `keys` MUST not be empty and already in strictly increasing order.
77    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    /// Sets the "alpn" parameter.
83    ///
84    /// The `alpn_ids` MUST not be empty.
85    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    /// Sets the "no-default-alpn" parameter.
97    pub fn set_no_default_alpn(&mut self) {
98        self.set_param(Self::NO_DEFAULT_ALPN, &b""[..]).unwrap();
99    }
100
101    /// Sets the "port" parameter.
102    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    /// Sets the "ipv4hint" parameter.
108    ///
109    /// The `ips` MUST not be empty.
110    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    /// Sets the "ipv6hint" parameter.
116    ///
117    /// The `ips` MUST not be empty.
118    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    /// Gets a read-only reference to the SvcParamValue of a given key in wire format.
124    ///
125    /// Returns `None` if the key does not exist.
126    // TODO actually parse the SvcParamValue?
127    pub fn get_param(&self, key: u16) -> Option<&[u8]> {
128        self.params.get(&key).map(|v| &**v)
129    }
130
131    /// Iterates over all parameters.
132    pub fn iter_params(&self) -> impl Iterator<Item = (u16, &[u8])> {
133        self.params.iter().map(|(k, v)| (*k, &**v))
134    }
135
136    /// Transforms the inner data into its owned type
137    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    // NOT implementing `write_compressed_to`,
196    // RFC9460 ยง2.2 specifically mentioned the TargetName is *uncompressed*.
197
198    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        // Copy of the answer from `dig crypto.cloudflare.com -t HTTPS`.
216        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        // Test vectors are taken from Appendix D.
253        // <https://www.rfc-editor.org/rfc/rfc9460.html#name-test-vectors>
254        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}