Skip to content

Commit

Permalink
Update PR #602: implement join_multicast_group() for IPv6
Browse files Browse the repository at this point in the history
This patch rebases @jgallagher's to the tip of the main branch,
adding support for IPv6 multicast groups. As in the original PR,
it is only possible to join groups by sending an initial MLDv2
Report packet. It is not yet possible to resend the change report,
leave groups, respond to queries, etc.
  • Loading branch information
lucasvr committed Apr 2, 2024
1 parent 4c27918 commit d5939b8
Show file tree
Hide file tree
Showing 16 changed files with 469 additions and 22 deletions.
8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -218,10 +218,10 @@ reassembly-buffer-count-8 = []
reassembly-buffer-count-16 = []
reassembly-buffer-count-32 = []

ipv6-hbh-max-options-1 = [] # Default
ipv6-hbh-max-options-1 = []
ipv6-hbh-max-options-2 = []
ipv6-hbh-max-options-3 = []
ipv6-hbh-max-options-4 = []
ipv6-hbh-max-options-4 = [] # Default
ipv6-hbh-max-options-8 = []
ipv6-hbh-max-options-16 = []
ipv6-hbh-max-options-32 = []
Expand Down Expand Up @@ -296,6 +296,10 @@ required-features = ["log", "medium-ethernet", "proto-ipv4", "socket-tcp"]
name = "multicast"
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "proto-igmp", "socket-udp"]

[[example]]
name = "multicast6"
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv6", "socket-udp"]

[[example]]
name = "benchmark"
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-raw", "socket-udp"]
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ Maximum length of DNS names that can be queried. Default: 255.

### IPV6_HBH_MAX_OPTIONS

The maximum amount of parsed options the IPv6 Hop-by-Hop header can hold. Default: 1.
The maximum amount of parsed options the IPv6 Hop-by-Hop header can hold. Default: 4.

## Hosted usage examples

