Skip to content

Commit

Permalink
Merge pull request #914 from lucasvr/lucas/multicastv6
Browse files Browse the repository at this point in the history
Implement `join/leave_multicast_group()` for IPv6
  • Loading branch information
thvdveld committed May 14, 2024
2 parents 125773e + 20aa66b commit 35bb01a
Show file tree
Hide file tree
Showing 18 changed files with 601 additions and 27 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,proto-rpl,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,proto-rpl,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
68 changes: 61 additions & 7 deletions src/iface/interface/igmp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ pub enum MulticastError {
Exhausted,
/// The table of joined multicast groups is already full.
GroupTableFull,
/// IPv6 multicast is not yet supported.
Ipv6NotSupported,
/// Cannot join/leave the given multicast group.
Unaddressable,
}

impl core::fmt::Display for MulticastError {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
match self {
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 +68,39 @@ 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 report_record = &[MldAddressRecordRepr::new(
MldRecordType::ChangeToInclude,
addr,
)];
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(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 Expand Up @@ -110,9 +140,33 @@ impl Interface {
Ok(false)
}
}
// Multicast is not yet implemented for other address families
#[cfg(feature = "proto-ipv6")]
IpAddress::Ipv6(addr) => {
let report_record = &[MldAddressRecordRepr::new(
MldRecordType::ChangeToExclude,
addr,
)];
let was_not_present = self.inner.ipv6_multicast_groups.remove(&addr).is_none();
if was_not_present {
Ok(false)
} else if let Some(pkt) = self.inner.mldv2_report_packet(report_record) {
// Send group leave packet
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
68 changes: 67 additions & 1 deletion src/iface/interface/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,23 @@ impl InterfaceInner {
})
}

/// 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
}
}
})
}

pub(super) fn process_ipv6<'frame>(
&mut self,
sockets: &mut SocketSet,
Expand Down Expand Up @@ -238,7 +255,8 @@ 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 +490,52 @@ 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 mut hbh_repr = Ipv6HopByHopRepr::mldv2_router_alert();
hbh_repr.push_padn_option(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)),
))
}
}
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 35bb01a

Please sign in to comment.