Skip to content

Commit dc64814

Browse files
author
Lucas C. Villa Real
committed
Update PR #602: implement join_multicast_group() for IPv6
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.
1 parent 4c27918 commit dc64814

File tree

17 files changed

+559
-22
lines changed

17 files changed

+559
-22
lines changed

Cargo.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,10 @@ reassembly-buffer-count-8 = []
218218
reassembly-buffer-count-16 = []
219219
reassembly-buffer-count-32 = []
220220

221-
ipv6-hbh-max-options-1 = [] # Default
221+
ipv6-hbh-max-options-1 = []
222222
ipv6-hbh-max-options-2 = []
223223
ipv6-hbh-max-options-3 = []
224-
ipv6-hbh-max-options-4 = []
224+
ipv6-hbh-max-options-4 = [] # Default
225225
ipv6-hbh-max-options-8 = []
226226
ipv6-hbh-max-options-16 = []
227227
ipv6-hbh-max-options-32 = []
@@ -296,6 +296,10 @@ required-features = ["log", "medium-ethernet", "proto-ipv4", "socket-tcp"]
296296
name = "multicast"
297297
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "proto-igmp", "socket-udp"]
298298

299+
[[example]]
300+
name = "multicast6"
301+
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv6", "socket-udp"]
302+
299303
[[example]]
300304
name = "benchmark"
301305
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-raw", "socket-udp"]

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,7 @@ Maximum length of DNS names that can be queried. Default: 255.
286286

287287
### IPV6_HBH_MAX_OPTIONS
288288

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

291291
## Hosted usage examples
292292

