Skip to main content

erbium/radv/
icmppkt.rs

1use crate::pktparser::Buffer;
2#[cfg(fuzzing)]
3use arbitrary::Arbitrary;
4use std::time::Duration;
5
6#[derive(Debug)]
7pub enum Error {
8    Truncated,
9    InvalidEncoding,
10    InvalidPacket,
11}
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
14#[cfg_attr(fuzzing, derive(Arbitrary))]
15struct Type(u8);
16const ICMP6_DESTINATION_UNREACHABLE: Type = Type(1);
17const ND_ROUTER_SOLICIT: Type = Type(133);
18const ND_ROUTER_ADVERT: Type = Type(134);
19const ND_NEIGHBOR_SOLICIT: Type = Type(135);
20const ND_NEIGHBOR_ADVERT: Type = Type(136);
21const ICMP6_REDIRECT: Type = Type(137);
22
23#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
24#[cfg_attr(fuzzing, derive(Arbitrary))]
25pub struct NDOption(u8);
26pub const SOURCE_LL_ADDR: NDOption = NDOption(1);
27pub const _TARGET_LL_ADDR: NDOption = NDOption(2);
28pub const PREFIX_INFO: NDOption = NDOption(3);
29pub const _REDIRECTED: NDOption = NDOption(4);
30pub const MTU: NDOption = NDOption(5);
31pub const _ROUTE_INFO: NDOption = NDOption(24);
32pub const RDNSS: NDOption = NDOption(25);
33pub const DNSSL: NDOption = NDOption(31);
34pub const CAPTIVE_PORTAL: NDOption = NDOption(37);
35pub const PREF64: NDOption = NDOption(38);
36pub const IPV6_ONLY_PREFERRED: NDOption = NDOption(108);
37
38#[derive(Clone, Debug, Eq, PartialEq)]
39pub enum NDOptionValue {
40    SourceLLAddr(Vec<u8>),
41    Mtu(u32),
42    Prefix(AdvPrefix),
43    RecursiveDnsServers((std::time::Duration, Vec<std::net::Ipv6Addr>)),
44    DnsSearchList((std::time::Duration, Vec<String>)), // TODO: String is probably the wrong type here.
45    CaptivePortal(String),
46    Pref64((std::time::Duration, u8, std::net::Ipv6Addr)),
47}
48
49#[cfg(fuzzing)]
50impl<'a> Arbitrary<'a> for NDOptionValue {
51    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
52        use std::convert::TryFrom as _;
53        match u.int_in_range(0..=5)? {
54            0 => Ok(NDOptionValue::SourceLLAddr(<[u8; 6]>::arbitrary(u)?.into())),
55            1 => Ok(NDOptionValue::Mtu(<_>::arbitrary(u)?)),
56            2 => Ok(NDOptionValue::Prefix(<_>::arbitrary(u)?)),
57            3 => {
58                let duration = Duration::from_secs(u.int_in_range(0..=2_u64.pow(32) - 1)?);
59                let num_ips = u.arbitrary_len::<[u8; 16]>()?;
60                let mut ips = Vec::new();
61                for _ in 0..std::cmp::min(num_ips, 127) {
62                    ips.push(<[u8; 16]>::arbitrary(u)?.into());
63                }
64
65                Ok(NDOptionValue::RecursiveDnsServers((duration, ips)))
66            }
67            4 => loop {
68                let mut url: String = <_>::arbitrary(u)?;
69                while url.ends_with('\0') {
70                    url.pop();
71                }
72                return Ok(NDOptionValue::CaptivePortal(url));
73            },
74            5 => Ok(NDOptionValue::Pref64((
75                Duration::from_secs(u.int_in_range(0..=2_u64.pow(13) - 1)? & !7),
76                u.int_in_range(0..=5)? * 8 + 32,
77                <[u8; 128 / 8]>::try_from(
78                    [&<[u8; 96 / 8]>::arbitrary(u)?, &[0_u8; (128 - 96) / 8][..]].concat(),
79                )
80                .unwrap()
81                .into(),
82            ))),
83            /* - no decoder (yet).  the domain type should be changed to support embedded '.' etc.
84            6 => Ok(NDOptionValue::DnsSearchList((
85                <_>::arbitrary(u)?,
86                <_>::arbitrary(u)?,
87            ))),
88            */
89            _ => unimplemented!(),
90        }
91    }
92}
93
94#[derive(Default, Debug, Eq, PartialEq)]
95#[cfg_attr(fuzzing, derive(Arbitrary))]
96pub struct NDOptions(Vec<NDOptionValue>);
97
98impl NDOptions {
99    pub fn add_option(&mut self, ov: NDOptionValue) {
100        self.0.push(ov);
101    }
102
103    #[cfg(test)]
104    pub fn find_option(&self, o: NDOption) -> Vec<NDOptionValue> {
105        self.0
106            .iter()
107            .filter(|x| match (&o, &x) {
108                (&RDNSS, NDOptionValue::RecursiveDnsServers(_)) => true,
109                (&RDNSS, _) => false,
110                (&DNSSL, NDOptionValue::DnsSearchList(_)) => true,
111                (&DNSSL, _) => false,
112                (&CAPTIVE_PORTAL, NDOptionValue::CaptivePortal(_)) => true,
113                (&CAPTIVE_PORTAL, _) => false,
114                (_, _) => unimplemented!(),
115            })
116            .cloned()
117            .collect()
118    }
119}
120
121#[derive(Debug, Eq, PartialEq)]
122#[cfg_attr(fuzzing, derive(Arbitrary))]
123pub enum Icmp6 {
124    Unknown,
125    RtrSolicit(NDOptions),
126    RtrAdvert(RtrAdvertisement),
127}
128
129#[derive(Debug, Eq, PartialEq)]
130pub struct RtrAdvertisement {
131    pub hop_limit: u8,
132    pub flag_managed: bool,
133    pub flag_other: bool,
134    pub lifetime: std::time::Duration,
135    pub reachable: std::time::Duration,
136    pub retrans: std::time::Duration,
137    pub options: NDOptions,
138}
139
140#[cfg(fuzzing)]
141impl<'a> Arbitrary<'a> for RtrAdvertisement {
142    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
143        Ok(Self {
144            hop_limit: <_>::arbitrary(u)?,
145            flag_managed: <_>::arbitrary(u)?,
146            flag_other: <_>::arbitrary(u)?,
147            lifetime: Duration::from_secs(<u16>::arbitrary(u)?.into()),
148            reachable: Duration::from_millis(<u32>::arbitrary(u)?.into()),
149            retrans: Duration::from_millis(<u32>::arbitrary(u)?.into()),
150            options: <_>::arbitrary(u)?,
151        })
152    }
153}
154
155#[derive(Clone, Debug, Eq, PartialEq)]
156pub struct AdvPrefix {
157    pub prefixlen: u8,
158    pub onlink: bool,
159    pub autonomous: bool,
160    pub valid: std::time::Duration,
161    pub preferred: std::time::Duration,
162    pub prefix: std::net::Ipv6Addr,
163}
164
165#[cfg(fuzzing)]
166impl<'a> Arbitrary<'a> for AdvPrefix {
167    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
168        Ok(AdvPrefix {
169            prefixlen: u.int_in_range(0..=128)?,
170            onlink: <_>::arbitrary(u)?,
171            autonomous: <_>::arbitrary(u)?,
172            valid: Duration::from_secs(<u32>::arbitrary(u)?.into()),
173            preferred: Duration::from_secs(<u32>::arbitrary(u)?.into()),
174            prefix: <[u8; 16]>::arbitrary(u)?.into(),
175        })
176    }
177}
178
179fn parse_nd_rtr_options(buf: &mut Buffer) -> Result<NDOptions, Error> {
180    let mut options: NDOptions = Default::default();
181    while buf.remaining() > 0 {
182        let ty = buf.get_u8().map(NDOption).ok_or(Error::Truncated)?;
183        let l = buf.get_u8().ok_or(Error::Truncated)? as usize;
184        if l == 0 {
185            // Nodes MUST silently discard an ND packet that contains an option with length zero.
186            return Err(Error::Truncated);
187        }
188        let data = buf.get_bytes(l * 8 - 2).ok_or(Error::Truncated)?;
189        match (ty, data) {
190            (SOURCE_LL_ADDR, value) => {
191                options.add_option(NDOptionValue::SourceLLAddr(value.to_vec()));
192            }
193            (CAPTIVE_PORTAL, value) => {
194                options.add_option(NDOptionValue::CaptivePortal(
195                    String::from_utf8(
196                        value[..value
197                            .iter()
198                            .rposition(|b| *b != 0)
199                            .map(|p| p + 1)
200                            .unwrap_or(0)]
201                            .into(),
202                    )
203                    .map_err(|_| Error::InvalidEncoding)?,
204                ));
205            }
206            (PREF64, value) => {
207                if value.len() != 2 * 8 - 2 {
208                    // The receiver MUST ignore the PREF64 option if the Length field value is not 2.
209                    // we ignore the entire packet instead...
210                    return Err(Error::InvalidPacket);
211                }
212                use std::convert::{TryFrom as _, TryInto as _};
213                let scaled_lifetime_plc = u16::from_be_bytes(value[0..=1].try_into().unwrap());
214                let lifetime = Duration::from_secs((scaled_lifetime_plc & !7).into());
215                let prefixlen = (scaled_lifetime_plc & 0x07) * 8 + 32;
216                let ip_octets =
217                    <[u8; 16]>::try_from([&value[2..], &[0, 0, 0, 0]].concat()).unwrap();
218                let prefix = std::net::Ipv6Addr::from(ip_octets);
219                options.add_option(NDOptionValue::Pref64((lifetime, prefixlen as u8, prefix)));
220            }
221            (MTU, value) => {
222                if value.len() != 8 - 2 {
223                    return Err(Error::InvalidPacket);
224                }
225                use std::convert::TryInto as _;
226                options.add_option(NDOptionValue::Mtu(u32::from_be_bytes(
227                    value[2..=5].try_into().unwrap(),
228                )));
229            }
230            (RDNSS, value) => {
231                use std::convert::{TryFrom as _, TryInto as _};
232                let lifetime =
233                    Duration::from_secs(u32::from_be_bytes(value[2..=5].try_into().unwrap()) as _);
234                let servers = value[6..]
235                    .chunks_exact(16)
236                    .map(|x| std::net::Ipv6Addr::from(<[u8; 16]>::try_from(x).unwrap()))
237                    .collect();
238                options.add_option(NDOptionValue::RecursiveDnsServers((lifetime, servers)));
239            }
240            (PREFIX_INFO, value) => {
241                use std::convert::{TryFrom as _, TryInto as _};
242                if value.len() != 4 * 8 - 2 {
243                    return Err(Error::InvalidPacket);
244                }
245                let prefixlen = value[0];
246                let onlink = value[1] & 0x80 != 0;
247                let autonomous = value[1] & 0x40 != 0;
248                let valid =
249                    Duration::from_secs(u32::from_be_bytes(value[2..6].try_into().unwrap()) as _);
250                let preferred =
251                    Duration::from_secs(u32::from_be_bytes(value[6..10].try_into().unwrap()) as _);
252                /* reserved (4 bytes) */
253                let prefix =
254                    std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&value[14..30]).unwrap());
255                options.add_option(NDOptionValue::Prefix(AdvPrefix {
256                    prefixlen,
257                    onlink,
258                    autonomous,
259                    valid,
260                    preferred,
261                    prefix,
262                }));
263            }
264            o => log::trace!("Unexpected ND RTR Solicit option {:?}", o),
265        }
266    }
267    Ok(options)
268}
269
270fn parse_nd_rtr_solicit(buf: &mut Buffer) -> Result<Icmp6, Error> {
271    let _reserved = buf.get_be32().ok_or(Error::Truncated)?;
272    Ok(Icmp6::RtrSolicit(parse_nd_rtr_options(buf)?))
273}
274
275fn parse_nd_rtr_advert(buf: &mut Buffer) -> Result<Icmp6, Error> {
276    let hop_limit = buf.get_u8().ok_or(Error::Truncated)?;
277    let mo_byte = buf.get_u8().ok_or(Error::Truncated)?;
278    let lifetime = Duration::from_secs(buf.get_be16().ok_or(Error::Truncated)?.into());
279    let reachable = Duration::from_millis(buf.get_be32().ok_or(Error::Truncated)?.into());
280    let retrans = Duration::from_millis(buf.get_be32().ok_or(Error::Truncated)?.into());
281    let options = parse_nd_rtr_options(buf)?;
282    let flag_managed = (0b1000_0000 & mo_byte) != 0;
283    let flag_other = (0b0100_0000 & mo_byte) != 0;
284
285    Ok(Icmp6::RtrAdvert(RtrAdvertisement {
286        hop_limit,
287        flag_managed,
288        flag_other,
289        lifetime,
290        reachable,
291        retrans,
292        options,
293    }))
294}
295
296pub fn parse(pkt: &[u8]) -> Result<Icmp6, Error> {
297    /* Section 6.1.1: [..] MUST silently discard [.. unless .. ] length is 8 or more octets. */
298    if pkt.len() < 8 {
299        return Err(Error::Truncated);
300    }
301    let mut buf = Buffer::new(pkt);
302    let ty = Type(buf.get_u8().ok_or(Error::Truncated)?);
303    let code = buf.get_u8().ok_or(Error::Truncated)?;
304    let _chksum = buf.get_be16().ok_or(Error::Truncated)?;
305
306    match (ty, code) {
307        (ICMP6_DESTINATION_UNREACHABLE, _) => Ok(Icmp6::Unknown), /* Ignore */
308        (ND_ROUTER_SOLICIT, 0) => parse_nd_rtr_solicit(&mut buf),
309        (ND_ROUTER_ADVERT, 0) => parse_nd_rtr_advert(&mut buf),
310        (ND_NEIGHBOR_SOLICIT, 0) => Ok(Icmp6::Unknown),
311        (ND_NEIGHBOR_ADVERT, 0) => Ok(Icmp6::Unknown),
312        (ICMP6_REDIRECT, _) => Ok(Icmp6::Unknown), /* Ignore */
313        (t, c) => {
314            log::trace!("Unexpected ICMP6 message: {:?}/{}", t, c);
315            Ok(Icmp6::Unknown) /* Ignore other ICMP codes */
316        }
317    }
318}
319
320#[derive(Default)]
321struct Serialise {
322    v: Vec<u8>,
323}
324
325impl Serialise {
326    fn serialise<T: SerialiseInto>(&mut self, value: T) {
327        value.serialise(&mut self.v)
328    }
329
330    fn len(&self) -> usize {
331        self.v.len()
332    }
333}
334
335trait SerialiseInto {
336    fn serialise(&self, v: &mut Vec<u8>);
337}
338
339impl SerialiseInto for u8 {
340    fn serialise(&self, v: &mut Vec<u8>) {
341        v.extend(self.to_be_bytes().iter())
342    }
343}
344
345impl SerialiseInto for u16 {
346    fn serialise(&self, v: &mut Vec<u8>) {
347        v.extend(self.to_be_bytes().iter())
348    }
349}
350
351impl SerialiseInto for u32 {
352    fn serialise(&self, v: &mut Vec<u8>) {
353        v.extend(self.to_be_bytes().iter())
354    }
355}
356
357impl SerialiseInto for &Vec<u8> {
358    fn serialise(&self, v: &mut Vec<u8>) {
359        v.extend(self.iter())
360    }
361}
362
363impl SerialiseInto for &std::net::Ipv6Addr {
364    fn serialise(&self, v: &mut Vec<u8>) {
365        v.extend(self.octets().iter())
366    }
367}
368
369impl SerialiseInto for &str {
370    fn serialise(&self, v: &mut Vec<u8>) {
371        v.extend(self.as_bytes())
372    }
373}
374
375fn serialise_router_advertisement(a: &RtrAdvertisement) -> Vec<u8> {
376    let mut v: Serialise = Default::default();
377    v.serialise(ND_ROUTER_ADVERT.0);
378    v.serialise(0_u8); /* Code */
379    v.serialise(0_u16); /* Checksum */
380    v.serialise(a.hop_limit);
381    v.serialise(
382        if a.flag_managed { 0x80_u8 } else { 0x00_u8 }
383            | if a.flag_other { 0x40_u8 } else { 0x00_u8 },
384    );
385    v.serialise(a.lifetime.as_secs() as u16);
386    v.serialise(a.reachable.as_millis() as u32);
387    v.serialise(a.retrans.as_millis() as u32);
388    for opt in &a.options.0 {
389        match opt {
390            NDOptionValue::SourceLLAddr(src) => {
391                use std::convert::TryFrom as _;
392                v.serialise(SOURCE_LL_ADDR.0);
393                v.serialise(u8::try_from(src.len().div_ceil(8)).unwrap());
394                v.serialise(src);
395            }
396            NDOptionValue::Mtu(mtu) => {
397                v.serialise(MTU.0);
398                v.serialise(1_u8);
399                v.serialise(0_u16);
400                v.serialise(*mtu);
401            }
402            NDOptionValue::Prefix(prefix) => {
403                v.serialise(PREFIX_INFO.0);
404                v.serialise(4_u8);
405                v.serialise(prefix.prefixlen);
406                v.serialise(
407                    if prefix.onlink { 0x80_u8 } else { 0x00_u8 }
408                        | if prefix.autonomous { 0x40_u8 } else { 0x00_u8 },
409                );
410                v.serialise(prefix.valid.as_secs() as u32);
411                v.serialise(prefix.preferred.as_secs() as u32);
412                v.serialise(0_u32);
413                v.serialise(&prefix.prefix);
414            }
415            NDOptionValue::RecursiveDnsServers((lifetime, servers)) => {
416                use std::convert::TryFrom as _;
417                v.serialise(RDNSS.0);
418                v.serialise(u8::try_from(1 + servers.len() * 2).unwrap());
419                v.serialise(0_u16); // Reserved / Padding.
420                v.serialise(lifetime.as_secs() as u32);
421                for server in servers {
422                    v.serialise(server);
423                }
424            }
425            NDOptionValue::DnsSearchList((lifetime, suffixes)) => {
426                let mut dnssl = Serialise::default();
427                for suffix in suffixes {
428                    for label in suffix.split('.') {
429                        dnssl.serialise(label.len() as u8);
430                        dnssl.serialise(label);
431                    }
432                    dnssl.serialise(0_u8);
433                }
434                // Pad with 0x00 to the full size.
435                while dnssl.v.len() % 8 != 0 {
436                    dnssl.serialise(0_u8);
437                }
438                v.serialise(DNSSL.0);
439                v.serialise(1 + (dnssl.v.len() / 8) as u8);
440                v.serialise(0_u16); // Reserved / Padding.
441                v.serialise(lifetime.as_secs() as u32);
442                v.serialise(&dnssl.v);
443            }
444            NDOptionValue::Pref64((lifetime, prefixlen, prefix)) => {
445                v.serialise(PREF64.0);
446                v.serialise(2_u8);
447                let scaled_lifetime = (lifetime.as_secs() / 8) as u16;
448                let plc = ((prefixlen - 32) / 8) as u16;
449                v.serialise((scaled_lifetime << 3) | plc);
450                for i in 0..12 {
451                    v.serialise(prefix.octets()[i])
452                }
453            }
454            NDOptionValue::CaptivePortal(url) => {
455                let mut b = url.clone().into_bytes();
456                // Pad with 0x00
457                while (b.len() + 2) % 8 != 0 {
458                    b.push(0x00_u8);
459                }
460                v.serialise(CAPTIVE_PORTAL.0);
461                v.serialise((1 + b.len() / 8) as u8);
462                v.serialise(&b);
463            }
464        }
465    }
466    assert_eq!(v.len() % 8, 0);
467    v.v
468}
469
470pub fn serialise(msg: &Icmp6) -> Vec<u8> {
471    match msg {
472        Icmp6::RtrAdvert(a) => serialise_router_advertisement(a),
473        Icmp6::Unknown => unimplemented!(),
474        Icmp6::RtrSolicit(_) => unimplemented!(),
475    }
476}
477
478#[test]
479fn test_parse_nd_rtr_solicit() {
480    let data = [133, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 2, 3, 4, 5, 6];
481    parse(&data).expect("Failed to parse");
482}
483
484#[test]
485fn test_decode_ra() {
486    let data = vec![
487        0x86, 0x00, 0xc4, 0xfe, 0x40, 0x00, 0x07, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
488        0x00, 0x01, 0x01, 0xc2, 0x00, 0x54, 0xf5, 0x00, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00,
489        0x05, 0xdc, 0x03, 0x04, 0x40, 0xc0, 0x00, 0x27, 0x8d, 0x00, 0x00, 0x09, 0x3a, 0x80, 0x00,
490        0x00, 0x00, 0x00, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
491        0x00, 0x00, 0x00, 0x00,
492    ];
493
494    let result = parse(&data);
495    assert!(result.is_ok());
496    let result_value = result.unwrap();
497    println!("{:?}", result_value);
498}
499
500#[test]
501fn test_reflexitivity() {
502    use std::time::Duration;
503    let data = serialise(&Icmp6::RtrAdvert(RtrAdvertisement {
504        hop_limit: 64,
505        flag_managed: false,
506        flag_other: false,
507        lifetime: Duration::from_secs(600),
508        reachable: Duration::from_secs(30),
509        retrans: Duration::from_secs(1),
510        options: NDOptions(vec![
511            NDOptionValue::Mtu(1480),
512            NDOptionValue::SourceLLAddr(vec![0, 1, 2, 3, 4, 5]),
513            NDOptionValue::Prefix(AdvPrefix {
514                prefixlen: 64,
515                onlink: true,
516                autonomous: true,
517                valid: Duration::from_secs(86400),
518                preferred: Duration::from_secs(3600),
519                prefix: "2001:db8::".parse().unwrap(),
520            }),
521            NDOptionValue::RecursiveDnsServers((
522                Duration::from_secs(600),
523                vec!["2001:db8::53".parse().unwrap()],
524            )),
525            NDOptionValue::DnsSearchList((
526                Duration::from_secs(600),
527                vec!["example.com".into(), "example.net".into()],
528            )),
529            NDOptionValue::CaptivePortal("http://example.com/".into()),
530        ]),
531    }));
532    parse(&data).unwrap();
533}