1use anyhow::{anyhow, bail, Context, Result};
45use serde_json::{json, Map, Value};
46use std::net::Ipv6Addr;
47
48pub const NEXT_HEADER_ICMPV6: u8 = 58;
54
55pub const TYPE_ROUTER_SOLICITATION: u8 = 133;
56pub const TYPE_ROUTER_ADVERTISEMENT: u8 = 134;
57pub const TYPE_NEIGHBOR_SOLICITATION: u8 = 135;
58pub const TYPE_NEIGHBOR_ADVERTISEMENT: u8 = 136;
59pub const TYPE_REDIRECT: u8 = 137;
60
61pub const NDP_CODE: u8 = 0;
63
64pub const OPT_SOURCE_LINK_LAYER_ADDRESS: u8 = 1;
66pub const OPT_TARGET_LINK_LAYER_ADDRESS: u8 = 2;
67pub const OPT_PREFIX_INFORMATION: u8 = 3;
68pub const OPT_MTU: u8 = 5;
69pub const OPT_RDNSS: u8 = 25;
70
71pub const PREFIX_FLAG_ON_LINK: u8 = 0x80;
73pub const PREFIX_FLAG_AUTONOMOUS: u8 = 0x40;
74
75pub const RA_FLAG_MANAGED: u8 = 0x80;
77pub const RA_FLAG_OTHER: u8 = 0x40;
78
79pub const NA_FLAG_ROUTER: u32 = 0x8000_0000;
81pub const NA_FLAG_SOLICITED: u32 = 0x4000_0000;
82pub const NA_FLAG_OVERRIDE: u32 = 0x2000_0000;
83
84pub const ALL_NODES_MULTICAST: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1);
87pub const ALL_ROUTERS_MULTICAST: Ipv6Addr = Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 2);
89
90pub const NDP_HOP_LIMIT: u32 = 255;
94
95pub const LIFETIME_INFINITE: u32 = 0xffff_ffff;
97
98pub fn parse_mac(text: &str) -> Result<[u8; 6]> {
104 let cleaned: String = text
105 .chars()
106 .filter(|c| !matches!(c, ':' | '-' | '.' | ' '))
107 .collect();
108 if cleaned.len() != 12 || !cleaned.chars().all(|c| c.is_ascii_hexdigit()) {
109 bail!(
110 "'{text}' is not a link-layer address (expected 6 hex octets, e.g. 00:11:22:33:44:55)"
111 );
112 }
113 let mut out = [0u8; 6];
114 for (i, byte) in out.iter_mut().enumerate() {
115 *byte = u8::from_str_radix(&cleaned[i * 2..i * 2 + 2], 16)
116 .with_context(|| format!("'{text}' is not a link-layer address"))?;
117 }
118 Ok(out)
119}
120
121pub fn format_mac(mac: &[u8; 6]) -> String {
123 format!(
124 "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
125 mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]
126 )
127}
128
129pub fn solicited_node_multicast(target: Ipv6Addr) -> Ipv6Addr {
135 let o = target.octets();
136 Ipv6Addr::from([
137 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01, 0xff, o[13], o[14], o[15],
138 ])
139}
140
141pub fn icmpv6_checksum(source: Ipv6Addr, destination: Ipv6Addr, message: &[u8]) -> u16 {
163 let mut sum: u32 = 0;
164
165 let mut add_pair = |hi: u8, lo: u8| {
166 sum += ((hi as u32) << 8) | lo as u32;
167 };
168
169 for chunk in source.octets().chunks(2) {
170 add_pair(chunk[0], chunk[1]);
171 }
172 for chunk in destination.octets().chunks(2) {
173 add_pair(chunk[0], chunk[1]);
174 }
175
176 let length = message.len() as u32;
179 add_pair((length >> 24) as u8, (length >> 16) as u8);
180 add_pair((length >> 8) as u8, length as u8);
181
182 add_pair(0, NEXT_HEADER_ICMPV6);
185
186 let mut chunks = message.chunks_exact(2);
187 for chunk in chunks.by_ref() {
188 add_pair(chunk[0], chunk[1]);
189 }
190 if let [last] = chunks.remainder() {
192 add_pair(*last, 0);
193 }
194
195 while sum >> 16 != 0 {
196 sum = (sum & 0xffff) + (sum >> 16);
197 }
198 !(sum as u16)
199}
200
201pub fn write_checksum(message: &mut [u8], source: Ipv6Addr, destination: Ipv6Addr) -> Result<u16> {
205 if message.len() < 4 {
206 bail!(
207 "an ICMPv6 message is at least 4 octets (type, code, checksum); got {}",
208 message.len()
209 );
210 }
211 message[2] = 0;
212 message[3] = 0;
213 let checksum = icmpv6_checksum(source, destination, message);
214 message[2..4].copy_from_slice(&checksum.to_be_bytes());
215 Ok(checksum)
216}
217
218pub fn verify_checksum(message: &[u8], source: Ipv6Addr, destination: Ipv6Addr) -> Result<()> {
224 if message.len() < 4 {
225 bail!(
226 "ICMPv6 message is {} octets, too short to check",
227 message.len()
228 );
229 }
230 let carried = u16::from_be_bytes([message[2], message[3]]);
231 let mut zeroed = message.to_vec();
232 zeroed[2] = 0;
233 zeroed[3] = 0;
234 let expected = icmpv6_checksum(source, destination, &zeroed);
235 if carried != expected {
236 bail!(
237 "ICMPv6 checksum is 0x{carried:04x} but the pseudo-header over {source} -> \
238 {destination} gives 0x{expected:04x}"
239 );
240 }
241 Ok(())
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub struct PrefixInformation {
255 pub prefix: Ipv6Addr,
256 pub prefix_length: u8,
257 pub on_link: bool,
258 pub autonomous: bool,
259 pub valid_lifetime: u32,
260 pub preferred_lifetime: u32,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
269pub enum NdpOption {
270 SourceLinkLayerAddress([u8; 6]),
271 TargetLinkLayerAddress([u8; 6]),
272 PrefixInformation(PrefixInformation),
273 Mtu(u32),
274 Rdnss {
276 lifetime: u32,
277 servers: Vec<Ipv6Addr>,
278 },
279 Other {
280 option_type: u8,
281 length_units: u8,
282 },
283}
284
285impl NdpOption {
286 pub fn option_type(&self) -> u8 {
287 match self {
288 NdpOption::SourceLinkLayerAddress(_) => OPT_SOURCE_LINK_LAYER_ADDRESS,
289 NdpOption::TargetLinkLayerAddress(_) => OPT_TARGET_LINK_LAYER_ADDRESS,
290 NdpOption::PrefixInformation(_) => OPT_PREFIX_INFORMATION,
291 NdpOption::Mtu(_) => OPT_MTU,
292 NdpOption::Rdnss { .. } => OPT_RDNSS,
293 NdpOption::Other { option_type, .. } => *option_type,
294 }
295 }
296
297 pub fn encode(&self) -> Result<Vec<u8>> {
304 let mut out = Vec::new();
305 match self {
306 NdpOption::SourceLinkLayerAddress(mac) | NdpOption::TargetLinkLayerAddress(mac) => {
307 out.push(self.option_type());
309 out.push(1);
310 out.extend_from_slice(mac);
311 }
312 NdpOption::PrefixInformation(p) => {
313 if p.prefix_length > 128 {
314 bail!(
315 "prefix length {} is impossible; an IPv6 prefix is 0-128 bits",
316 p.prefix_length
317 );
318 }
319 if p.autonomous && p.prefix_length != 64 {
322 bail!(
323 "an autonomous prefix must be a /64 — SLAAC appends a 64-bit interface \
324 identifier, so /{} leaves no room and every host would ignore it. Set \
325 autonomous to false for a non-/64 prefix.",
326 p.prefix_length
327 );
328 }
329 if p.preferred_lifetime > p.valid_lifetime {
330 bail!(
331 "preferred_lifetime ({}) must not exceed valid_lifetime ({}); RFC 4861 \
332 §4.6.2 requires a host to ignore the whole option when it does",
333 p.preferred_lifetime,
334 p.valid_lifetime
335 );
336 }
337 out.push(OPT_PREFIX_INFORMATION);
338 out.push(4); out.push(p.prefix_length);
340 let mut flags = 0u8;
341 if p.on_link {
342 flags |= PREFIX_FLAG_ON_LINK;
343 }
344 if p.autonomous {
345 flags |= PREFIX_FLAG_AUTONOMOUS;
346 }
347 out.push(flags);
348 out.extend_from_slice(&p.valid_lifetime.to_be_bytes());
349 out.extend_from_slice(&p.preferred_lifetime.to_be_bytes());
350 out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&p.prefix.octets());
352 }
353 NdpOption::Mtu(mtu) => {
354 out.push(OPT_MTU);
355 out.push(1); out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&mtu.to_be_bytes());
358 }
359 NdpOption::Rdnss { lifetime, servers } => {
360 if servers.is_empty() {
361 bail!("an RDNSS option with no servers says nothing; omit it instead");
362 }
363 let units = 1 + 2 * servers.len();
365 if units > u8::MAX as usize {
366 bail!(
367 "{} RDNSS servers need {units} 8-octet units, past the 255 the length \
368 field can express",
369 servers.len()
370 );
371 }
372 out.push(OPT_RDNSS);
373 out.push(units as u8);
374 out.extend_from_slice(&0u16.to_be_bytes()); out.extend_from_slice(&lifetime.to_be_bytes());
376 for server in servers {
377 out.extend_from_slice(&server.octets());
378 }
379 }
380 NdpOption::Other { option_type, .. } => {
381 bail!(
382 "option type {option_type} was decoded but cannot be re-encoded: its contents \
383 were deliberately not kept"
384 );
385 }
386 }
387
388 if out.len() % 8 != 0 || out.is_empty() {
392 bail!(
393 "option type {} encoded to {} octets, which is not a whole number of 8-octet \
394 units",
395 self.option_type(),
396 out.len()
397 );
398 }
399 Ok(out)
400 }
401
402 pub fn to_event_data(&self) -> Value {
404 match self {
405 NdpOption::SourceLinkLayerAddress(mac) => json!({
406 "option": "source_link_layer_address",
407 "link_layer_address": format_mac(mac),
408 }),
409 NdpOption::TargetLinkLayerAddress(mac) => json!({
410 "option": "target_link_layer_address",
411 "link_layer_address": format_mac(mac),
412 }),
413 NdpOption::PrefixInformation(p) => json!({
414 "option": "prefix_information",
415 "prefix": p.prefix.to_string(),
416 "length": p.prefix_length,
417 "on_link": p.on_link,
418 "autonomous": p.autonomous,
419 "valid_lifetime": p.valid_lifetime,
420 "preferred_lifetime": p.preferred_lifetime,
421 }),
422 NdpOption::Mtu(mtu) => json!({"option": "mtu", "mtu": mtu}),
423 NdpOption::Rdnss { lifetime, servers } => json!({
424 "option": "rdnss",
425 "lifetime": lifetime,
426 "servers": servers.iter().map(|s| s.to_string()).collect::<Vec<_>>(),
427 }),
428 NdpOption::Other {
429 option_type,
430 length_units,
431 } => json!({
432 "option": "unrecognised",
433 "option_type": option_type,
434 "length_units": length_units,
435 }),
436 }
437 }
438}
439
440pub fn decode_options(mut data: &[u8]) -> Result<Vec<NdpOption>> {
446 let mut out = Vec::new();
447 while !data.is_empty() {
448 if data.len() < 2 {
449 bail!("option header is truncated: {} octet(s) remain", data.len());
450 }
451 let option_type = data[0];
452 let units = data[1];
453 if units == 0 {
454 bail!("option type {option_type} declares length 0, which RFC 4861 §4.6 forbids");
455 }
456 let total = units as usize * 8;
457 if total > data.len() {
458 bail!(
459 "option type {option_type} declares {units} 8-octet units ({total} octets) but \
460 only {} remain",
461 data.len()
462 );
463 }
464 let body = &data[2..total];
465 data = &data[total..];
466
467 let option = match (option_type, units) {
468 (OPT_SOURCE_LINK_LAYER_ADDRESS, 1) => {
469 let mut mac = [0u8; 6];
470 mac.copy_from_slice(&body[..6]);
471 NdpOption::SourceLinkLayerAddress(mac)
472 }
473 (OPT_TARGET_LINK_LAYER_ADDRESS, 1) => {
474 let mut mac = [0u8; 6];
475 mac.copy_from_slice(&body[..6]);
476 NdpOption::TargetLinkLayerAddress(mac)
477 }
478 (OPT_PREFIX_INFORMATION, 4) => {
479 let mut prefix = [0u8; 16];
480 prefix.copy_from_slice(&body[14..30]);
481 NdpOption::PrefixInformation(PrefixInformation {
482 prefix_length: body[0],
483 on_link: body[1] & PREFIX_FLAG_ON_LINK != 0,
484 autonomous: body[1] & PREFIX_FLAG_AUTONOMOUS != 0,
485 valid_lifetime: u32::from_be_bytes([body[2], body[3], body[4], body[5]]),
486 preferred_lifetime: u32::from_be_bytes([body[6], body[7], body[8], body[9]]),
487 prefix: Ipv6Addr::from(prefix),
488 })
489 }
490 (OPT_MTU, 1) => {
491 NdpOption::Mtu(u32::from_be_bytes([body[2], body[3], body[4], body[5]]))
492 }
493 (OPT_RDNSS, units) if units >= 3 && units % 2 == 1 => {
495 let lifetime = u32::from_be_bytes([body[2], body[3], body[4], body[5]]);
496 let mut servers = Vec::new();
497 for chunk in body[6..].chunks_exact(16) {
498 let mut addr = [0u8; 16];
499 addr.copy_from_slice(chunk);
500 servers.push(Ipv6Addr::from(addr));
501 }
502 NdpOption::Rdnss { lifetime, servers }
503 }
504 (option_type, length_units) => NdpOption::Other {
509 option_type,
510 length_units,
511 },
512 };
513 out.push(option);
514 }
515 Ok(out)
516}
517
518#[derive(Debug, Clone, PartialEq, Eq)]
528pub struct RouterAdvertisement {
529 pub cur_hop_limit: u8,
531 pub managed: bool,
533 pub other: bool,
535 pub router_lifetime: u16,
538 pub reachable_time: u32,
540 pub retrans_timer: u32,
542 pub options: Vec<NdpOption>,
543}
544
545#[derive(Debug, Clone, PartialEq, Eq)]
547pub enum NdpMessage {
548 RouterSolicitation {
549 options: Vec<NdpOption>,
550 },
551 RouterAdvertisement(RouterAdvertisement),
552 NeighborSolicitation {
553 target: Ipv6Addr,
554 options: Vec<NdpOption>,
555 },
556 NeighborAdvertisement {
557 router: bool,
558 solicited: bool,
559 override_flag: bool,
560 target: Ipv6Addr,
561 options: Vec<NdpOption>,
562 },
563 Redirect {
564 target: Ipv6Addr,
566 destination: Ipv6Addr,
567 options: Vec<NdpOption>,
568 },
569}
570
571impl NdpMessage {
572 pub fn message_type(&self) -> u8 {
573 match self {
574 NdpMessage::RouterSolicitation { .. } => TYPE_ROUTER_SOLICITATION,
575 NdpMessage::RouterAdvertisement(_) => TYPE_ROUTER_ADVERTISEMENT,
576 NdpMessage::NeighborSolicitation { .. } => TYPE_NEIGHBOR_SOLICITATION,
577 NdpMessage::NeighborAdvertisement { .. } => TYPE_NEIGHBOR_ADVERTISEMENT,
578 NdpMessage::Redirect { .. } => TYPE_REDIRECT,
579 }
580 }
581
582 pub fn type_name(&self) -> &'static str {
583 match self {
584 NdpMessage::RouterSolicitation { .. } => "router_solicitation",
585 NdpMessage::RouterAdvertisement(_) => "router_advertisement",
586 NdpMessage::NeighborSolicitation { .. } => "neighbor_solicitation",
587 NdpMessage::NeighborAdvertisement { .. } => "neighbor_advertisement",
588 NdpMessage::Redirect { .. } => "redirect",
589 }
590 }
591
592 pub fn options(&self) -> &[NdpOption] {
593 match self {
594 NdpMessage::RouterSolicitation { options }
595 | NdpMessage::NeighborSolicitation { options, .. }
596 | NdpMessage::NeighborAdvertisement { options, .. }
597 | NdpMessage::Redirect { options, .. } => options,
598 NdpMessage::RouterAdvertisement(ra) => &ra.options,
599 }
600 }
601
602 fn options_mut(&mut self) -> &mut Vec<NdpOption> {
603 match self {
604 NdpMessage::RouterSolicitation { options }
605 | NdpMessage::NeighborSolicitation { options, .. }
606 | NdpMessage::NeighborAdvertisement { options, .. }
607 | NdpMessage::Redirect { options, .. } => options,
608 NdpMessage::RouterAdvertisement(ra) => &mut ra.options,
609 }
610 }
611
612 pub fn source_link_layer(&self) -> Option<[u8; 6]> {
614 self.options().iter().find_map(|o| match o {
615 NdpOption::SourceLinkLayerAddress(mac) => Some(*mac),
616 _ => None,
617 })
618 }
619
620 pub fn target_link_layer(&self) -> Option<[u8; 6]> {
622 self.options().iter().find_map(|o| match o {
623 NdpOption::TargetLinkLayerAddress(mac) => Some(*mac),
624 _ => None,
625 })
626 }
627
628 pub fn encode(&self, source: Ipv6Addr, destination: Ipv6Addr) -> Result<Vec<u8>> {
634 let mut out = self.encode_without_checksum()?;
635 write_checksum(&mut out, source, destination)?;
636 Ok(out)
637 }
638
639 pub fn encode_without_checksum(&self) -> Result<Vec<u8>> {
645 let mut out = Vec::with_capacity(64);
646 out.push(self.message_type());
647 out.push(NDP_CODE);
648 out.extend_from_slice(&[0, 0]); match self {
651 NdpMessage::RouterSolicitation { .. } => {
652 out.extend_from_slice(&0u32.to_be_bytes()); }
654 NdpMessage::RouterAdvertisement(ra) => {
655 out.push(ra.cur_hop_limit);
656 let mut flags = 0u8;
657 if ra.managed {
658 flags |= RA_FLAG_MANAGED;
659 }
660 if ra.other {
661 flags |= RA_FLAG_OTHER;
662 }
663 out.push(flags);
664 out.extend_from_slice(&ra.router_lifetime.to_be_bytes());
665 out.extend_from_slice(&ra.reachable_time.to_be_bytes());
666 out.extend_from_slice(&ra.retrans_timer.to_be_bytes());
667 }
668 NdpMessage::NeighborSolicitation { target, .. } => {
669 out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&target.octets());
671 }
672 NdpMessage::NeighborAdvertisement {
673 router,
674 solicited,
675 override_flag,
676 target,
677 ..
678 } => {
679 let mut flags = 0u32;
680 if *router {
681 flags |= NA_FLAG_ROUTER;
682 }
683 if *solicited {
684 flags |= NA_FLAG_SOLICITED;
685 }
686 if *override_flag {
687 flags |= NA_FLAG_OVERRIDE;
688 }
689 out.extend_from_slice(&flags.to_be_bytes());
690 out.extend_from_slice(&target.octets());
691 }
692 NdpMessage::Redirect {
693 target,
694 destination,
695 ..
696 } => {
697 out.extend_from_slice(&0u32.to_be_bytes()); out.extend_from_slice(&target.octets());
699 out.extend_from_slice(&destination.octets());
700 }
701 }
702
703 for option in self.options() {
704 out.extend_from_slice(&option.encode()?);
705 }
706 Ok(out)
707 }
708
709 pub fn decode(data: &[u8]) -> Result<Self> {
714 if data.len() < 4 {
715 bail!(
716 "ICMPv6 message is {} octet(s); the header alone is 4",
717 data.len()
718 );
719 }
720 let message_type = data[0];
721 let code = data[1];
722 if code != NDP_CODE {
723 bail!(
724 "ICMPv6 type {message_type} carries code {code}; RFC 4861 requires code 0 for \
725 every Neighbour Discovery message and a receiver discards anything else"
726 );
727 }
728 let body = &data[4..];
729
730 let need = |want: usize, what: &str| -> Result<()> {
731 if body.len() < want {
732 bail!(
733 "{what} needs {} octets after the ICMPv6 header, got {}",
734 want,
735 body.len()
736 );
737 }
738 Ok(())
739 };
740
741 let addr_at = |offset: usize| -> Ipv6Addr {
742 let mut octets = [0u8; 16];
743 octets.copy_from_slice(&body[offset..offset + 16]);
744 Ipv6Addr::from(octets)
745 };
746
747 match message_type {
748 TYPE_ROUTER_SOLICITATION => {
749 need(4, "a Router Solicitation")?;
750 Ok(NdpMessage::RouterSolicitation {
751 options: decode_options(&body[4..])?,
752 })
753 }
754 TYPE_ROUTER_ADVERTISEMENT => {
755 need(12, "a Router Advertisement")?;
756 Ok(NdpMessage::RouterAdvertisement(RouterAdvertisement {
757 cur_hop_limit: body[0],
758 managed: body[1] & RA_FLAG_MANAGED != 0,
759 other: body[1] & RA_FLAG_OTHER != 0,
760 router_lifetime: u16::from_be_bytes([body[2], body[3]]),
761 reachable_time: u32::from_be_bytes([body[4], body[5], body[6], body[7]]),
762 retrans_timer: u32::from_be_bytes([body[8], body[9], body[10], body[11]]),
763 options: decode_options(&body[12..])?,
764 }))
765 }
766 TYPE_NEIGHBOR_SOLICITATION => {
767 need(20, "a Neighbour Solicitation")?;
768 Ok(NdpMessage::NeighborSolicitation {
769 target: addr_at(4),
770 options: decode_options(&body[20..])?,
771 })
772 }
773 TYPE_NEIGHBOR_ADVERTISEMENT => {
774 need(20, "a Neighbour Advertisement")?;
775 let flags = u32::from_be_bytes([body[0], body[1], body[2], body[3]]);
776 Ok(NdpMessage::NeighborAdvertisement {
777 router: flags & NA_FLAG_ROUTER != 0,
778 solicited: flags & NA_FLAG_SOLICITED != 0,
779 override_flag: flags & NA_FLAG_OVERRIDE != 0,
780 target: addr_at(4),
781 options: decode_options(&body[20..])?,
782 })
783 }
784 TYPE_REDIRECT => {
785 need(36, "a Redirect")?;
786 Ok(NdpMessage::Redirect {
787 target: addr_at(4),
788 destination: addr_at(20),
789 options: decode_options(&body[36..])?,
790 })
791 }
792 other => bail!(
793 "ICMPv6 type {other} is not a Neighbour Discovery message (133 Router \
794 Solicitation, 134 Router Advertisement, 135 Neighbour Solicitation, 136 \
795 Neighbour Advertisement, 137 Redirect)"
796 ),
797 }
798 }
799
800 pub fn to_event_data(&self) -> Map<String, Value> {
806 let mut map = Map::new();
807 map.insert("message_type".into(), json!(self.type_name()));
808 map.insert("icmpv6_type".into(), json!(self.message_type()));
809
810 match self {
811 NdpMessage::RouterSolicitation { .. } => {}
812 NdpMessage::RouterAdvertisement(ra) => {
813 map.insert("cur_hop_limit".into(), json!(ra.cur_hop_limit));
814 map.insert("managed".into(), json!(ra.managed));
815 map.insert("other".into(), json!(ra.other));
816 map.insert("router_lifetime".into(), json!(ra.router_lifetime));
817 map.insert("reachable_time".into(), json!(ra.reachable_time));
818 map.insert("retrans_timer".into(), json!(ra.retrans_timer));
819
820 let prefixes: Vec<Value> = ra
821 .options
822 .iter()
823 .filter_map(|o| match o {
824 NdpOption::PrefixInformation(p) => Some(json!({
825 "prefix": p.prefix.to_string(),
826 "length": p.prefix_length,
827 "on_link": p.on_link,
828 "autonomous": p.autonomous,
829 "valid_lifetime": p.valid_lifetime,
830 "preferred_lifetime": p.preferred_lifetime,
831 })),
832 _ => None,
833 })
834 .collect();
835 if !prefixes.is_empty() {
836 map.insert("prefixes".into(), json!(prefixes));
837 }
838 let rdnss: Vec<String> = ra
839 .options
840 .iter()
841 .flat_map(|o| match o {
842 NdpOption::Rdnss { servers, .. } => {
843 servers.iter().map(|s| s.to_string()).collect()
844 }
845 _ => Vec::new(),
846 })
847 .collect();
848 if !rdnss.is_empty() {
849 map.insert("rdnss".into(), json!(rdnss));
850 }
851 if let Some(mtu) = ra.options.iter().find_map(|o| match o {
852 NdpOption::Mtu(m) => Some(*m),
853 _ => None,
854 }) {
855 map.insert("mtu".into(), json!(mtu));
856 }
857 }
858 NdpMessage::NeighborSolicitation { target, .. } => {
859 map.insert("target_address".into(), json!(target.to_string()));
860 map.insert(
861 "solicited_node_multicast".into(),
862 json!(solicited_node_multicast(*target).to_string()),
863 );
864 }
865 NdpMessage::NeighborAdvertisement {
866 router,
867 solicited,
868 override_flag,
869 target,
870 ..
871 } => {
872 map.insert("target_address".into(), json!(target.to_string()));
873 map.insert("router".into(), json!(router));
874 map.insert("solicited".into(), json!(solicited));
875 map.insert("override".into(), json!(override_flag));
876 }
877 NdpMessage::Redirect {
878 target,
879 destination,
880 ..
881 } => {
882 map.insert("target_address".into(), json!(target.to_string()));
883 map.insert("destination_address".into(), json!(destination.to_string()));
884 }
885 }
886
887 if let Some(mac) = self.source_link_layer() {
888 map.insert("source_link_layer".into(), json!(format_mac(&mac)));
889 }
890 if let Some(mac) = self.target_link_layer() {
891 map.insert("target_link_layer".into(), json!(format_mac(&mac)));
892 }
893 let options: Vec<Value> = self.options().iter().map(|o| o.to_event_data()).collect();
894 if !options.is_empty() {
895 map.insert("options".into(), json!(options));
896 }
897 map
898 }
899}
900
901#[derive(Debug, Clone, PartialEq, Eq)]
912pub struct SendRequest {
913 pub message: NdpMessage,
914 pub source: Option<Ipv6Addr>,
915 pub destination: Option<Ipv6Addr>,
916}
917
918impl SendRequest {
919 pub fn from_action(action: &Value) -> Result<Self> {
925 let action_type = action
926 .get("type")
927 .and_then(Value::as_str)
928 .ok_or_else(|| anyhow!("action has no 'type'"))?;
929
930 let message = match action_type {
931 "send_router_advertisement" => Self::router_advertisement(action)?,
932 "send_neighbor_advertisement" => Self::neighbor_advertisement(action)?,
933 "send_neighbor_solicitation" => Self::neighbor_solicitation(action)?,
934 other => bail!("'{other}' does not describe an NDP message this server can transmit"),
935 };
936
937 message.encode_without_checksum()?;
941
942 Ok(Self {
943 message,
944 source: optional_addr(action, "source_address")?,
945 destination: optional_addr(action, "destination_address")?,
946 })
947 }
948
949 pub fn with_default_link_layer(mut self, mac: [u8; 6]) -> Self {
956 let wanted = match &self.message {
957 NdpMessage::NeighborAdvertisement { .. } => OPT_TARGET_LINK_LAYER_ADDRESS,
958 NdpMessage::RouterAdvertisement(_) | NdpMessage::NeighborSolicitation { .. } => {
959 OPT_SOURCE_LINK_LAYER_ADDRESS
960 }
961 _ => return self,
962 };
963 if self
964 .message
965 .options()
966 .iter()
967 .any(|o| o.option_type() == wanted)
968 {
969 return self;
970 }
971 let option = if wanted == OPT_TARGET_LINK_LAYER_ADDRESS {
972 NdpOption::TargetLinkLayerAddress(mac)
973 } else {
974 NdpOption::SourceLinkLayerAddress(mac)
975 };
976 self.message.options_mut().push(option);
977 self
978 }
979
980 fn router_advertisement(action: &Value) -> Result<NdpMessage> {
981 let mut options = Vec::new();
982
983 for (index, entry) in array_field(action, "prefixes")?.iter().enumerate() {
984 let prefix_text = entry
985 .get("prefix")
986 .and_then(Value::as_str)
987 .ok_or_else(|| anyhow!("prefixes[{index}] has no 'prefix'"))?;
988 let (base, slash_length) = match prefix_text.split_once('/') {
991 Some((addr, len)) => (
992 addr,
993 Some(len.trim().parse::<u8>().with_context(|| {
994 format!(
995 "prefixes[{index}] has an unreadable prefix length in '{prefix_text}'"
996 )
997 })?),
998 ),
999 None => (prefix_text, None),
1000 };
1001 let prefix = base.trim().parse::<Ipv6Addr>().with_context(|| {
1002 format!("prefixes[{index}] prefix '{base}' is not an IPv6 address")
1003 })?;
1004 let prefix_length = match entry.get("length") {
1005 Some(v) => u8::try_from(
1006 v.as_u64()
1007 .ok_or_else(|| anyhow!("prefixes[{index}].length must be a number"))?,
1008 )
1009 .map_err(|_| anyhow!("prefixes[{index}].length must be 0-128"))?,
1010 None => slash_length.unwrap_or(64),
1011 };
1012
1013 options.push(NdpOption::PrefixInformation(PrefixInformation {
1014 prefix,
1015 prefix_length,
1016 on_link: bool_field(entry, "on_link")?.unwrap_or(true),
1017 autonomous: bool_field(entry, "autonomous")?.unwrap_or(true),
1018 valid_lifetime: u32_field(entry, "valid_lifetime")?.unwrap_or(2_592_000),
1019 preferred_lifetime: u32_field(entry, "preferred_lifetime")?.unwrap_or(604_800),
1020 }));
1021 }
1022
1023 if let Some(mtu) = u32_field(action, "mtu")? {
1024 if mtu < 1280 {
1025 bail!("an MTU of {mtu} is below IPv6's minimum link MTU of 1280 (RFC 8200 §5)");
1026 }
1027 options.push(NdpOption::Mtu(mtu));
1028 }
1029
1030 let servers = array_field(action, "rdnss")?;
1031 if !servers.is_empty() {
1032 let mut resolved = Vec::with_capacity(servers.len());
1033 for (index, entry) in servers.iter().enumerate() {
1034 let text = entry
1035 .as_str()
1036 .ok_or_else(|| anyhow!("rdnss[{index}] must be an IPv6 address string"))?;
1037 resolved.push(
1038 text.trim().parse::<Ipv6Addr>().with_context(|| {
1039 format!("rdnss[{index}] '{text}' is not an IPv6 address")
1040 })?,
1041 );
1042 }
1043 options.push(NdpOption::Rdnss {
1044 lifetime: u32_field(action, "rdnss_lifetime")?.unwrap_or(600),
1045 servers: resolved,
1046 });
1047 }
1048
1049 if let Some(mac) = mac_field(action, "source_link_layer")? {
1050 options.push(NdpOption::SourceLinkLayerAddress(mac));
1051 }
1052
1053 let router_lifetime = match u32_field(action, "router_lifetime")? {
1054 None => 1800,
1055 Some(v) => u16::try_from(v)
1056 .map_err(|_| anyhow!("router_lifetime is 16 bits: 0-65535 seconds, got {v}"))?,
1057 };
1058
1059 Ok(NdpMessage::RouterAdvertisement(RouterAdvertisement {
1060 cur_hop_limit: match u32_field(action, "hop_limit")? {
1061 None => 64,
1062 Some(v) => u8::try_from(v)
1063 .map_err(|_| anyhow!("hop_limit is one octet: 0-255, got {v}"))?,
1064 },
1065 managed: bool_field(action, "managed")?.unwrap_or(false),
1066 other: bool_field(action, "other")?.unwrap_or(false),
1067 router_lifetime,
1068 reachable_time: u32_field(action, "reachable_time")?.unwrap_or(0),
1069 retrans_timer: u32_field(action, "retrans_timer")?.unwrap_or(0),
1070 options,
1071 }))
1072 }
1073
1074 fn neighbor_advertisement(action: &Value) -> Result<NdpMessage> {
1075 let target = required_addr(action, "target")?;
1076 let mut options = Vec::new();
1077 if let Some(mac) = mac_field(action, "target_link_layer")? {
1078 options.push(NdpOption::TargetLinkLayerAddress(mac));
1079 }
1080 Ok(NdpMessage::NeighborAdvertisement {
1081 router: bool_field(action, "router")?.unwrap_or(false),
1082 solicited: bool_field(action, "solicited")?.unwrap_or(true),
1085 override_flag: bool_field(action, "override")?.unwrap_or(true),
1086 target,
1087 options,
1088 })
1089 }
1090
1091 fn neighbor_solicitation(action: &Value) -> Result<NdpMessage> {
1092 let target = required_addr(action, "target")?;
1093 let mut options = Vec::new();
1094 if let Some(mac) = mac_field(action, "source_link_layer")? {
1095 options.push(NdpOption::SourceLinkLayerAddress(mac));
1096 }
1097 Ok(NdpMessage::NeighborSolicitation { target, options })
1098 }
1099
1100 pub fn default_destination(&self, peer: Option<Ipv6Addr>) -> Ipv6Addr {
1104 let peer = peer.filter(|p| !p.is_unspecified());
1107 match &self.message {
1108 NdpMessage::RouterAdvertisement(_) => peer.unwrap_or(ALL_NODES_MULTICAST),
1109 NdpMessage::NeighborAdvertisement { .. } => peer.unwrap_or(ALL_NODES_MULTICAST),
1110 NdpMessage::NeighborSolicitation { target, .. } => solicited_node_multicast(*target),
1113 NdpMessage::RouterSolicitation { .. } => ALL_ROUTERS_MULTICAST,
1114 NdpMessage::Redirect { .. } => peer.unwrap_or(ALL_NODES_MULTICAST),
1115 }
1116 }
1117}
1118
1119fn required_addr(action: &Value, key: &str) -> Result<Ipv6Addr> {
1124 let text = action
1125 .get(key)
1126 .and_then(Value::as_str)
1127 .ok_or_else(|| anyhow!("'{key}' is required and must be an IPv6 address string"))?;
1128 text.trim()
1129 .parse::<Ipv6Addr>()
1130 .with_context(|| format!("'{key}' value '{text}' is not an IPv6 address"))
1131}
1132
1133fn optional_addr(action: &Value, key: &str) -> Result<Option<Ipv6Addr>> {
1134 match action.get(key) {
1135 None | Some(Value::Null) => Ok(None),
1136 Some(Value::String(s)) => {
1137 Ok(Some(s.trim().parse::<Ipv6Addr>().with_context(|| {
1138 format!("'{key}' value '{s}' is not an IPv6 address")
1139 })?))
1140 }
1141 Some(other) => bail!("'{key}' must be an IPv6 address string, got {other}"),
1142 }
1143}
1144
1145fn mac_field(action: &Value, key: &str) -> Result<Option<[u8; 6]>> {
1146 match action.get(key) {
1147 None | Some(Value::Null) => Ok(None),
1148 Some(Value::String(s)) => Ok(Some(parse_mac(s)?)),
1149 Some(other) => bail!("'{key}' must be a link-layer address string, got {other}"),
1150 }
1151}
1152
1153fn bool_field(action: &Value, key: &str) -> Result<Option<bool>> {
1154 match action.get(key) {
1155 None | Some(Value::Null) => Ok(None),
1156 Some(Value::Bool(b)) => Ok(Some(*b)),
1157 Some(other) => bail!("'{key}' must be true or false, got {other}"),
1158 }
1159}
1160
1161fn u32_field(action: &Value, key: &str) -> Result<Option<u32>> {
1162 match action.get(key) {
1163 None | Some(Value::Null) => Ok(None),
1164 Some(v) => {
1165 let n = v
1166 .as_u64()
1167 .ok_or_else(|| anyhow!("'{key}' must be a whole non-negative number, got {v}"))?;
1168 Ok(Some(u32::try_from(n).map_err(|_| {
1169 anyhow!("'{key}' must fit in 32 bits, got {n}")
1170 })?))
1171 }
1172 }
1173}
1174
1175fn array_field(action: &Value, key: &str) -> Result<Vec<Value>> {
1176 match action.get(key) {
1177 None | Some(Value::Null) => Ok(Vec::new()),
1178 Some(Value::Array(items)) => Ok(items.clone()),
1179 Some(other) => bail!("'{key}' must be an array, got {other}"),
1180 }
1181}
1182
1183pub const ADDRESSED_PREFIX_LEN: usize = 32;
1189
1190pub fn encode_addressed(source: Ipv6Addr, destination: Ipv6Addr, message: &[u8]) -> Vec<u8> {
1200 let mut out = Vec::with_capacity(ADDRESSED_PREFIX_LEN + message.len());
1201 out.extend_from_slice(&source.octets());
1202 out.extend_from_slice(&destination.octets());
1203 out.extend_from_slice(message);
1204 out
1205}
1206
1207pub fn decode_addressed(datagram: &[u8]) -> Result<(Ipv6Addr, Ipv6Addr, &[u8])> {
1209 if datagram.len() < ADDRESSED_PREFIX_LEN + 4 {
1210 bail!(
1211 "datagram is {} octets; the UDP test transport carries 16 octets of source address, \
1212 16 of destination and then at least a 4-octet ICMPv6 header",
1213 datagram.len()
1214 );
1215 }
1216 let mut source = [0u8; 16];
1217 source.copy_from_slice(&datagram[0..16]);
1218 let mut destination = [0u8; 16];
1219 destination.copy_from_slice(&datagram[16..32]);
1220 Ok((
1221 Ipv6Addr::from(source),
1222 Ipv6Addr::from(destination),
1223 &datagram[ADDRESSED_PREFIX_LEN..],
1224 ))
1225}