build.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ static CONFIGS: &[(&str, usize)] = &[
1515
("ASSEMBLER_MAX_SEGMENT_COUNT", 4),
1616
("REASSEMBLY_BUFFER_SIZE", 1500),
1717
("REASSEMBLY_BUFFER_COUNT", 1),
18-
("IPV6_HBH_MAX_OPTIONS", 1),
18+
("IPV6_HBH_MAX_OPTIONS", 4),
1919
("DNS_MAX_RESULT_COUNT", 1),
2020
("DNS_MAX_SERVER_COUNT", 1),
2121
("DNS_MAX_NAME_SIZE", 255),

ci.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@ FEATURES_TEST=(
2121
"std,medium-ethernet,proto-ipv4,proto-igmp,socket-raw,socket-dns"
2222
"std,medium-ethernet,proto-ipv4,socket-udp,socket-tcp,socket-dns"
2323
"std,medium-ethernet,proto-ipv4,proto-dhcpv4,socket-udp"
24-
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,socket-udp,socket-dns"
24+
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv6,proto-igmp,socket-udp,socket-dns"
2525
"std,medium-ethernet,proto-ipv6,socket-tcp"
2626
"std,medium-ethernet,medium-ip,proto-ipv4,socket-icmp,socket-tcp"
2727
"std,medium-ip,proto-ipv6,socket-icmp,socket-tcp"
2828
"std,medium-ieee802154,proto-sixlowpan,socket-udp"
2929
"std,medium-ieee802154,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
3030
"std,medium-ieee802154,proto-rpl,proto-sixlowpan,proto-sixlowpan-fragmentation,socket-udp"
3131
"std,medium-ip,proto-ipv4,proto-ipv6,socket-tcp,socket-udp"
32-
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
32+
"std,medium-ethernet,medium-ip,medium-ieee802154,proto-ipv4,proto-ipv6,proto-igmp,socket-raw,socket-udp,socket-tcp,socket-icmp,socket-dns,async"
3333
"std,medium-ieee802154,medium-ip,proto-ipv4,socket-raw"
3434
"std,medium-ethernet,proto-ipv4,proto-ipsec,socket-raw"
3535
)

examples/multicast6.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
mod utils;
2+
3+
use std::os::unix::io::AsRawFd;
4+
5+
use smoltcp::iface::{Config, Interface, SocketSet};
6+
use smoltcp::phy::wait as phy_wait;
7+
use smoltcp::phy::{Device, Medium};
8+
use smoltcp::socket::udp;
9+
use smoltcp::time::Instant;
10+
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv6Address};
11+
12+
// Note: If testing with a tap interface in linux, you may need to specify the
13+
// interface index when addressing. E.g.,
14+
//
15+
// ```
16+
// ncat -u ff02::1234%tap0 8123
17+
// ```
18+
//
19+
// will send packets to the multicast group we join below on tap0.
20+
21+
const PORT: u16 = 8123;
22+
const GROUP: [u16; 8] = [0xff02, 0, 0, 0, 0, 0, 0, 0x1234];
23+
const LOCAL_ADDR: [u16; 8] = [0xfe80, 0, 0, 0, 0, 0, 0, 0x101];
24+
const ROUTER_ADDR: [u16; 8] = [0xfe80, 0, 0, 0, 0, 0, 0, 0x100];
25+
26+
fn main() {
27+
utils::setup_logging("warn");
28+
29+
let (mut opts, mut free) = utils::create_options();
30+
utils::add_tuntap_options(&mut opts, &mut free);
31+
utils::add_middleware_options(&mut opts, &mut free);
32+
33+
let mut matches = utils::parse_options(&opts, free);
34+
let device = utils::parse_tuntap_options(&mut matches);
35+
let fd = device.as_raw_fd();
36+
let mut device =
37+
utils::parse_middleware_options(&mut matches, device, /*loopback=*/ false);
38+
39+
// Create interface
40+
let local_addr = Ipv6Address::from_parts(&LOCAL_ADDR);
41+
let ethernet_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]);
42+
let mut config = match device.capabilities().medium {
43+
Medium::Ethernet => Config::new(ethernet_addr.into()),
44+
Medium::Ip => Config::new(smoltcp::wire::HardwareAddress::Ip),
45+
Medium::Ieee802154 => todo!(),
46+
};
47+
config.random_seed = rand::random();
48+
49+
let mut iface = Interface::new(config, &mut device, Instant::now());
50+
iface.update_ip_addrs(|ip_addrs| {
51+
ip_addrs
52+
.push(IpCidr::new(IpAddress::from(local_addr), 64))
53+
.unwrap();
54+
});
55+
iface
56+
.routes_mut()
57+
.add_default_ipv6_route(Ipv6Address::from_parts(&ROUTER_ADDR))
58+
.unwrap();
59+
60+
// Create sockets
61+
let mut sockets = SocketSet::new(vec![]);
62+
let udp_rx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY; 4], vec![0; 1024]);
63+
let udp_tx_buffer = udp::PacketBuffer::new(vec![udp::PacketMetadata::EMPTY], vec![0; 0]);
64+
let udp_socket = udp::Socket::new(udp_rx_buffer, udp_tx_buffer);
65+
let udp_handle = sockets.add(udp_socket);
66+
67+
// Join a multicast group
68+
iface
69+
.join_multicast_group(&mut device, Ipv6Address::from_parts(&GROUP), Instant::now())
70+
.unwrap();
71+
72+
loop {
73+
let timestamp = Instant::now();
74+
iface.poll(timestamp, &mut device, &mut sockets);
75+
76+
let socket = sockets.get_mut::<udp::Socket>(udp_handle);
77+
if !socket.is_open() {
78+
socket.bind(PORT).unwrap()
79+
}
80+
81+
if socket.can_recv() {
82+
socket
83+
.recv()
84+
.map(|(data, sender)| println!("traffic: {} UDP bytes from {}", data.len(), sender))
85+
.unwrap_or_else(|e| println!("Recv UDP error: {:?}", e));
86+
}
87+
88+
phy_wait(fd, iface.poll_delay(timestamp, &sockets)).expect("wait error");
89+
}
90+
}

