Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement join/leave_multicast_group() for IPv6 #914

Merged
merged 8 commits into from
May 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"]
thvdveld marked this conversation as resolved.
Show resolved Hide resolved

[[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
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
66 changes: 65 additions & 1 deletion src/iface/interface/ipv6.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,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 +473,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
}
}
})
}
thvdveld marked this conversation as resolved.
Show resolved Hide resolved
}
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