Skip to content

slaac: add initial stateless address autoconfiguration (SLAAC) implementation #1039

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,15 @@ iface-max-route-count-256 = []
iface-max-route-count-512 = []
iface-max-route-count-1024 = []

iface-max-prefix-count-1 = [] # Default
iface-max-prefix-count-2 = []
iface-max-prefix-count-3 = []
iface-max-prefix-count-4 = []
iface-max-prefix-count-5 = []
iface-max-prefix-count-6 = []
iface-max-prefix-count-7 = []
iface-max-prefix-count-8 = []

fragmentation-buffer-size-256 = []
fragmentation-buffer-size-512 = []
fragmentation-buffer-size-1024 = []
Expand Down Expand Up @@ -328,5 +337,9 @@ required-features = ["std", "medium-ieee802154", "phy-raw_socket", "proto-sixlow
name = "dns"
required-features = ["std", "medium-ethernet", "medium-ip", "phy-tuntap_interface", "proto-ipv4", "socket-dns"]

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

[profile.release]
debug = 2
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ The ICMPv6 protocol is supported, and ICMP sockets are available.
#### NDISC

* Neighbor Advertisement messages are generated in response to Neighbor Solicitations.
* Router Advertisement messages are **not** generated or read.
* Router Solicitation messages are **not** generated or read.
* Router Advertisement messages are read, but **not** generated.
* Router Solicitation messages are generated, but **not** read.
* Redirected Header messages are **not** generated or read.

### UDP layer
Expand Down Expand Up @@ -256,6 +256,11 @@ Amount of "IP address -> hardware address" entries the neighbor cache (also know

Max amount of routes that can be added to one interface. Includes the default route. Includes both IPv4 and IPv6. Default: 2.

### `IFACE_MAX_PREFIX_COUNT`

Max amount of IPv6 prefixes that can be added to one interface via SLAAC.
Should be lower or equal to `IFACE_MAX_ADDR_COUNT`.

### `FRAGMENTATION_BUFFER_SIZE`

Size of the buffer used for fragmenting outgoing packets larger than the MTU. Packets larger than this setting will be dropped instead of fragmented. Default: 1500.
Expand Down
1 change: 1 addition & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ static CONFIGS: &[(&str, usize)] = &[
("IFACE_MAX_SIXLOWPAN_ADDRESS_CONTEXT_COUNT", 4),
("IFACE_NEIGHBOR_CACHE_COUNT", 8),
("IFACE_MAX_ROUTE_COUNT", 2),
("IFACE_MAX_PREFIX_COUNT", 1),
("FRAGMENTATION_BUFFER_SIZE", 1500),
("ASSEMBLER_MAX_SEGMENT_COUNT", 4),
("REASSEMBLY_BUFFER_SIZE", 1500),
Expand Down
76 changes: 76 additions & 0 deletions examples/slaac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
mod utils;

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

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

const LOCAL_ADDR: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 0, 0x0, 0, 0, 0x01);

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 mut config = match device.capabilities().medium {
Medium::Ethernet => {
Config::new(EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]).into())
}
Medium::Ip => Config::new(smoltcp::wire::HardwareAddress::Ip),
Medium::Ieee802154 => todo!(),
};
config.slaac = true;

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();
});

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);

let mut last_print = Instant::now();
loop {
let timestamp = Instant::now();
iface.poll(timestamp, &mut device, &mut sockets);
let mut delay = iface.poll_delay(timestamp, &sockets);
if delay.is_none() || delay.is_some_and(|d| d > Duration::from_millis(1000)) {
delay = Some(Duration::from_millis(1000));
}

phy_wait(fd, delay).expect("wait error");

let timestamp = Instant::now();
if timestamp > last_print + Duration::from_secs(1) {
last_print = timestamp;
println!();
println!("Addresses:");
for addr in iface.ip_addrs() {
println!(" - {addr}");
}
println!("Routes:");
iface.routes_mut().update(|routes| {
for route in routes {
println!(" - {} via {}", route.cidr, route.via_router);
}
});
}
}
}
1 change: 1 addition & 0 deletions gen_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def feature(name, default, min, max, pow2=None):
feature("iface_max_sixlowpan_address_context_count", default=4, min=1, max=1024, pow2=8)
feature("iface_neighbor_cache_count", default=8, min=1, max=1024, pow2=8)
feature("iface_max_route_count", default=2, min=1, max=1024, pow2=8)
feature("iface_max_prefix_count", default=1, min=1, max=8)
feature("fragmentation_buffer_size", default=1500, min=256, max=65536, pow2=True)
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)
Expand Down
155 changes: 155 additions & 0 deletions src/iface/interface/ipv6.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::*;

use crate::iface::Route;