Expand Down
2 changes: 1 addition & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ static CONFIGS: &[(&str, usize)] = &[
("ASSEMBLER_MAX_SEGMENT_COUNT", 4),
("REASSEMBLY_BUFFER_SIZE", 1500),
("REASSEMBLY_BUFFER_COUNT", 1),
("IPV6_HBH_MAX_OPTIONS", 1),
("IPV6_HBH_MAX_OPTIONS", 4),
("DNS_MAX_RESULT_COUNT", 1),
("DNS_MAX_SERVER_COUNT", 1),
("DNS_MAX_NAME_SIZE", 255),
Expand Down
4 changes: 2 additions & 2 deletions ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ FEATURES_TEST=(
"std,medium-ethernet,proto-ipv4,proto-igmp,socket-raw,socket-dns"
"std,medium-ethernet,proto-ipv4,socket-udp,socket-tcp,socket-dns"
"std,medium-ethernet,proto-ipv4,proto-dhcpv4,socket-udp"
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,socket-udp,socket-dns"
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,proto-igmp,socket-udp,socket-dns"
"std,medium-ethernet,proto-ipv6,socket-tcp"
"std,medium-ethernet,medium-ip,proto-ipv4,socket-icmp,socket-tcp"
"std,medium-ip,proto-ipv6,socket-icmp,socket-tcp"
"std,medium-ieee802154,proto-sixlowpan,socket-udp"
"std,medium-ieee802154,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
"std,medium-ieee802154,proto-rpl,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
"std,medium-ip,proto-ipv4,proto-ipv6,socket-tcp,socket-udp"
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,proto-igmp,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
"std,medium-ieee802154,medium-ip,proto-ipv4,socket-raw"
"std,medium-ethernet,proto-ipv4,proto-ipsec,socket-raw"
)
Expand Down
2 changes: 1 addition & 1 deletion gen_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def feature(name, default, min, max, pow2=None):
feature("assembler_max_segment_count", default=4, min=1, max=32, pow2=4)
feature("reassembly_buffer_size", default=1500, min=256, max=65536, pow2=True)
feature("reassembly_buffer_count", default=1, min=1, max=32, pow2=4)
feature("ipv6_hbh_max_options", default=1, min=1, max=32, pow2=4)
feature("ipv6_hbh_max_options", default=4, min=1, max=32, pow2=4)
feature("dns_max_result_count", default=1, min=1, max=32, pow2=4)
feature("dns_max_server_count", default=1, min=1, max=32, pow2=4)
feature("dns_max_name_size", default=255, min=64, max=255, pow2=True)
Expand Down
41 changes: 39 additions & 2 deletions src/iface/interface/igmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum MulticastError {
GroupTableFull,
/// IPv6 multicast is not yet supported.
Ipv6NotSupported,
/// Cannot join/leave the given multicast group.
Unaddressable,
}

impl core::fmt::Display for MulticastError {
Expand All @@ -18,6 +20,7 @@ impl core::fmt::Display for MulticastError {
MulticastError::Exhausted => write!(f, "Exhausted"),
MulticastError::GroupTableFull => write!(f, "GroupTableFull"),
MulticastError::Ipv6NotSupported => write!(f, "Ipv6NotSupported"),
MulticastError::Unaddressable => write!(f, "Unaddressable"),
}
}
}
Expand Down Expand Up @@ -68,9 +71,43 @@ impl Interface {
Ok(false)
}
}
// Multicast is not yet implemented for other address families
#[cfg(feature = "proto-ipv6")]
IpAddress::Ipv6(addr) => {
// Build report packet containing this new address
let initial_report_record: &[MldAddressRecordRepr] = &[MldAddressRecordRepr {
num_srcs: 0,
mcast_addr: addr,
record_type: MldRecordType::ChangeToInclude,
aux_data_len: 0,
payload: &[],
}];

let is_not_new = self
.inner
.ipv6_multicast_groups
.insert(addr, ())
.map_err(|_| MulticastError::GroupTableFull)?
.is_some();
if is_not_new {
Ok(false)
} else if let Some(pkt) = self.inner.mldv2_report_packet(initial_report_record) {
// Send initial membership report
let tx_token = device
.transmit(timestamp)
.ok_or(MulticastError::Exhausted)?;

// NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
self.inner
.dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
.unwrap();

Ok(true)
} else {
Ok(false)
}
}
#[allow(unreachable_patterns)]
_ => Err(MulticastError::Ipv6NotSupported),
_ => Err(MulticastError::Unaddressable),
}
}

Expand Down
65 changes: 64 additions & 1 deletion src/iface/interface/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ impl InterfaceInner {

for opt_repr in &hbh_repr.options {
match opt_repr {
Ipv6OptionRepr::Pad1 | Ipv6OptionRepr::PadN(_) => (),
Ipv6OptionRepr::Pad1 | Ipv6OptionRepr::PadN(_) | Ipv6OptionRepr::RouterAlert(_) => {}
#[cfg(feature = "proto-rpl")]
Ipv6OptionRepr::Rpl(_) => {}

Expand Down Expand Up @@ -472,4 +472,67 @@ impl InterfaceInner {
IpPayload::Icmpv6(icmp_repr),
))
}

pub(super) fn mldv2_report_packet<'any>(
&self,
records: &'any [MldAddressRecordRepr<'any>],
) -> Option<Packet<'any>> {
// Per [RFC 3810 § 5.2.13], source addresses must be link-local, falling
// back to the unspecified address if we haven't acquired one.
// [RFC 3810 § 5.2.13]: https://tools.ietf.org/html/rfc3810#section-5.2.13
let src_addr = self
.link_local_ipv6_address()
.unwrap_or(Ipv6Address::UNSPECIFIED);

// Per [RFC 3810 § 5.2.14], all MLDv2 reports are sent to ff02::16.
// [RFC 3810 § 5.2.14]: https://tools.ietf.org/html/rfc3810#section-5.2.14
let dst_addr = Ipv6Address::LINK_LOCAL_ALL_MLDV2_ROUTERS;