gen_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def feature(name, default, min, max, pow2=None):
3636
feature("assembler_max_segment_count", default=4, min=1, max=32, pow2=4)
3737
feature("reassembly_buffer_size", default=1500, min=256, max=65536, pow2=True)
3838
feature("reassembly_buffer_count", default=1, min=1, max=32, pow2=4)
39-
feature("ipv6_hbh_max_options", default=1, min=1, max=32, pow2=4)
39+
feature("ipv6_hbh_max_options", default=4, min=1, max=32, pow2=4)
4040
feature("dns_max_result_count", default=1, min=1, max=32, pow2=4)
4141
feature("dns_max_server_count", default=1, min=1, max=32, pow2=4)
4242
feature("dns_max_name_size", default=255, min=64, max=255, pow2=True)

src/iface/interface/igmp.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ pub enum MulticastError {
1010
GroupTableFull,
1111
/// IPv6 multicast is not yet supported.
1212
Ipv6NotSupported,
13+
/// Cannot join/leave the given multicast group.
14+
Unaddressable,
1315
}
1416

1517
impl core::fmt::Display for MulticastError {
@@ -18,6 +20,7 @@ impl core::fmt::Display for MulticastError {
1820
MulticastError::Exhausted => write!(f, "Exhausted"),
1921
MulticastError::GroupTableFull => write!(f, "GroupTableFull"),
2022
MulticastError::Ipv6NotSupported => write!(f, "Ipv6NotSupported"),
23+
MulticastError::Unaddressable => write!(f, "Unaddressable"),
2124
}
2225
}
2326
}
@@ -68,9 +71,43 @@ impl Interface {
6871
Ok(false)
6972
}
7073
}
71-
// Multicast is not yet implemented for other address families
74+
#[cfg(feature = "proto-ipv6")]
75+
IpAddress::Ipv6(addr) => {
76+
// Build report packet containing this new address
77+
let initial_report_record: &[MldAddressRecordRepr] = &[MldAddressRecordRepr {
78+
num_srcs: 0,
79+
mcast_addr: addr,
80+
record_type: MldRecordType::ChangeToInclude,
81+
aux_data_len: 0,
82+
payload: &[],
83+
}];
84+
85+
let is_not_new = self
86+
.inner
87+
.ipv6_multicast_groups
88+
.insert(addr, ())
89+
.map_err(|_| MulticastError::GroupTableFull)?
90+
.is_some();
91+
if is_not_new {
92+
Ok(false)
93+
} else if let Some(pkt) = self.inner.mldv2_report_packet(initial_report_record) {
94+
// Send initial membership report
95+
let tx_token = device
96+
.transmit(timestamp)
97+
.ok_or(MulticastError::Exhausted)?;
98+
99+
// NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
100+
self.inner
101+
.dispatch_ip(tx_token, PacketMeta::default(), pkt, &mut self.fragmenter)
102+
.unwrap();
103+
104+
Ok(true)
105+
} else {
106+
Ok(false)
107+
}
108+
}
72109
#[allow(unreachable_patterns)]
73-
_ => Err(MulticastError::Ipv6NotSupported),
110+
_ => Err(MulticastError::Unaddressable),
74111
}
75112
}
76113