/// Enum used for the process_hopbyhop function. In some cases, when discarding a packet, an ICMP
/// parameter problem message needs to be transmitted to the source of the address. In other cases,
/// the processing of the IP packet can continue.
Expand Down Expand Up @@ -502,6 +504,30 @@ impl InterfaceInner {
None
}
}
NdiscRepr::RouterAdvert {
hop_limit: _,
flags: _,
router_lifetime,
reachable_time: _,
retrans_time: _,
lladdr: _,
mtu: _,
prefix_info,
} if self.slaac_enabled => {
if ip_repr.src_addr.is_link_local()
&& (ip_repr.dst_addr == IPV6_LINK_LOCAL_ALL_NODES
|| ip_repr.dst_addr.is_link_local())
&& ip_repr.hop_limit == 255
{
self.slaac.process_advertisement(
&ip_repr.src_addr,
router_lifetime,
prefix_info,
self.now,
)
}
None
}
_ => None,
}
}
Expand Down Expand Up @@ -581,3 +607,132 @@ impl InterfaceInner {
))
}
}

impl Interface {
/// Synchronize the slaac address and router state with the interface state.
#[cfg(all(
feature = "proto-ipv6",
any(feature = "medium-ethernet", feature = "medium-ieee802154")
))]
pub(super) fn sync_slaac_state(&mut self, timestamp: Instant) {
let required_addresses: Vec<_, IFACE_MAX_PREFIX_COUNT> = self
.inner
.slaac
.prefix()
.iter()
.filter_map(|(prefix, prefixinfo)| {
if prefixinfo.is_valid(timestamp) {
Ipv6Cidr::from_link_prefix(prefix, self.inner.hardware_addr())
} else {
None
}
})
.collect();
let removed_addresses: Vec<_, IFACE_MAX_PREFIX_COUNT> = self
.inner
.slaac
.prefix()
.iter()
.filter_map(|(prefix, prefixinfo)| {
if !prefixinfo.is_valid(timestamp) {
Ipv6Cidr::from_link_prefix(prefix, self.inner.hardware_addr())
} else {
None
}
})
.collect();

self.update_ip_addrs(|addresses| {
for address in required_addresses {
if !addresses.contains(&IpCidr::Ipv6(address)) {
let _ = addresses.push(IpCidr::Ipv6(address));
}
}
addresses.retain(|address| {
if let IpCidr::Ipv6(address) = address {
!removed_addresses.contains(address)
} else {
true
}
});
});

{
let required_routes = self
.inner
.slaac
.routes()
.into_iter()
.filter(|required| required.is_valid(timestamp));

let removed_routes = self
.inner
.slaac
.routes()
.into_iter()
.filter(|r| !r.is_valid(timestamp));

self.inner.routes.update(|routes| {
routes.retain(|r| match (&r.cidr, &r.via_router) {
(IpCidr::Ipv6(cidr), IpAddress::Ipv6(via_router)) => !removed_routes
.clone()
.any(|f| f.same_route(cidr, via_router)),
_ => true,
});

for route in required_routes {
if routes.iter().all(|r| match (&r.cidr, &r.via_router) {
(IpCidr::Ipv6(cidr), IpAddress::Ipv6(via_router)) => {
!route.same_route(cidr, via_router)
}
_ => false,
}) {
let _ = routes.push(Route {
cidr: route.cidr.into(),
via_router: route.via_router.into(),
preferred_until: None,
expires_at: None,
});
}
}
});
}

self.inner.slaac.update_slaac_state(timestamp);
}

/// Emit a router solicitation when required by the interface's slaac state machine.
#[cfg(all(
feature = "proto-ipv6",
any(feature = "medium-ethernet", feature = "medium-ieee802154")
))]
pub(super) fn ndisc_rs_egress(&mut self, device: &mut (impl Device + ?Sized)) {
if !self.inner.slaac.rs_required(self.inner.now) {
return;
}
let rs_repr = Icmpv6Repr::Ndisc(NdiscRepr::RouterSolicit {
lladdr: Some(self.hardware_addr().into()),
});
let ipv6_repr = Ipv6Repr {
src_addr: self.inner.link_local_ipv6_address().unwrap(),
dst_addr: IPV6_LINK_LOCAL_ALL_ROUTERS,
next_header: IpProtocol::Icmpv6,
payload_len: rs_repr.buffer_len(),
hop_limit: 255,
};
let packet = Packet::new_ipv6(ipv6_repr, IpPayload::Icmpv6(rs_repr));
let Some(tx_token) = device.transmit(self.inner.now) else {
return;
};
// NOTE(unwrap): packet destination is multicast, which is always routable and doesn't require neighbor discovery.
self.inner
.dispatch_ip(
tx_token,
PacketMeta::default(),
packet,
&mut self.fragmenter,
)
.unwrap();
self.inner.slaac.rs_sent(self.inner.now);
}
}
Loading
Loading