// Create a dummy IPv6 extension header so we can calculate the total length of the packet.
// The actual extension header will be created later by Packet::emit_payload().
let dummy_ext_hdr = Ipv6ExtHeaderRepr {
next_header: IpProtocol::Unknown(0),
length: 0,
data: &[],
};

let hbh_repr = Ipv6HopByHopRepr::mldv2_router_alert(0);
let mld_repr = MldRepr::ReportRecordReprs(records);
let records_len = records
.iter()
.map(MldAddressRecordRepr::buffer_len)
.sum::<usize>();

// All MLDv2 messages must be sent with an IPv6 Hop limit of 1.
Some(Packet::new_ipv6(
Ipv6Repr {
src_addr,
dst_addr,
next_header: IpProtocol::HopByHop,
payload_len: dummy_ext_hdr.header_len()
+ hbh_repr.buffer_len()
+ mld_repr.buffer_len()
+ records_len,
hop_limit: 1,
},
IpPayload::HopByHopIcmpv6(hbh_repr, Icmpv6Repr::Mld(mld_repr)),
))
}

/// Get the first link-local IPv6 address of the interface, if present.
fn link_local_ipv6_address(&self) -> Option<Ipv6Address> {
self.ip_addrs.iter().find_map(|addr| match *addr {
#[cfg(feature = "proto-ipv4")]
IpCidr::Ipv4(_) => None,
#[cfg(feature = "proto-ipv6")]
IpCidr::Ipv6(cidr) => {
let addr = cidr.address();
if addr.is_link_local() {
Some(addr)
} else {
None
}
}
})
}
}
12 changes: 9 additions & 3 deletions src/iface/interface/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ pub struct InterfaceInner {
routes: Routes,
#[cfg(feature = "proto-igmp")]
ipv4_multicast_groups: LinearMap<Ipv4Address, (), IFACE_MAX_MULTICAST_GROUP_COUNT>,
#[cfg(feature = "proto-ipv6")]
ipv6_multicast_groups: LinearMap<Ipv6Address, (), IFACE_MAX_MULTICAST_GROUP_COUNT>,
/// When to report for (all or) the next multicast group membership via IGMP
#[cfg(feature = "proto-igmp")]
igmp_report_state: IgmpReportState,
Expand Down Expand Up @@ -228,6 +230,8 @@ impl Interface {
neighbor_cache: NeighborCache::new(),
#[cfg(feature = "proto-igmp")]
ipv4_multicast_groups: LinearMap::new(),
#[cfg(feature = "proto-ipv6")]
ipv6_multicast_groups: LinearMap::new(),
#[cfg(feature = "proto-igmp")]
igmp_report_state: IgmpReportState::Inactive,
#[cfg(feature = "medium-ieee802154")]
Expand Down Expand Up @@ -771,11 +775,13 @@ impl InterfaceInner {
|| self.ipv4_multicast_groups.get(&key).is_some()
}
#[cfg(feature = "proto-ipv6")]
IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_NODES) => true,
IpAddress::Ipv6(key) => {
key == Ipv6Address::LINK_LOCAL_ALL_NODES
|| self.has_solicited_node(key)
|| self.ipv6_multicast_groups.get(&key).is_some()
}
#[cfg(feature = "proto-rpl")]
IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_RPL_NODES) => true,
#[cfg(feature = "proto-ipv6")]
IpAddress::Ipv6(addr) => self.has_solicited_node(addr),
#[allow(unreachable_patterns)]
_ => false,
}
Expand Down
118 changes: 118 additions & 0 deletions src/iface/interface/tests/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1169,3 +1169,121 @@ fn get_source_address_empty_interface() {
Ipv6Address::LOOPBACK
);
}