src/iface/interface/ipv6.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ impl InterfaceInner {
238238

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

@@ -472,4 +472,67 @@ impl InterfaceInner {
472472
IpPayload::Icmpv6(icmp_repr),
473473
))
474474
}
475+
476+
pub(super) fn mldv2_report_packet<'any>(
477+
&self,
478+
records: &'any [MldAddressRecordRepr<'any>],
479+
) -> Option<Packet<'any>> {
480+
// Per [RFC 3810 § 5.2.13], source addresses must be link-local, falling
481+
// back to the unspecified address if we haven't acquired one.
482+
// [RFC 3810 § 5.2.13]: https://tools.ietf.org/html/rfc3810#section-5.2.13
483+
let src_addr = self
484+
.link_local_ipv6_address()
485+
.unwrap_or(Ipv6Address::UNSPECIFIED);
486+
487+
// Per [RFC 3810 § 5.2.14], all MLDv2 reports are sent to ff02::16.
488+
// [RFC 3810 § 5.2.14]: https://tools.ietf.org/html/rfc3810#section-5.2.14
489+
let dst_addr = Ipv6Address::LINK_LOCAL_ALL_MLDV2_ROUTERS;
490+
491+
// Create a dummy IPv6 extension header so we can calculate the total length of the packet.
492+
// The actual extension header will be created later by Packet::emit_payload().
493+
let dummy_ext_hdr = Ipv6ExtHeaderRepr {
494+
next_header: IpProtocol::Unknown(0),
495+
length: 0,
496+
data: &[],
497+
};
498+
499+
let hbh_repr = Ipv6HopByHopRepr::mldv2_router_alert(0);
500+
let mld_repr = MldRepr::ReportRecordReprs(records);
501+
let records_len = records
502+
.iter()
503+
.map(MldAddressRecordRepr::buffer_len)
504+
.sum::<usize>();
505+
506+
// All MLDv2 messages must be sent with an IPv6 Hop limit of 1.
507+
Some(Packet::new_ipv6(
508+
Ipv6Repr {
509+
src_addr,
510+
dst_addr,
511+
next_header: IpProtocol::HopByHop,
512+
payload_len: dummy_ext_hdr.header_len()
513+
+ hbh_repr.buffer_len()
514+
+ mld_repr.buffer_len()
515+
+ records_len,
516+
hop_limit: 1,
517+
},
518+
IpPayload::HopByHopIcmpv6(hbh_repr, Icmpv6Repr::Mld(mld_repr)),
519+
))
520+
}
521+
522+
/// Get the first link-local IPv6 address of the interface, if present.
523+
fn link_local_ipv6_address(&self) -> Option<Ipv6Address> {
524+
self.ip_addrs.iter().find_map(|addr| match *addr {
525+
#[cfg(feature = "proto-ipv4")]
526+
IpCidr::Ipv4(_) => None,
527+
#[cfg(feature = "proto-ipv6")]
528+
IpCidr::Ipv6(cidr) => {
529+
let addr = cidr.address();
530+
if addr.is_link_local() {
531+
Some(addr)
532+
} else {
533+
None
534+
}
535+
}
536+
})
537+
}
475538
}

src/iface/interface/mod.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ pub struct InterfaceInner {
114114
routes: Routes,
115115
#[cfg(feature = "proto-igmp")]
116116
ipv4_multicast_groups: LinearMap<Ipv4Address, (), IFACE_MAX_MULTICAST_GROUP_COUNT>,
117+
#[cfg(feature = "proto-ipv6")]
118+
ipv6_multicast_groups: LinearMap<Ipv6Address, (), IFACE_MAX_MULTICAST_GROUP_COUNT>,
117119
/// When to report for (all or) the next multicast group membership via IGMP
118120
#[cfg(feature = "proto-igmp")]
119121
igmp_report_state: IgmpReportState,
@@ -228,6 +230,8 @@ impl Interface {
228230
neighbor_cache: NeighborCache::new(),
229231
#[cfg(feature = "proto-igmp")]
230232
ipv4_multicast_groups: LinearMap::new(),
233+
#[cfg(feature = "proto-ipv6")]
234+
ipv6_multicast_groups: LinearMap::new(),
231235
#[cfg(feature = "proto-igmp")]
232236
igmp_report_state: IgmpReportState::Inactive,
233237
#[cfg(feature = "medium-ieee802154")]
@@ -771,11 +775,13 @@ impl InterfaceInner {
771775
|| self.ipv4_multicast_groups.get(&key).is_some()
772776
}
773777
#[cfg(feature = "proto-ipv6")]
774-
IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_NODES) => true,
778+
IpAddress::Ipv6(key) => {
779+
key == Ipv6Address::LINK_LOCAL_ALL_NODES
780+
|| self.has_solicited_node(key)
781+
|| self.ipv6_multicast_groups.get(&key).is_some()
782+
}
775783
#[cfg(feature = "proto-rpl")]
776784
IpAddress::Ipv6(Ipv6Address::LINK_LOCAL_ALL_RPL_NODES) => true,
777-
#[cfg(feature = "proto-ipv6")]
778-
IpAddress::Ipv6(addr) => self.has_solicited_node(addr),
779785
#[allow(unreachable_patterns)]
780786
_ => false,
781787
}

0 commit comments

Comments
 (0)