Skip to content

Commit

Permalink
Update PR smoltcp-rs#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 dc64814
Show file tree
Hide file tree
Showing 17 changed files with 559 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
90 changes: 90 additions & 0 deletions examples/multicast6.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
mod utils;

use std::os::unix::io::AsRawFd;

use smoltcp::iface::{Config, Interface, SocketSet};
use smoltcp::phy::wait as phy_wait;
use smoltcp::phy::{Device, Medium};
use smoltcp::socket::udp;
use smoltcp::time::Instant;
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv6Address};

// Note: If testing with a tap interface in linux, you may need to specify the
// interface index when addressing. E.g.,
//
// ```
// ncat -u ff02::1234%tap0 8123
// ```
//
// will send packets to the multicast group we join below on tap0.

const PORT: u16 = 8123;
const GROUP: [u16; 8] = [0xff02, 0, 0, 0, 0, 0, 0, 0x1234];
const LOCAL_ADDR: [u16; 8] = [0xfe80, 0, 0, 0, 0, 0, 0, 0x101];
const ROUTER_ADDR: [u16; 8] = [0xfe80, 0, 0, 0, 0, 0, 0, 0x100];

fn main() {
utils::setup_logging("warn");

let (mut opts, mut free) = utils::create_options();
utils::add_tuntap_options(&mut opts, &mut free);
utils::add_middleware_options(&mut opts, &mut free);

let mut matches = utils::parse_options(&opts, free);
let device = utils::parse_tuntap_options(&mut matches);
let fd = device.as_raw_fd();
let mut device =
utils::parse_middleware_options(&mut matches, device, /*loopback=*/ false);

// Create interface
let local_addr = Ipv6Address::from_parts(&LOCAL_ADDR);
let ethernet_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]);
let mut config = match device.capabilities().medium {
Medium::Ethernet => Config::new(ethernet_addr.into()),
Medium::Ip => Config::new(smoltcp::wire::HardwareAddress::Ip),
Medium::Ieee802154 => todo!(),
};
config.random_seed = rand::random();

let mut iface = Interface::new(config, &mut device, Instant::now());
iface.update_ip_addrs(|ip_addrs| {
ip_addrs
.push(IpCidr::new(IpAddress::from(local_addr), 64))
.unwrap();
});
iface
.routes_mut()
.add_default_ipv6_route(Ipv6Address::from_parts(&ROUTER_ADDR))
.unwrap();

// Create sockets
let mut sockets = SocketSet::new(vec![]);
let udp_rx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 4], vec![0; 1024]);
let udp_tx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY], vec![0; 0]);
let udp_socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
let udp_handle = sockets.add(udp_socket);

// Join a multicast group
iface
.join_multicast_group(&mut device, Ipv6Address::from_parts(&GROUP), Instant::now())
.unwrap();

loop {
let timestamp = Instant::now();
iface.poll(timestamp, &mut device, &mut sockets);

let socket = sockets.get_mut::<udp::Socket>(udp_handle);
if !socket.is_open() {
socket.bind(PORT).unwrap()
}

if socket.can_recv() {
socket
.recv()
.map(|(data, sender)| println!("traffic: {} UDP bytes from {}", data.len(), sender))
.unwrap_or_else(|e| println!("Recv UDP error: {:?}", e));
}

phy_wait(fd, iface.poll_delay(timestamp, &sockets)).expect("wait error");
}
}
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

0 comments on commit dc64814

Please sign in to comment.