#[rstest]
#[case(Medium::Ip)]
#[cfg(feature = "medium-ip")]
#[case(Medium::Ethernet)]
#[cfg(feature = "medium-ethernet")]
fn test_join_ipv6_multicast_group(#[case] medium: Medium) {
fn recv_icmpv6(
device: &mut crate::tests::TestingDevice,
timestamp: Instant,
) -> std::vec::Vec<Ipv6Packet<std::vec::Vec<u8>>> {
let caps = device.capabilities();
recv_all(device, timestamp)
.iter()
.filter_map(|frame| {
let ipv6_packet = match caps.medium {
#[cfg(feature = "medium-ethernet")]
Medium::Ethernet => {
let eth_frame = EthernetFrame::new_checked(frame).ok()?;
Ipv6Packet::new_checked(eth_frame.payload()).ok()?
}
#[cfg(feature = "medium-ip")]
Medium::Ip => Ipv6Packet::new_checked(&frame[..]).ok()?,
#[cfg(feature = "medium-ieee802154")]
Medium::Ieee802154 => todo!(),
};
let buf = ipv6_packet.into_inner().to_vec();
Some(Ipv6Packet::new_unchecked(buf))
})
.collect::<std::vec::Vec<_>>()
}

let (mut iface, _sockets, mut device) = setup(medium);

let groups = [
Ipv6Address::from_parts(&[0xff05, 0, 0, 0, 0, 0, 0, 0x0001]),
Ipv6Address::from_parts(&[0xff0e, 0, 0, 0, 0, 0, 0, 0x0017]),
];

let timestamp = Instant::from_millis(0);

for &group in &groups {
iface
.join_multicast_group(&mut device, group, timestamp)
.unwrap();
assert!(iface.has_multicast_group(group));
}
assert!(iface.has_multicast_group(Ipv6Address::LINK_LOCAL_ALL_NODES));

let reports = recv_icmpv6(&mut device, timestamp);
assert_eq!(reports.len(), 2);

let caps = device.capabilities();
let checksum_caps = &caps.checksum;
for (&group_addr, ipv6_packet) in groups.iter().zip(reports) {
let buf = ipv6_packet.into_inner();
let ipv6_packet = Ipv6Packet::new_unchecked(buf.as_slice());

let _ipv6_repr = Ipv6Repr::parse(&ipv6_packet).unwrap();
let ip_payload = ipv6_packet.payload();

// The first 2 octets of this payload hold the next-header indicator and the
// Hop-by-Hop header length (in 8-octet words, minus 1), which we parse as an
// Unknown option. The remaining 6 octets hold the Hop-by-Hop PadN and Router
// Alert options.
let hbh_header = Ipv6HopByHopHeader::new_checked(&ip_payload[..8]).unwrap();
let hbh_repr = Ipv6HopByHopRepr::parse(&hbh_header).unwrap();

let mut expected_hbh_repr = Ipv6HopByHopRepr::mldv2_router_alert(0);
expected_hbh_repr
.options
.insert(
0,
Ipv6OptionRepr::Unknown {
type_: Ipv6OptionType::Unknown(IpProtocol::Icmpv6.into()),
length: 0,
data: &[],
},
)
.unwrap();
assert_eq!(hbh_repr, expected_hbh_repr);

let icmpv6_packet =
Icmpv6Packet::new_checked(&ip_payload[hbh_repr.buffer_len()..]).unwrap();
let icmpv6_repr = Icmpv6Repr::parse(
&ipv6_packet.src_addr(),
&ipv6_packet.dst_addr(),
&icmpv6_packet,
checksum_caps,
)
.unwrap();

let record_data = match icmpv6_repr {
Icmpv6Repr::Mld(MldRepr::Report {
nr_mcast_addr_rcrds,
data,
}) => {
assert_eq!(nr_mcast_addr_rcrds, 1);
data
}
other => panic!("unexpected icmpv6_repr: {:?}", other),
};

let record = MldAddressRecord::new_checked(record_data).unwrap();
let record_repr = MldAddressRecordRepr::parse(&record).unwrap();

assert_eq!(
record_repr,
MldAddressRecordRepr {
num_srcs: 0,
mcast_addr: group_addr,
record_type: MldRecordType::ChangeToInclude,
aux_data_len: 0,
payload: &[],
}
);
}
}

0 comments on commit d5939b8

Please sign in to comment.