blob: a55dbf6125bd97ff95a882959a46c0acf2e696a8 [file] [log] [blame]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! The Neighbor Discovery Protocol (NDP).
//!
//! Neighbor Discovery for IPv6 as defined in [RFC 4861] defines mechanisms for
//! solving the following problems:
//! - Router Discovery
//! - Prefix Discovery
//! - Parameter Discovery
//! - Address Autoconfiguration
//! - Address resolution
//! - Next-hop determination
//! - Neighbor Unreachability Detection
//! - Duplicate Address Detection
//! - Redirect
//!
//! [RFC 4861]: https://tools.ietf.org/html/rfc4861
/// Test utilities for NDP.
#[cfg(any(test, feature = "testutils"))]
pub(crate) mod testutil {
use crate::ip::icmp::REQUIRED_NDP_IP_PACKET_HOP_LIMIT;
use alloc::vec::Vec;
use net_types::{
ethernet::Mac,
ip::{Ipv6, Ipv6Addr},
};
use packet::{Buf, InnerPacketBuilder as _, Serializer as _};
use packet_formats::{
icmp::{
ndp::{
options::NdpOptionBuilder, NeighborAdvertisement, NeighborSolicitation,
OptionSequenceBuilder,
},
IcmpPacketBuilder, IcmpUnusedCode,
},
ip::Ipv6Proto,
ipv6::Ipv6PacketBuilder,
};
/// Serialize an IP packet containing a neighbor advertisement with the
/// provided parameters.
pub fn neighbor_advertisement_ip_packet(
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
router_flag: bool,
solicited_flag: bool,
override_flag: bool,
mac: Mac,
) -> Buf<Vec<u8>> {
OptionSequenceBuilder::new([NdpOptionBuilder::TargetLinkLayerAddress(&mac.bytes())].iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
NeighborAdvertisement::new(router_flag, solicited_flag, override_flag, src_ip),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
/// Serialize an IP packet containing a neighbor solicitation with the
/// provided parameters.
pub fn neighbor_solicitation_ip_packet(
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
target_addr: Ipv6Addr,
mac: Mac,
) -> Buf<Vec<u8>> {
OptionSequenceBuilder::new([NdpOptionBuilder::SourceLinkLayerAddress(&mac.bytes())].iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
NeighborSolicitation::new(target_addr),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::{collections::HashSet, vec, vec::Vec};
use core::{
convert::TryInto as _,
fmt::Debug,
num::{NonZeroU16, NonZeroU8},
time::Duration,
};
use assert_matches::assert_matches;
use net_declare::net::{mac, subnet_v6};
use net_types::{
ethernet::Mac,
ip::{AddrSubnet, Ip as _, Ipv6, Ipv6Addr, Ipv6Scope, Mtu, Subnet},
NonMappedAddr, ScopeableAddress as _, UnicastAddr, Witness as _,
};
use packet::{Buf, EmptyBuf, InnerPacketBuilder as _, Serializer as _};
use packet_formats::{
ethernet::EthernetFrameLengthCheck,
icmp::{
ndp::{
options::{NdpOption, NdpOptionBuilder, PrefixInformation},
OptionSequenceBuilder, Options, RouterAdvertisement, RouterSolicitation,
},
IcmpEchoRequest, IcmpPacketBuilder, IcmpUnusedCode,
},
ip::{IpProto, Ipv6Proto},
ipv6::Ipv6PacketBuilder,
testutil::{parse_ethernet_frame, parse_icmp_packet_in_ip_packet_in_ethernet_frame},
utils::NonZeroDuration,
};
use rand::RngCore;
use tracing::trace;
use zerocopy::ByteSlice;
use crate::{
algorithm::{
generate_opaque_interface_identifier, OpaqueIidNonce, STABLE_IID_SECRET_KEY_BYTES,
},
context::{
testutil::{FakeInstant, FakeNetwork, FakeNetworkLinks, StepResult},
InstantContext as _, RngContext as _, TimerContext,
},
device::{
ethernet::{EthernetCreationProperties, EthernetLinkDevice, MaxEthernetFrameSize},
link::LinkAddress,
testutil::{is_forwarding_enabled, set_forwarding_enabled},
DeviceId, EthernetDeviceId, FrameDestination,
},
ip::{
device::{
config::{
IpDeviceConfigurationUpdate, Ipv4DeviceConfigurationUpdate,
Ipv6DeviceConfigurationUpdate,
},
get_ipv6_hop_limit,
router_solicitation::{MAX_RTR_SOLICITATION_DELAY, RTR_SOLICITATION_INTERVAL},
slaac::{SlaacConfiguration, SlaacTimerId, TemporarySlaacAddressConfiguration},
state::{
Ipv6AddrConfig, Ipv6AddressFlags, Ipv6AddressState, Lifetime, SlaacConfig,
TemporarySlaacConfig,
},
testutil::with_assigned_ipv6_addr_subnets,
IpAddressId as _, Ipv6DeviceAddr, Ipv6DeviceHandler, Ipv6DeviceTimerId,
},
icmp::REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
SendIpPacketMeta,
},
testutil::{
assert_empty, set_logger_for_test, Ctx, DispatchedFrame, FakeBindingsCtx,
FakeEventDispatcherBuilder, TestIpExt, DEFAULT_INTERFACE_METRIC, FAKE_CONFIG_V6,
IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
time::TimerIdInner,
Instant, TimerId,
};
#[derive(Debug, PartialEq, Copy, Clone)]
struct GlobalIpv6Addr<I> {
addr_sub: AddrSubnet<Ipv6Addr, Ipv6DeviceAddr>,
flags: Ipv6AddressFlags,
config: Ipv6AddrConfig<I>,
}
impl<I> GlobalIpv6Addr<I> {
fn addr_sub(&self) -> &AddrSubnet<Ipv6Addr, Ipv6DeviceAddr> {
&self.addr_sub
}
}
fn get_global_ipv6_addrs(
ctx: &crate::testutil::FakeCtx,
device_id: &DeviceId<crate::testutil::FakeBindingsCtx>,
) -> Vec<GlobalIpv6Addr<crate::context::testutil::FakeInstant>> {
crate::ip::device::IpDeviceStateContext::<Ipv6, _>::with_address_ids(
&mut ctx.core_ctx(),
device_id,
|addrs, core_ctx| {
addrs
.filter_map(|addr_id| {
let addr_sub = addr_id.addr_sub();
crate::ip::device::IpDeviceAddressContext::<Ipv6, _>::with_ip_address_state(
core_ctx,
device_id,
&addr_id,
|Ipv6AddressState { flags, config }| match addr_sub.addr().scope() {
Ipv6Scope::Global => Some(GlobalIpv6Addr {
addr_sub,
flags: *flags,
config: *config,
}),
Ipv6Scope::InterfaceLocal
| Ipv6Scope::LinkLocal
| Ipv6Scope::AdminLocal
| Ipv6Scope::SiteLocal
| Ipv6Scope::OrganizationLocal
| Ipv6Scope::Reserved(_)
| Ipv6Scope::Unassigned(_) => None,
},
)
})
.collect()
},
)
}
// TODO(https://github.com/rust-lang/rust/issues/67441): Make these constants once const
// Option::unwrap is stablized
fn local_mac() -> UnicastAddr<Mac> {
UnicastAddr::new(Mac::new([0, 1, 2, 3, 4, 5])).unwrap()
}
fn remote_mac() -> UnicastAddr<Mac> {
UnicastAddr::new(Mac::new([6, 7, 8, 9, 10, 11])).unwrap()
}
fn local_ip() -> Ipv6DeviceAddr {
FAKE_CONFIG_V6.local_ipv6_device_addr()
}
fn remote_ip() -> Ipv6DeviceAddr {
FAKE_CONFIG_V6.remote_ipv6_device_addr()
}
impl TryFrom<DeviceId<crate::testutil::FakeBindingsCtx>> for EthernetDeviceId<FakeBindingsCtx> {
type Error = DeviceId<crate::testutil::FakeBindingsCtx>;
fn try_from(
id: DeviceId<crate::testutil::FakeBindingsCtx>,
) -> Result<EthernetDeviceId<FakeBindingsCtx>, DeviceId<crate::testutil::FakeBindingsCtx>>
{
match id {
DeviceId::Ethernet(id) => Ok(id),
DeviceId::Loopback(_) | DeviceId::PureIp(_) => Err(id),
}
}
}
fn setup_net() -> (
FakeNetwork<
&'static str,
crate::testutil::FakeCtx,
impl FakeNetworkLinks<DispatchedFrame, EthernetDeviceId<FakeBindingsCtx>, &'static str>,
>,
EthernetDeviceId<FakeBindingsCtx>,
EthernetDeviceId<FakeBindingsCtx>,
) {
let mut local = FakeEventDispatcherBuilder::default();
let local_dev_idx = local.add_device_with_config(
local_mac(),
Ipv4DeviceConfigurationUpdate::default(),
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
..Default::default()
},
);
let mut remote = FakeEventDispatcherBuilder::default();
let remote_dev_idx = remote.add_device_with_config(
remote_mac(),
Ipv4DeviceConfigurationUpdate::default(),
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
..Default::default()
},
);
let (local, local_device_ids) = local.build();
let (remote, remote_device_ids) = remote.build();
let local_eth_device_id = local_device_ids[local_dev_idx].clone();
let remote_eth_device_id = remote_device_ids[remote_dev_idx].clone();
let net = crate::context::testutil::new_simple_fake_network(
"local",
local,
local_eth_device_id.downgrade(),
"remote",
remote,
remote_eth_device_id.downgrade(),
);
(net, local_eth_device_id, remote_eth_device_id)
}
#[test]
fn test_address_resolution() {
set_logger_for_test();
let (net, local_eth_device_id, remote_eth_device_id) = setup_net();
// Remove the devices so that existing NUD timers get cleaned up;
// otherwise, they would hold dangling references to the device when the
// core context is dropped at the end of the test.
let local_device_id_clone = local_eth_device_id.clone();
let remote_device_id_clone = remote_eth_device_id.clone();
let mut net = scopeguard::guard(net, move |mut net| {
net.with_context("local", |ctx| {
ctx.core_api().device().remove_device(local_device_id_clone).into_removed();
});
net.with_context("remote", |ctx| {
ctx.core_api().device().remove_device(remote_device_id_clone).into_removed();
});
});
let local_device_id = local_eth_device_id.into();
let remote_device_id = remote_eth_device_id.into();
// Let's try to ping the remote device from the local device:
let req = IcmpEchoRequest::new(0, 0);
let req_body = &[1, 2, 3, 4];
let body = Buf::new(req_body.to_vec(), ..).encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
local_ip(),
remote_ip(),
IcmpUnusedCode,
req,
));
// Manually assigning the addresses.
net.with_context("remote", |ctx| {
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(
&remote_device_id,
AddrSubnet::new(remote_ip().into(), 128).unwrap(),
)
.unwrap();
assert_matches!(ctx.bindings_ctx.copy_ethernet_frames()[..], []);
});
net.with_context("local", |mut ctx| {
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(
&local_device_id,
AddrSubnet::new(local_ip().into(), 128).unwrap(),
)
.unwrap();
let Ctx { core_ctx, bindings_ctx } = &mut ctx;
assert_matches!(bindings_ctx.copy_ethernet_frames()[..], []);
crate::ip::send_ip_packet_from_device::<Ipv6, _, _, _>(
&mut core_ctx.context(),
bindings_ctx,
SendIpPacketMeta {
device: &local_device_id,
src_ip: Some(local_ip().into_specified()),
dst_ip: remote_ip().into_specified(),
broadcast: None,
next_hop: remote_ip().into_specified(),
proto: Ipv6Proto::Icmpv6,
ttl: None,
mtu: None,
},
body,
)
.unwrap();
// This should've triggered a neighbor solicitation to come out of
// local.
assert_matches!(&bindings_ctx.copy_ethernet_frames()[..], [_frame]);
// A timer should've been started.
assert_eq!(bindings_ctx.timer_ctx().timers().len(), 1);
});
let _: StepResult = net.step();
assert_eq!(
net.core_ctx("remote").ndp_counters().rx.neighbor_solicitation.get(),
1,
"remote received solicitation"
);
assert_matches!(&net.bindings_ctx("remote").copy_ethernet_frames()[..], [_frame]);
// Forward advertisement response back to local.
let _: StepResult = net.step();
assert_eq!(
net.core_ctx("local").ndp_counters().rx.neighbor_advertisement.get(),
1,
"local received advertisement"
);
// Upon link layer resolution, the original ping request should've been
// sent out.
net.with_context("local", |Ctx { core_ctx: _, bindings_ctx }| {
assert_matches!(&bindings_ctx.copy_ethernet_frames()[..], [_frame]);
});
let _: StepResult = net.step();
assert_eq!(
net.core_ctx("remote").inner_icmp_state::<Ipv6>().rx_counters.echo_request.get(),
1
);
// TODO(brunodalbo): We should be able to verify that remote also sends
// back an echo reply, but we're having some trouble with IPv6 link
// local addresses.
}
#[test]
fn test_dad_duplicate_address_detected_solicitation() {
// Tests whether a duplicate address will get detected by solicitation
// In this test, two nodes having the same MAC address will come up at
// the same time. And both of them will use the EUI address. Each of
// them should be able to detect each other is using the same address,
// so they will both give up using that address.
set_logger_for_test();
let mac = UnicastAddr::new(Mac::new([6, 5, 4, 3, 2, 1])).unwrap();
let ll_addr: Ipv6Addr = mac.to_ipv6_link_local().addr().get();
let multicast_addr = ll_addr.to_solicited_node_address();
// Create the devices (will start DAD at the same time).
let make_ctx_and_dev = || {
let mut ctx = crate::testutil::FakeCtx::default();
let device_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
(ctx, device_id)
};
let (local, local_device_id) = make_ctx_and_dev();
let (remote, remote_device_id) = make_ctx_and_dev();
let mut net = crate::context::testutil::new_simple_fake_network(
"local",
local,
local_device_id.downgrade(),
"remote",
remote,
remote_device_id.downgrade(),
);
let local_device_id = local_device_id.into();
let remote_device_id = remote_device_id.into();
// Create the devices (will start DAD at the same time).
let update = Ipv6DeviceConfigurationUpdate {
// Doesn't matter as long as we perform DAD.
dad_transmits: Some(NonZeroU16::new(1)),
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
ip_config: IpDeviceConfigurationUpdate { ip_enabled: Some(true), ..Default::default() },
..Default::default()
};
net.with_context("local", |ctx| {
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(&local_device_id, update)
.unwrap();
assert_matches!(&ctx.bindings_ctx.copy_ethernet_frames()[..], [_frame]);
});
net.with_context("remote", |ctx| {
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(&remote_device_id, update)
.unwrap();
assert_matches!(&ctx.bindings_ctx.copy_ethernet_frames()[..], [_frame]);
});
// Both devices should be in the solicited-node multicast group.
assert!(net
.context("local")
.test_api()
.is_in_ip_multicast(&local_device_id, multicast_addr));
assert!(net
.context("remote")
.test_api()
.is_in_ip_multicast(&remote_device_id, multicast_addr));
let _: StepResult = net.step();
// They should now realize the address they intend to use has a
// duplicate in the local network.
with_assigned_ipv6_addr_subnets(
&mut net.context("local").core_ctx(),
&local_device_id,
|addrs| assert_empty(addrs),
);
with_assigned_ipv6_addr_subnets(
&mut net.context("remote").core_ctx(),
&remote_device_id,
|addrs| assert_empty(addrs),
);
// Both devices should not be in the multicast group
assert!(!net
.context("local")
.test_api()
.is_in_ip_multicast(&local_device_id, multicast_addr));
assert!(!net
.context("remote")
.test_api()
.is_in_ip_multicast(&remote_device_id, multicast_addr));
}
fn dad_timer_id(
id: EthernetDeviceId<FakeBindingsCtx>,
addr: Ipv6DeviceAddr,
) -> TimerId<crate::testutil::FakeBindingsCtx> {
TimerId(TimerIdInner::Ipv6Device(
Ipv6DeviceTimerId::Dad(crate::ip::device::dad::DadTimerId {
device_id: id.into(),
addr: addr.get(),
})
.into(),
))
}
fn rs_timer_id(
id: EthernetDeviceId<FakeBindingsCtx>,
) -> TimerId<crate::testutil::FakeBindingsCtx> {
TimerId(TimerIdInner::Ipv6Device(
Ipv6DeviceTimerId::Rs(crate::ip::device::router_solicitation::RsTimerId {
device_id: id.into(),
})
.into(),
))
}
#[test]
fn test_dad_duplicate_address_detected_advertisement() {
// Tests whether a duplicate address will get detected by advertisement
// In this test, one of the node first assigned itself the local_ip(),
// then the second node comes up and it should be able to find out that
// it cannot use the address because someone else has already taken that
// address.
set_logger_for_test();
let (mut net, local_eth_device_id, remote_eth_device_id) = setup_net();
let local_device_id = local_eth_device_id.clone().into();
let remote_device_id = remote_eth_device_id.into();
// Enable DAD.
let update = Ipv6DeviceConfigurationUpdate {
// Doesn't matter as long as we perform DAD.
dad_transmits: Some(NonZeroU16::new(1)),
ip_config: IpDeviceConfigurationUpdate { ip_enabled: Some(true), ..Default::default() },
..Default::default()
};
let addr = AddrSubnet::<Ipv6Addr, _>::new(local_ip().into(), 128).unwrap();
let multicast_addr = local_ip().to_solicited_node_address();
net.with_context("local", |ctx| {
let mut api = ctx.core_api().device_ip::<Ipv6>();
let _: Ipv6DeviceConfigurationUpdate =
api.update_configuration(&local_device_id, update).unwrap();
api.add_ip_addr_subnet(&local_device_id, addr).unwrap();
});
net.with_context("remote", |ctx| {
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(&remote_device_id, update)
.unwrap();
});
// Only local should be in the solicited node multicast group.
assert!(net
.context("local")
.test_api()
.is_in_ip_multicast(&local_device_id, multicast_addr));
assert!(!net
.context("remote")
.test_api()
.is_in_ip_multicast(&remote_device_id, multicast_addr));
net.with_context("local", |ctx| {
assert_eq!(
ctx.trigger_next_timer().unwrap(),
dad_timer_id(local_eth_device_id, local_ip())
);
});
assert_eq!(
get_address_assigned(net.context("local"), &local_device_id, local_ip()),
Some(true)
);
net.with_context("remote", |ctx| {
ctx.core_api().device_ip::<Ipv6>().add_ip_addr_subnet(&remote_device_id, addr).unwrap();
});
// Local & remote should be in the multicast group.
assert!(net
.context("local")
.test_api()
.is_in_ip_multicast(&local_device_id, multicast_addr));
assert!(net
.context("remote")
.test_api()
.is_in_ip_multicast(&remote_device_id, multicast_addr));
let _: StepResult = net.step();
assert_eq!(
with_assigned_ipv6_addr_subnets(
&mut net.context("remote").core_ctx(),
&remote_device_id,
|addrs| addrs.count()
),
1
);
// Let's make sure that only our local node still can use that address.
assert_eq!(
get_address_assigned(net.context("local"), &local_device_id, local_ip()),
Some(true)
);
assert_eq!(
get_address_assigned(net.context("remote"), &remote_device_id, local_ip()),
None,
);
// Only local should be in the solicited node multicast group.
assert!(net
.context("local")
.test_api()
.is_in_ip_multicast(&local_device_id, multicast_addr));
assert!(!net
.context("remote")
.test_api()
.is_in_ip_multicast(&remote_device_id, multicast_addr));
}
#[test]
fn test_dad_set_ipv6_address_when_ongoing() {
// Test that we can make our tentative address change when DAD is
// ongoing.
let mut ctx = crate::testutil::FakeCtx::default();
let dev_id = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac(),
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&dev_id,
Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(NonZeroU16::new(1)),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let addr = local_ip();
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&dev_id, AddrSubnet::new(addr.into(), 128).unwrap())
.unwrap();
assert_eq!(get_address_assigned(&ctx, &dev_id, addr,), Some(false));
let addr = remote_ip();
assert_eq!(get_address_assigned(&ctx, &dev_id, addr,), None,);
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&dev_id, AddrSubnet::new(addr.into(), 128).unwrap())
.unwrap();
assert_eq!(get_address_assigned(&ctx, &dev_id, addr,), Some(false));
// Clear all device references.
ctx.core_api().device().remove_device(dev_id.unwrap_ethernet()).into_removed();
}
#[test]
fn test_dad_three_transmits_no_conflicts() {
let mut ctx = crate::testutil::FakeCtx::default();
let eth_dev_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac(),
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let dev_id = eth_dev_id.clone().into();
crate::device::testutil::enable_device(&mut ctx, &dev_id);
// Enable DAD.
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&dev_id,
Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(NonZeroU16::new(3)),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&dev_id, AddrSubnet::new(local_ip().into(), 128).unwrap())
.unwrap();
for _ in 0..3 {
assert_eq!(
ctx.trigger_next_timer().unwrap(),
dad_timer_id(eth_dev_id.clone(), local_ip())
);
}
assert_eq!(get_address_assigned(&ctx, &dev_id, local_ip(),), Some(true));
}
#[test]
fn test_dad_three_transmits_with_conflicts() {
// Test if the implementation is correct when we have more than 1 NS
// packets to send.
set_logger_for_test();
let (mut net, local_eth_device_id, remote_eth_device_id) = setup_net();
let local_device_id = local_eth_device_id.clone().into();
let remote_device_id = remote_eth_device_id.into();
let update = Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(NonZeroU16::new(3)),
ip_config: IpDeviceConfigurationUpdate { ip_enabled: Some(true), ..Default::default() },
..Default::default()
};
net.with_context("local", |ctx| {
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(&local_device_id, update)
.unwrap();
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(
&local_device_id,
AddrSubnet::new(local_ip().into(), 128).unwrap(),
)
.unwrap();
});
net.with_context("remote", |ctx| {
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(&remote_device_id, update)
.unwrap();
});
let expected_timer_id = dad_timer_id(local_eth_device_id, local_ip());
// During the first and second period, the remote host is still down.
net.with_context("local", |ctx| {
assert_eq!(ctx.trigger_next_timer().unwrap(), expected_timer_id);
assert_eq!(ctx.trigger_next_timer().unwrap(), expected_timer_id);
});
net.with_context("remote", |ctx| {
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(
&remote_device_id,
AddrSubnet::new(local_ip().into(), 128).unwrap(),
)
.unwrap();
});
// The local host should have sent out 3 packets while the remote one
// should only have sent out 1.
assert_matches!(
&net.bindings_ctx("local").copy_ethernet_frames()[..],
[_frame1, _frame2, _frame3]
);
assert_matches!(&net.bindings_ctx("remote").copy_ethernet_frames()[..], [_frame1]);
let _: StepResult = net.step();
// Let's make sure that all timers are cancelled properly.
net.with_context("local", |Ctx { core_ctx: _, bindings_ctx }| {
assert_empty(bindings_ctx.timer_ctx().timers());
});
net.with_context("remote", |Ctx { core_ctx: _, bindings_ctx }| {
assert_empty(bindings_ctx.timer_ctx().timers());
});
// They should now realize the address they intend to use has a
// duplicate in the local network.
assert_eq!(
with_assigned_ipv6_addr_subnets(
&mut net.context("local").core_ctx(),
&local_device_id,
|a| a.count()
),
1
);
assert_eq!(
with_assigned_ipv6_addr_subnets(
&mut net.context("remote").core_ctx(),
&remote_device_id,
|a| a.count()
),
1
);
}
fn get_address_assigned(
ctx: &crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
addr: Ipv6DeviceAddr,
) -> Option<bool> {
crate::ip::device::IpDeviceStateContext::<Ipv6, _>::with_address_ids(
&mut ctx.core_ctx(),
device,
|mut addrs, core_ctx| {
addrs.find_map(|addr_id| {
crate::ip::device::IpDeviceAddressContext::<Ipv6, _>::with_ip_address_state(
core_ctx,
device,
&addr_id,
|Ipv6AddressState {
flags: Ipv6AddressFlags { deprecated: _, assigned },
config: _,
}| {
(addr_id.addr_sub().addr() == addr).then_some(*assigned)
},
)
})
},
)
}
#[test]
fn test_dad_multiple_ips_simultaneously() {
let mut ctx = crate::testutil::FakeCtx::default();
let eth_dev_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac(),
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let dev_id = eth_dev_id.clone().into();
crate::device::testutil::enable_device(&mut ctx, &dev_id);
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&dev_id,
Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(NonZeroU16::new(3)),
max_router_solicitations: Some(None),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
// Add an IP.
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&dev_id, AddrSubnet::new(local_ip().into(), 128).unwrap())
.unwrap();
assert_matches!(get_address_assigned(&ctx, &dev_id, local_ip()), Some(false));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
// Send another NS.
let local_timer_id = dad_timer_id(eth_dev_id.clone(), local_ip());
assert_eq!(ctx.trigger_timers_for(Duration::from_secs(1)), [local_timer_id.clone()]);
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
// Add another IP
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&dev_id, AddrSubnet::new(remote_ip().into(), 128).unwrap())
.unwrap();
assert_matches!(get_address_assigned(&ctx, &dev_id, local_ip()), Some(false));
assert_matches!(get_address_assigned(&ctx, &dev_id, remote_ip()), Some(false));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
// Run to the end for DAD for local ip
let remote_timer_id = dad_timer_id(eth_dev_id, remote_ip());
assert_eq!(
ctx.trigger_timers_for(Duration::from_secs(2)),
[
local_timer_id.clone(),
remote_timer_id.clone(),
local_timer_id,
remote_timer_id.clone()
]
);
assert_eq!(get_address_assigned(&ctx, &dev_id, local_ip()), Some(true));
assert_matches!(get_address_assigned(&ctx, &dev_id, remote_ip()), Some(false));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame1, _frame2, _frame3]);
// Run to the end for DAD for local ip
assert_eq!(ctx.trigger_timers_for(Duration::from_secs(1)), [remote_timer_id]);
assert_eq!(get_address_assigned(&ctx, &dev_id, local_ip()), Some(true));
assert_eq!(get_address_assigned(&ctx, &dev_id, remote_ip()), Some(true));
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
// No more timers.
assert_eq!(ctx.trigger_next_timer(), None);
}
#[test]
fn test_dad_cancel_when_ip_removed() {
let mut ctx = crate::testutil::FakeCtx::default();
let eth_dev_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac(),
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let dev_id = eth_dev_id.clone().into();
crate::device::testutil::enable_device(&mut ctx, &dev_id);
// Enable DAD.
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&dev_id,
Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(NonZeroU16::new(3)),
max_router_solicitations: Some(None),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
// Add an IP.
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&dev_id, AddrSubnet::new(local_ip().into(), 128).unwrap())
.unwrap();
assert_matches!(get_address_assigned(&ctx, &dev_id, local_ip()), Some(false));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
// Send another NS.
let local_timer_id = dad_timer_id(eth_dev_id.clone(), local_ip());
assert_eq!(ctx.trigger_timers_for(Duration::from_secs(1)), [local_timer_id.clone()]);
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
// Add another IP
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&dev_id, AddrSubnet::new(remote_ip().into(), 128).unwrap())
.unwrap();
assert_matches!(get_address_assigned(&ctx, &dev_id, local_ip()), Some(false));
assert_matches!(get_address_assigned(&ctx, &dev_id, remote_ip()), Some(false));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
// Run 1s
let remote_timer_id = dad_timer_id(eth_dev_id, remote_ip());
assert_eq!(
ctx.trigger_timers_for(Duration::from_secs(1)),
[local_timer_id, remote_timer_id.clone()]
);
assert_matches!(get_address_assigned(&ctx, &dev_id, local_ip()), Some(false));
assert_matches!(get_address_assigned(&ctx, &dev_id, remote_ip()), Some(false));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame1, _frame2]);
// Remove local ip
ctx.core_api()
.device_ip::<Ipv6>()
.del_ip_addr(&dev_id, local_ip().into_specified())
.unwrap();
assert_eq!(get_address_assigned(&ctx, &dev_id, local_ip()), None);
assert_matches!(get_address_assigned(&ctx, &dev_id, remote_ip()), Some(false));
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
// Run to the end for DAD for local ip
assert_eq!(
ctx.trigger_timers_for(Duration::from_secs(2)),
[remote_timer_id.clone(), remote_timer_id]
);
assert_eq!(get_address_assigned(&ctx, &dev_id, local_ip()), None);
assert_eq!(get_address_assigned(&ctx, &dev_id, remote_ip()), Some(true));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
// No more timers.
assert_eq!(ctx.trigger_next_timer(), None);
}
#[test]
fn test_receiving_router_solicitation_validity_check() {
let config = Ipv6::FAKE_CONFIG;
let src_ip = Ipv6Addr::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 0, 10]);
let src_mac = [10, 11, 12, 13, 14, 15];
let options = vec![NdpOptionBuilder::SourceLinkLayerAddress(&src_mac[..])];
// Test receiving NDP RS when not a router (should not receive)
let (mut ctx, device_ids) = FakeEventDispatcherBuilder::from_config(config).build();
let device_id: DeviceId<_> = device_ids[0].clone().into();
let icmpv6_packet_buf = OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
IcmpUnusedCode,
RouterSolicitation::default(),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap();
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
assert_eq!(ctx.core_ctx.ndp_counters().rx.router_solicitation.get(), 0);
}
#[test]
fn test_receiving_router_advertisement_validity_check() {
fn router_advertisement_message(src_ip: Ipv6Addr, dst_ip: Ipv6Addr) -> Buf<Vec<u8>> {
EmptyBuf
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
RouterAdvertisement::new(
0, /* current_hop_limit */
false, /* managed_flag */
false, /* other_config_flag */
0, /* router_lifetime */
0, /* reachable_time */
0, /* retransmit_timer */
),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
let config = Ipv6::FAKE_CONFIG;
let src_mac = [10, 11, 12, 13, 14, 15];
let src_ip = Ipv6Addr::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 192, 168, 0, 10]);
let (mut ctx, device_ids) = FakeEventDispatcherBuilder::from_config(config.clone()).build();
let device_id: DeviceId<_> = device_ids[0].clone().into();
// Test receiving NDP RA where source IP is not a link local address
// (should not receive).
let icmpv6_packet_buf = router_advertisement_message(src_ip, config.local_ip.get());
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Individual { local: true }),
icmpv6_packet_buf,
);
assert_eq!(ctx.core_ctx.ndp_counters().rx.router_advertisement.get(), 0);
// Test receiving NDP RA where source IP is a link local address (should
// receive).
let src_ip = Mac::new(src_mac).to_ipv6_link_local().addr().get();
let icmpv6_packet_buf = router_advertisement_message(src_ip, config.local_ip.get());
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device_id,
Some(FrameDestination::Individual { local: true }),
icmpv6_packet_buf,
);
assert_eq!(ctx.core_ctx.ndp_counters().rx.router_advertisement.get(), 1);
}
#[test]
fn test_sending_ipv6_packet_after_hop_limit_change() {
// Sets the hop limit with a router advertisement and sends a packet to
// make sure the packet uses the new hop limit.
fn inner_test(
ctx: &mut crate::testutil::FakeCtx,
device_id: &DeviceId<crate::testutil::FakeBindingsCtx>,
hop_limit: u8,
) {
let config = Ipv6::FAKE_CONFIG;
let src_ip = config.remote_mac.to_ipv6_link_local().addr();
let src_ip: Ipv6Addr = src_ip.get();
let icmpv6_packet_buf = EmptyBuf
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
IcmpUnusedCode,
RouterAdvertisement::new(hop_limit, false, false, 0, 0, 0),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b();
ctx.test_api().receive_ip_packet::<Ipv6, _>(
device_id,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
let (mut core_ctx, bindings_ctx) = ctx.contexts();
assert_eq!(get_ipv6_hop_limit(&mut core_ctx, device_id).get(), hop_limit);
crate::ip::send_ip_packet_from_device::<Ipv6, _, _, _>(
&mut core_ctx,
bindings_ctx,
SendIpPacketMeta {
device: device_id,
src_ip: Some(config.local_ip),
dst_ip: config.remote_ip,
broadcast: None,
next_hop: config.remote_ip,
proto: IpProto::Tcp.into(),
ttl: None,
mtu: None,
},
Buf::new(vec![0; 10], ..),
)
.unwrap();
let frames = bindings_ctx.take_ethernet_frames();
let (_dev, frame) = assert_matches!(&frames[..], [frame] => frame);
let (buf, _, _, _) =
parse_ethernet_frame(&frame[..], EthernetFrameLengthCheck::NoCheck).unwrap();
// Packet's hop limit should be 100.
assert_eq!(buf[7], hop_limit);
}
let (mut ctx, device_ids) =
FakeEventDispatcherBuilder::from_config(Ipv6::FAKE_CONFIG).build();
let device_id: DeviceId<_> = device_ids[0].clone().into();
// Set hop limit to 100.
inner_test(&mut ctx, &device_id, 100);
// Set hop limit to 30.
inner_test(&mut ctx, &device_id, 30);
}
#[test]
fn test_receiving_router_advertisement_mtu_option() {
fn packet_buf(src_ip: Ipv6Addr, dst_ip: Ipv6Addr, mtu: u32) -> Buf<Vec<u8>> {
let options = &[NdpOptionBuilder::Mtu(mtu)];
OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
RouterAdvertisement::new(0, false, false, 0, 0, 0),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
let mut ctx = crate::testutil::FakeCtx::default();
let hw_mtu = Mtu::new(5000);
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: local_mac(),
max_frame_size: MaxEthernetFrameSize::from_mtu(hw_mtu).unwrap(),
},
DEFAULT_INTERFACE_METRIC,
)
.into();
let src_mac = Mac::new([10, 11, 12, 13, 14, 15]);
let src_ip = src_mac.to_ipv6_link_local().addr();
crate::device::testutil::enable_device(&mut ctx, &device);
// Receive a new RA with a valid MTU option (but the new MTU should only
// be 5000 as that is the max MTU of the device).
let icmpv6_packet_buf =
packet_buf(src_ip.get(), Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(), 5781);
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
assert_eq!(ctx.core_ctx.ndp_counters().rx.router_advertisement.get(), 1);
assert_eq!(
crate::ip::IpDeviceContext::<Ipv6, _>::get_mtu(&mut ctx.core_ctx(), &device),
hw_mtu
);
// Receive a new RA with an invalid MTU option (value is lower than IPv6
// min MTU).
let icmpv6_packet_buf = packet_buf(
src_ip.get(),
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
u32::from(Ipv6::MINIMUM_LINK_MTU) - 1,
);
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
assert_eq!(ctx.core_ctx.ndp_counters().rx.router_advertisement.get(), 2);
assert_eq!(
crate::ip::IpDeviceContext::<Ipv6, _>::get_mtu(&mut ctx.core_ctx(), &device),
hw_mtu
);
// Receive a new RA with a valid MTU option (value is exactly IPv6 min
// MTU).
let icmpv6_packet_buf = packet_buf(
src_ip.get(),
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
Ipv6::MINIMUM_LINK_MTU.into(),
);
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
assert_eq!(ctx.core_ctx.ndp_counters().rx.router_advertisement.get(), 3);
assert_eq!(
crate::ip::IpDeviceContext::<Ipv6, _>::get_mtu(&mut ctx.core_ctx(), &device),
Ipv6::MINIMUM_LINK_MTU,
);
}
fn get_source_link_layer_option<L: LinkAddress, B>(options: &Options<B>) -> Option<L>
where
B: ByteSlice,
{
options.iter().find_map(|o| match o {
NdpOption::SourceLinkLayerAddress(a) => {
if a.len() >= L::BYTES_LENGTH {
Some(L::from_bytes(&a[..L::BYTES_LENGTH]))
} else {
None
}
}
_ => None,
})
}
#[test]
fn test_host_send_router_solicitations() {
#[track_caller]
fn validate_params(
src_mac: Mac,
src_ip: Ipv6Addr,
message: RouterSolicitation,
code: IcmpUnusedCode,
) {
let fake_config = Ipv6::FAKE_CONFIG;
assert_eq!(src_mac, fake_config.local_mac.get());
assert_eq!(src_ip, fake_config.local_mac.to_ipv6_link_local().addr().get());
assert_eq!(message, RouterSolicitation::default());
assert_eq!(code, IcmpUnusedCode);
}
let fake_config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
let eth_device_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: fake_config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let device_id = eth_device_id.clone().into();
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device_id,
Ipv6DeviceConfigurationUpdate {
// Test expects to send 3 RSs.
max_router_solicitations: Some(NonZeroU8::new(3)),
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
let time = ctx.bindings_ctx.now();
assert_eq!(ctx.trigger_next_timer().unwrap(), rs_timer_id(eth_device_id.clone()));
// Initial router solicitation should be a random delay between 0 and
// `MAX_RTR_SOLICITATION_DELAY`.
assert!(ctx.bindings_ctx.now().duration_since(time) < MAX_RTR_SOLICITATION_DELAY);
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (_dev, frame) = assert_matches!(&frames[..], [frame] => frame);
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&frame,
EthernetFrameLengthCheck::NoCheck,
|_| {},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
// Should get 2 more router solicitation messages
let time = ctx.bindings_ctx.now();
assert_eq!(ctx.trigger_next_timer().unwrap(), rs_timer_id(eth_device_id.clone()));
assert_eq!(ctx.bindings_ctx.now().duration_since(time), RTR_SOLICITATION_INTERVAL);
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (_dev, frame) = assert_matches!(&frames[..], [frame] => frame);
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&frame,
EthernetFrameLengthCheck::NoCheck,
|_| {},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
// Before the next one, lets assign an IP address (DAD won't be
// performed so it will be assigned immediately). The router solicitation
// message should continue to use the link-local address.
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(
&device_id,
AddrSubnet::new(fake_config.local_ip.get(), 128).unwrap(),
)
.unwrap();
let time = ctx.bindings_ctx.now();
assert_eq!(ctx.trigger_next_timer().unwrap(), rs_timer_id(eth_device_id));
assert_eq!(ctx.bindings_ctx.now().duration_since(time), RTR_SOLICITATION_INTERVAL);
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (_dev, frame) = assert_matches!(&frames[..], [frame] => frame);
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&frame,
EthernetFrameLengthCheck::NoCheck,
|p| {
// We should have a source link layer option now because we
// have a source IP address set.
assert_eq!(p.body().iter().count(), 1);
if let Some(ll) = get_source_link_layer_option::<Mac, _>(p.body()) {
assert_eq!(ll, fake_config.local_mac.get());
} else {
panic!("Should have a source link layer option");
}
},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
// No more timers.
assert_eq!(ctx.trigger_next_timer(), None);
// Should have only sent 3 packets (Router solicitations).
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
let mut ctx = crate::testutil::FakeCtx::default();
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
let eth_device_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: fake_config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let device_id = eth_device_id.clone().into();
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device_id,
Ipv6DeviceConfigurationUpdate {
max_router_solicitations: Some(NonZeroU8::new(2)),
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
let time = ctx.bindings_ctx.now();
assert_eq!(ctx.trigger_next_timer().unwrap(), rs_timer_id(eth_device_id.clone()));
// Initial router solicitation should be a random delay between 0 and
// `MAX_RTR_SOLICITATION_DELAY`.
assert!(ctx.bindings_ctx.now().duration_since(time) < MAX_RTR_SOLICITATION_DELAY);
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (_dev, frame1) = assert_matches!(&frames[..], [frame] => frame);
// Should trigger 1 more router solicitations
let time = ctx.bindings_ctx.now();
assert_eq!(ctx.trigger_next_timer().unwrap(), rs_timer_id(eth_device_id));
assert_eq!(ctx.bindings_ctx.now().duration_since(time), RTR_SOLICITATION_INTERVAL);
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (_dev, frame2) = assert_matches!(&frames[..], [frame] => frame);
// Each packet would be the same.
for f in [frame1, frame2] {
let (src_mac, _, src_ip, _, _, message, code) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&f,
EthernetFrameLengthCheck::NoCheck,
|_| {},
)
.unwrap();
validate_params(src_mac, src_ip, message, code);
}
// No more timers.
assert_eq!(ctx.trigger_next_timer(), None);
}
#[test]
fn test_router_solicitation_on_forwarding_enabled_changes() {
// Make sure that when an interface goes from host -> router, it stops
// sending Router Solicitations, and starts sending them when it goes
// form router -> host as routers should not send Router Solicitation
// messages, but hosts should.
let fake_config = Ipv6::FAKE_CONFIG;
// If netstack is not set to forward packets, make sure router
// solicitations do not get cancelled when we enable forwarding on the
// device.
let mut ctx = crate::testutil::FakeCtx::default();
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
assert_empty(ctx.bindings_ctx.timer_ctx().timers());
let eth_device =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: fake_config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let device = eth_device.clone().into();
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
// Doesn't matter as long as we are configured to send at least 2
// solicitations.
max_router_solicitations: Some(NonZeroU8::new(2)),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let timer_id: TimerId<_> =
rs_timer_id(device.clone().try_into().expect("expected ethernet ID"));
// Send the first router solicitation.
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
ctx.bindings_ctx.timer_ctx().assert_timers_installed_range([(timer_id.clone(), ..)]);
assert_eq!(ctx.trigger_next_timer().unwrap(), timer_id);
// Should have sent a router solicitation and still have the timer
// setup.
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (_dev, frame) = assert_matches!(&frames[..], [frame] => frame);
let (_, _dst_mac, _, _, _, _, _) =
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&frame,
EthernetFrameLengthCheck::NoCheck,
|_| {},
)
.unwrap();
ctx.bindings_ctx.timer_ctx().assert_timers_installed_range([(timer_id.clone(), ..)]);
// Enable routing on device.
set_forwarding_enabled::<_, Ipv6>(&mut ctx, &device, true);
assert!(is_forwarding_enabled::<_, Ipv6>(&mut ctx, &device));
// Should have not sent any new packets, but unset the router
// solicitation timer.
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
assert_empty(ctx.bindings_ctx.timer_ctx().timers().iter().filter(|x| &x.1 == &timer_id));
// Unsetting routing should succeed.
set_forwarding_enabled::<_, Ipv6>(&mut ctx, &device, false);
assert!(!is_forwarding_enabled::<_, Ipv6>(&mut ctx, &device));
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
ctx.bindings_ctx.timer_ctx().assert_timers_installed_range([(timer_id.clone(), ..)]);
// Send the first router solicitation after being turned into a host.
assert_eq!(ctx.trigger_next_timer().unwrap(), timer_id);
// Should have sent a router solicitation.
let frames = ctx.bindings_ctx.take_ethernet_frames();
let (_dev, frame) = assert_matches!(&frames[..], [frame] => frame);
assert_matches!(
parse_icmp_packet_in_ip_packet_in_ethernet_frame::<Ipv6, _, RouterSolicitation, _>(
&frame,
EthernetFrameLengthCheck::NoCheck,
|_| {},
),
Ok((_, _, _, _, _, _, _))
);
ctx.bindings_ctx.timer_ctx().assert_timers_installed_range([(timer_id, ..)]);
// Clear all device references.
core::mem::drop(device);
ctx.core_api().device().remove_device(eth_device).into_removed();
}
#[test]
fn test_set_ndp_config_dup_addr_detect_transmits() {
// Test that updating the DupAddrDetectTransmits parameter on an
// interface updates the number of DAD messages (NDP Neighbor
// Solicitations) sent before concluding that an address is not a
// duplicate.
let fake_config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: fake_config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
crate::device::testutil::enable_device(&mut ctx, &device);
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
assert_empty(ctx.bindings_ctx.timer_ctx().timers());
// Updating the IP should resolve immediately since DAD is turned off by
// `FakeEventDispatcherBuilder::build`.
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&device, AddrSubnet::new(fake_config.local_ip.get(), 128).unwrap())
.unwrap();
let device_id = device.clone().try_into().unwrap();
assert_eq!(get_address_assigned(&ctx, &device, local_ip()), Some(true));
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
assert_empty(ctx.bindings_ctx.timer_ctx().timers());
// Enable DAD for the device.
const DUP_ADDR_DETECT_TRANSMITS: u16 = 3;
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(NonZeroU16::new(DUP_ADDR_DETECT_TRANSMITS)),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
// Updating the IP should start the DAD process.
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&device, AddrSubnet::new(fake_config.remote_ip.get(), 128).unwrap())
.unwrap();
assert_eq!(get_address_assigned(&ctx, &device, local_ip()), Some(true));
assert_eq!(get_address_assigned(&ctx, &device, remote_ip()), Some(false));
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
assert_eq!(ctx.bindings_ctx.timer_ctx().timers().len(), 1);
// Disable DAD during DAD.
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate { dad_transmits: Some(None), ..Default::default() },
)
.unwrap();
let expected_timer_id = dad_timer_id(device_id, remote_ip());
// Allow already started DAD to complete (2 more more NS, 3 more timers).
assert_eq!(ctx.trigger_next_timer().unwrap(), expected_timer_id);
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
assert_eq!(ctx.trigger_next_timer().unwrap(), expected_timer_id);
assert_matches!(&ctx.bindings_ctx.take_ethernet_frames()[..], [_frame]);
assert_eq!(ctx.trigger_next_timer().unwrap(), expected_timer_id);
assert_matches!(ctx.bindings_ctx.take_ethernet_frames()[..], []);
assert_eq!(get_address_assigned(&ctx, &device, remote_ip()), Some(true));
// Updating the IP should resolve immediately since DAD has just been
// turned off.
let new_ip = Ipv6::get_other_ip_address(3);
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&device, AddrSubnet::new(new_ip.get(), 128).unwrap())
.unwrap();
assert_eq!(get_address_assigned(&ctx, &device, local_ip()), Some(true));
assert_eq!(get_address_assigned(&ctx, &device, remote_ip()), Some(true));
assert_eq!(
get_address_assigned(
&ctx,
&device,
NonMappedAddr::new(new_ip.try_into().unwrap()).unwrap()
),
Some(true)
);
}
fn slaac_packet_buf(
src_ip: Ipv6Addr,
dst_ip: Ipv6Addr,
prefix: Ipv6Addr,
prefix_length: u8,
on_link_flag: bool,
autonomous_address_configuration_flag: bool,
valid_lifetime_secs: u32,
preferred_lifetime_secs: u32,
) -> Buf<Vec<u8>> {
let p = PrefixInformation::new(
prefix_length,
on_link_flag,
autonomous_address_configuration_flag,
valid_lifetime_secs,
preferred_lifetime_secs,
prefix,
);
let options = &[NdpOptionBuilder::PrefixInformation(p)];
OptionSequenceBuilder::new(options.iter())
.into_serializer()
.encapsulate(IcmpPacketBuilder::<Ipv6, _>::new(
src_ip,
dst_ip,
IcmpUnusedCode,
RouterAdvertisement::new(0, false, false, 0, 0, 0),
))
.encapsulate(Ipv6PacketBuilder::new(
src_ip,
dst_ip,
REQUIRED_NDP_IP_PACKET_HOP_LIMIT,
Ipv6Proto::Icmpv6,
))
.serialize_vec_outer()
.unwrap()
.unwrap_b()
}
#[test]
fn test_router_stateless_address_autoconfiguration() {
// Routers should not perform SLAAC for global addresses.
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
crate::device::testutil::enable_device(&mut ctx, &device);
set_forwarding_enabled::<_, Ipv6>(&mut ctx, &device, true);
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&src_mac.to_eui64()[..]);
// Receive a new RA with new prefix (autonomous).
//
// Should not get a new IP.
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
false,
100,
0,
);
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
assert_empty(get_global_ipv6_addrs(&ctx, &device));
// No timers.
assert_eq!(ctx.trigger_next_timer(), None);
}
impl From<SlaacTimerId<DeviceId<crate::testutil::FakeBindingsCtx>>>
for TimerId<crate::testutil::FakeBindingsCtx>
{
fn from(
id: SlaacTimerId<DeviceId<crate::testutil::FakeBindingsCtx>>,
) -> TimerId<crate::testutil::FakeBindingsCtx> {
TimerId(TimerIdInner::Ipv6Device(Ipv6DeviceTimerId::Slaac(id).into()))
}
}
#[derive(Copy, Clone, Debug)]
struct TestSlaacPrefix {
prefix: Subnet<Ipv6Addr>,
valid_for: u32,
preferred_for: u32,
}
impl TestSlaacPrefix {
fn send_prefix_update(
&self,
ctx: &mut crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
src_ip: Ipv6Addr,
) {
let Self { prefix, valid_for, preferred_for } = *self;
receive_prefix_update(ctx, device, src_ip, prefix, preferred_for, valid_for);
}
fn valid_until<I: Instant>(&self, now: I) -> I {
now.checked_add(Duration::from_secs(self.valid_for.into())).unwrap()
}
}
fn slaac_address<I: Instant>(
entry: GlobalIpv6Addr<I>,
) -> Option<(UnicastAddr<Ipv6Addr>, SlaacConfig<I>)> {
match entry.config {
Ipv6AddrConfig::Manual(_manual_config) => None,
Ipv6AddrConfig::Slaac(s) => Some((entry.addr_sub.addr().get(), s)),
}
}
/// Extracts the single static and temporary address config from the provided iterator and
/// returns them as (static, temporary).
///
/// Panics
///
/// Panics if the iterator doesn't contain exactly one static and one temporary SLAAC entry.
fn single_static_and_temporary<
I: Copy + Debug,
A: Copy + Debug,
It: Iterator<Item = (A, SlaacConfig<I>)>,
>(
slaac_configs: It,
) -> ((A, SlaacConfig<I>), (A, SlaacConfig<I>)) {
{
let (static_addresses, temporary_addresses): (Vec<_>, Vec<_>) = slaac_configs
.partition(|(_, s)| if let SlaacConfig::Static { .. } = s { true } else { false });
let static_addresses: [_; 1] =
static_addresses.try_into().expect("expected a single static address");
let temporary_addresses: [_; 1] =
temporary_addresses.try_into().expect("expected a single temporary address");
(static_addresses[0], temporary_addresses[0])
}
}
/// Enables temporary addressing with the provided parameters.
///
/// `rng` is used to initialize the key that is used to generate new addresses.
fn enable_temporary_addresses<R: RngCore>(
config: &mut SlaacConfiguration,
mut rng: R,
max_valid_lifetime: NonZeroDuration,
max_preferred_lifetime: NonZeroDuration,
max_generation_retries: u8,
) {
let mut secret_key = [0; STABLE_IID_SECRET_KEY_BYTES];
rng.fill_bytes(&mut secret_key);
config.temporary_address_configuration = Some(TemporarySlaacAddressConfiguration {
temp_valid_lifetime: max_valid_lifetime,
temp_preferred_lifetime: max_preferred_lifetime,
temp_idgen_retries: max_generation_retries,
secret_key,
})
}
fn initialize_with_temporary_addresses_enabled(
) -> (crate::testutil::FakeCtx, DeviceId<crate::testutil::FakeBindingsCtx>, SlaacConfiguration)
{
set_logger_for_test();
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
crate::device::testutil::enable_device(&mut ctx, &device);
let max_valid_lifetime = Duration::from_secs(60 * 60);
let max_preferred_lifetime = Duration::from_secs(30 * 60);
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
ctx.bindings_ctx.rng(),
NonZeroDuration::new(max_valid_lifetime).unwrap(),
NonZeroDuration::new(max_preferred_lifetime).unwrap(),
idgen_retries,
);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(slaac_config),
..Default::default()
},
)
.unwrap();
(ctx, device, slaac_config)
}
#[test]
fn test_host_stateless_address_autoconfiguration_multiple_prefixes() {
let (mut ctx, device, _): (_, _, SlaacConfiguration) =
initialize_with_temporary_addresses_enabled();
let mut api = ctx.core_api().device_ip::<Ipv6>();
let config = api.get_configuration(&device);
let _: Ipv6DeviceConfigurationUpdate = api
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..config.config.slaac_config
}),
..Default::default()
},
)
.unwrap();
let prefix1 = TestSlaacPrefix {
prefix: subnet_v6!("1:2:3:4::/64"),
valid_for: 1500,
preferred_for: 900,
};
let prefix2 = TestSlaacPrefix {
prefix: subnet_v6!("5:6:7:8::/64"),
valid_for: 1200,
preferred_for: 600,
};
let config = Ipv6::FAKE_CONFIG;
let src_mac = config.remote_mac;
let src_ip: Ipv6Addr = src_mac.to_ipv6_link_local().addr().get();
// After the RA for the first prefix, we should have two addresses, one
// static and one temporary.
prefix1.send_prefix_update(&mut ctx, &device, src_ip);
let (prefix_1_static, prefix_1_temporary) = {
let slaac_configs = get_global_ipv6_addrs(&ctx, &device)
.into_iter()
.filter_map(slaac_address)
.filter(|(a, _)| prefix1.prefix.contains(a));
let (static_address, temporary_address) = single_static_and_temporary(slaac_configs);
let now = ctx.bindings_ctx.now();
let prefix1_valid_until = prefix1.valid_until(now);
assert_matches!(static_address, (_addr,
SlaacConfig::Static { valid_until }) => {
assert_eq!(valid_until, Lifetime::Finite(prefix1_valid_until))
});
assert_matches!(temporary_address, (_addr,
SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until,
creation_time,
desync_factor: _,
dad_counter: _ })) => {
assert_eq!(creation_time, now);
assert_eq!(valid_until, prefix1_valid_until);
});
(static_address.0, temporary_address.0)
};
// When the RA for the second prefix comes in, we should leave the entries for the addresses
// in the first prefix alone.
prefix2.send_prefix_update(&mut ctx, &device, src_ip);
{
// Check prefix 1 addresses again.
let slaac_configs = get_global_ipv6_addrs(&ctx, &device)
.into_iter()
.filter_map(slaac_address)
.filter(|(a, _)| prefix1.prefix.contains(a));
let (static_address, temporary_address) = single_static_and_temporary(slaac_configs);
let now = ctx.bindings_ctx.now();
let prefix1_valid_until = prefix1.valid_until(now);
assert_matches!(static_address, (addr, SlaacConfig::Static { valid_until }) => {
assert_eq!(addr, prefix_1_static);
assert_eq!(valid_until, Lifetime::Finite(prefix1_valid_until));
});
assert_matches!(temporary_address,
(addr, SlaacConfig::Temporary(TemporarySlaacConfig { valid_until, creation_time, desync_factor: _, dad_counter: 0 })) => {
assert_eq!(addr, prefix_1_temporary);
assert_eq!(creation_time, now);
assert_eq!(valid_until, prefix1_valid_until);
});
}
{
// Check prefix 2 addresses.
let slaac_configs = get_global_ipv6_addrs(&ctx, &device)
.into_iter()
.filter_map(slaac_address)
.filter(|(a, _)| prefix2.prefix.contains(a));
let (static_address, temporary_address) = single_static_and_temporary(slaac_configs);
let now = ctx.bindings_ctx.now();
let prefix2_valid_until = prefix2.valid_until(now);
assert_matches!(static_address, (_, SlaacConfig::Static { valid_until }) => {
assert_eq!(valid_until, Lifetime::Finite(prefix2_valid_until))
});
assert_matches!(temporary_address,
(_, SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until, creation_time, desync_factor: _, dad_counter: 0 })) => {
assert_eq!(creation_time, now);
assert_eq!(valid_until, prefix2_valid_until);
});
}
// Clean up device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
}
fn test_host_generate_temporary_slaac_address(
valid_lifetime_in_ra: u32,
preferred_lifetime_in_ra: u32,
) -> (crate::testutil::FakeCtx, DeviceId<crate::testutil::FakeBindingsCtx>, UnicastAddr<Ipv6Addr>)
{
set_logger_for_test();
let (mut ctx, device, slaac_config) = initialize_with_temporary_addresses_enabled();
let max_valid_lifetime =
slaac_config.temporary_address_configuration.unwrap().temp_valid_lifetime.get();
let config = Ipv6::FAKE_CONFIG;
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let subnet = subnet_v6!("0102:0304:0506:0708::/64");
let interface_identifier = generate_opaque_interface_identifier(
subnet,
&config.local_mac.to_eui64()[..],
[],
// Clone the RNG so we can see what the next value (which will be
// used to generate the temporary address) will be.
OpaqueIidNonce::Random(ctx.bindings_ctx.rng().deep_clone().next_u64()),
&slaac_config.temporary_address_configuration.unwrap().secret_key,
);
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&interface_identifier.to_be_bytes()[..8]);
let expected_addr = UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, subnet.prefix()).unwrap();
assert_eq!(expected_addr_sub.subnet(), subnet);
// Receive a new RA with new prefix (autonomous).
//
// Should get a new temporary IP.
receive_prefix_update(
&mut ctx,
&device,
src_ip,
subnet,
preferred_lifetime_in_ra,
valid_lifetime_in_ra,
);
// Should have gotten a new temporary IP.
let temporary_slaac_addresses = get_global_ipv6_addrs(&ctx, &device)
.into_iter()
.filter_map(|entry| match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Static { .. }) => None,
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
creation_time: _,
desync_factor: _,
valid_until,
dad_counter: _,
})) => Some((*entry.addr_sub(), entry.flags.assigned, valid_until)),
Ipv6AddrConfig::Manual(_manual_config) => None,
})
.collect::<Vec<_>>();
assert_eq!(temporary_slaac_addresses.len(), 1);
let (addr_sub, assigned, valid_until) =
temporary_slaac_addresses.into_iter().next().unwrap();
assert_eq!(addr_sub.subnet(), subnet);
assert!(assigned);
assert!(valid_until <= ctx.bindings_ctx.now().checked_add(max_valid_lifetime).unwrap());
(ctx, device, expected_addr)
}
const INFINITE_LIFETIME: u32 = u32::MAX;
#[test]
fn test_host_temporary_slaac_and_manual_addresses_conflict() {
// Verify that if the temporary SLAAC address generation picks an
// address that is already assigned, it tries again. The difficulty here
// is that the test uses an RNG to pick an address. To make sure we
// assign the address that the code _would_ pick, we run the code twice
// with the same RNG seed and parameters. The first time is lets us
// figure out the temporary address that is generated. Then, we run the
// same code with the address already assigned to verify the behavior.
const RNG_SEED: [u8; 16] = [1; 16];
let config = Ipv6::FAKE_CONFIG;
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let subnet = subnet_v6!("0102:0304:0506:0708::/64");
// Receive an RA to figure out the temporary address that is assigned.
let conflicted_addr = {
let (mut ctx, device, _config) = initialize_with_temporary_addresses_enabled();
*ctx.bindings_ctx.rng().rng() = rand::SeedableRng::from_seed(RNG_SEED);
// Receive an RA and determine what temporary address was assigned, then return it.
receive_prefix_update(&mut ctx, &device, src_ip, subnet, 9000, 10000);
let conflicted_addr =
*get_matching_slaac_address_entry(&ctx, &device, |entry| match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Manual(_manual_config) => false,
})
.unwrap()
.addr_sub();
// Clean up device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
conflicted_addr
};
assert!(subnet.contains(&conflicted_addr.addr().get()));
// Now that we know what address will be assigned, create a new instance
// of the stack and assign that same address manually.
let (mut ctx, device, _config) = initialize_with_temporary_addresses_enabled();
ctx.core_api()
.device_ip::<Ipv6>()
.add_ip_addr_subnet(&device, conflicted_addr.to_witness())
.expect("adding address failed");
// Sanity check: `conflicted_addr` is already assigned on the device.
assert_matches!(
get_global_ipv6_addrs(&ctx, &device)
.into_iter()
.find(|entry| entry.addr_sub() == &conflicted_addr),
Some(_)
);
// Seed the RNG right before the RA is received, just like in our
// earlier run above.
*ctx.bindings_ctx.rng().rng() = rand::SeedableRng::from_seed(RNG_SEED);
// Receive a new RA with new prefix (autonomous). The system will assign
// a temporary and static SLAAC address. The first temporary address
// tried will conflict with `conflicted_addr` assigned above, so a
// different one will be generated.
receive_prefix_update(&mut ctx, &device, src_ip, subnet, 9000, 10000);
// Verify that `conflicted_addr` was generated and rejected.
assert_eq!(ctx.core_ctx.slaac_counters().generated_slaac_addr_exists.get(), 1);
// Should have gotten a new temporary IP.
let temporary_slaac_addresses =
get_matching_slaac_address_entries(&ctx, &device, |entry| match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Manual(_manual_config) => false,
})
.map(|entry| *entry.addr_sub())
.collect::<Vec<_>>();
assert_matches!(&temporary_slaac_addresses[..], [temporary_addr] => {
assert_eq!(temporary_addr.subnet(), conflicted_addr.subnet());
assert_ne!(temporary_addr, &conflicted_addr);
});
// Clean up device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
}
#[test]
fn test_host_slaac_invalid_prefix_information() {
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
crate::device::testutil::enable_device(&mut ctx, &device);
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
assert_empty(get_global_ipv6_addrs(&ctx, &device));
// Receive a new RA with new prefix (autonomous), but preferred lifetime
// is greater than valid.
//
// Should not get a new IP.
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
true,
9000,
10000,
);
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
assert_empty(get_global_ipv6_addrs(&ctx, &device));
// Address invalidation timers were added.
assert_empty(ctx.bindings_ctx.timer_ctx().timers());
}
#[test]
fn test_host_slaac_address_deprecate_while_tentative() {
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
crate::device::testutil::enable_device(&mut ctx, &device);
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = subnet_v6!("0102:0304:0506:0708::/64");
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&config.local_mac.to_eui64()[..]);
let expected_addr =
NonMappedAddr::new(UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap()).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, prefix.prefix()).unwrap();
// Have no addresses yet.
assert_empty(get_global_ipv6_addrs(&ctx, &device));
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
// Doesn't matter as long as we perform DAD.
dad_transmits: Some(NonZeroU16::new(1)),
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let Ctx { core_ctx, bindings_ctx } = &mut ctx;
// Set the retransmit timer between neighbor solicitations to be greater
// than the preferred lifetime of the prefix.
Ipv6DeviceHandler::set_discovered_retrans_timer(
&mut core_ctx.context(),
bindings_ctx,
&device,
const_unwrap::const_unwrap_option(NonZeroDuration::from_secs(10)),
);
// Receive a new RA with new prefix (autonomous).
//
// Should get a new IP and set preferred lifetime to 1s.
let valid_lifetime = 2;
let preferred_lifetime = 1;
receive_prefix_update(
&mut ctx,
&device,
src_ip,
prefix,
preferred_lifetime,
valid_lifetime,
);
// Should have gotten a new IP.
let now = ctx.bindings_ctx.now();
let valid_until = now + Duration::from_secs(valid_lifetime.into());
let expected_address_entry = GlobalIpv6Addr {
addr_sub: expected_addr_sub,
config: Ipv6AddrConfig::Slaac(SlaacConfig::Static {
valid_until: Lifetime::Finite(valid_until),
}),
flags: Ipv6AddressFlags { deprecated: false, assigned: false },
};
assert_eq!(get_global_ipv6_addrs(&ctx, &device), [expected_address_entry]);
// Make sure deprecate and invalidation timers are set.
ctx.bindings_ctx.timer_ctx().assert_some_timers_installed([
(
SlaacTimerId::new_deprecate_slaac_address(device.clone(), expected_addr).into(),
now + Duration::from_secs(preferred_lifetime.into()),
),
(
SlaacTimerId::new_invalidate_slaac_address(device.clone(), expected_addr).into(),
valid_until,
),
]);
// Trigger the deprecation timer.
assert_eq!(
ctx.trigger_next_timer().unwrap(),
SlaacTimerId::new_deprecate_slaac_address(device.clone(), expected_addr).into()
);
assert_eq!(
get_global_ipv6_addrs(&ctx, &device),
[GlobalIpv6Addr {
flags: Ipv6AddressFlags { deprecated: true, ..expected_address_entry.flags },
..expected_address_entry
}]
);
// Clear all device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
}
fn receive_prefix_update(
ctx: &mut crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
src_ip: Ipv6Addr,
subnet: Subnet<Ipv6Addr>,
preferred_lifetime: u32,
valid_lifetime: u32,
) {
let prefix = subnet.network();
let prefix_length = subnet.prefix();
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
true,
valid_lifetime,
preferred_lifetime,
);
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
}
fn get_matching_slaac_address_entries<F: FnMut(&GlobalIpv6Addr<FakeInstant>) -> bool>(
ctx: &crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
filter: F,
) -> impl Iterator<Item = GlobalIpv6Addr<FakeInstant>> {
get_global_ipv6_addrs(ctx, device).into_iter().filter(filter)
}
fn get_matching_slaac_address_entry<F: FnMut(&GlobalIpv6Addr<FakeInstant>) -> bool>(
ctx: &crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
filter: F,
) -> Option<GlobalIpv6Addr<FakeInstant>> {
let mut matching_addrs = get_matching_slaac_address_entries(ctx, device, filter);
let entry = matching_addrs.next();
assert_eq!(matching_addrs.next(), None);
entry
}
fn get_slaac_address_entry(
ctx: &crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
addr_sub: AddrSubnet<Ipv6Addr, Ipv6DeviceAddr>,
) -> Option<GlobalIpv6Addr<FakeInstant>> {
let mut matching_addrs = get_global_ipv6_addrs(ctx, device)
.into_iter()
.filter(|entry| *entry.addr_sub() == addr_sub);
let entry = matching_addrs.next();
assert_eq!(matching_addrs.next(), None);
entry
}
fn assert_slaac_lifetimes_enforced(
bindings_ctx: &crate::testutil::FakeBindingsCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
entry: GlobalIpv6Addr<FakeInstant>,
valid_until: FakeInstant,
preferred_until: FakeInstant,
) {
assert!(entry.flags.assigned);
assert_matches!(entry.config, Ipv6AddrConfig::Slaac(_));
let entry_valid_until = match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until }) => valid_until,
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until,
desync_factor: _,
creation_time: _,
dad_counter: _,
})) => Lifetime::Finite(valid_until),
Ipv6AddrConfig::Manual(_manual_config) => unreachable!(),
};
assert_eq!(entry_valid_until, Lifetime::Finite(valid_until));
bindings_ctx.timer_ctx().assert_some_timers_installed([
(
SlaacTimerId::new_deprecate_slaac_address(device.clone(), entry.addr_sub().addr())
.into(),
preferred_until,
),
(
SlaacTimerId::new_invalidate_slaac_address(device.clone(), entry.addr_sub().addr())
.into(),
valid_until,
),
]);
}
#[test]
fn test_host_static_slaac_valid_lifetime_updates() {
// Make sure we update the valid lifetime only in certain scenarios
// to prevent denial-of-service attacks as outlined in RFC 4862 section
// 5.5.3.e. Note, the preferred lifetime should always be updated.
set_logger_for_test();
fn inner_test(
ctx: &mut crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
src_ip: Ipv6Addr,
subnet: Subnet<Ipv6Addr>,
addr_sub: AddrSubnet<Ipv6Addr, Ipv6DeviceAddr>,
preferred_lifetime: u32,
valid_lifetime: u32,
expected_valid_lifetime: u32,
) {
receive_prefix_update(ctx, device, src_ip, subnet, preferred_lifetime, valid_lifetime);
let entry = get_slaac_address_entry(&ctx, &device, addr_sub).unwrap();
let now = ctx.bindings_ctx.now();
let valid_until =
now.checked_add(Duration::from_secs(expected_valid_lifetime.into())).unwrap();
let preferred_until =
now.checked_add(Duration::from_secs(preferred_lifetime.into())).unwrap();
assert_slaac_lifetimes_enforced(
&ctx.bindings_ctx,
&device,
entry,
valid_until,
preferred_until,
);
}
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&config.local_mac.to_eui64()[..]);
let expected_addr =
NonMappedAddr::new(UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap()).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, prefix_length).unwrap();
// Have no addresses yet.
assert_empty(get_global_ipv6_addrs(&ctx, &device));
// Receive a new RA with new prefix (autonomous).
//
// Should get a new IP and set preferred lifetime to 1s.
// Make sure deprecate and invalidation timers are set.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 30, 60, 60);
// If the valid lifetime is greater than the remaining lifetime, update
// the valid lifetime.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 70, 70, 70);
// If the valid lifetime is greater than 2 hrs, update the valid
// lifetime.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1001, 7201, 7201);
// Make remaining lifetime < 2 hrs.
assert_eq!(ctx.trigger_timers_for(Duration::from_secs(1000)), []);
// If the remaining lifetime is <= 2 hrs & valid lifetime is less than
// that, don't update valid lifetime.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1000, 2000, 6201);
// Make the remaining lifetime > 2 hours.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1000, 10800, 10800);
// If the remaining lifetime is > 2 hours, and new valid lifetime is < 2
// hours, set the valid lifetime to 2 hours.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1000, 1000, 7200);
// If the remaining lifetime is <= 2 hrs & valid lifetime is less than
// that, don't update valid lifetime.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1000, 2000, 7200);
// Increase valid lifetime twice while it is greater than 2 hours.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1001, 7201, 7201);
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1001, 7202, 7202);
// Make remaining lifetime < 2 hrs.
assert_eq!(ctx.trigger_timers_for(Duration::from_secs(1000)), []);
// If the remaining lifetime is <= 2 hrs & valid lifetime is less than
// that, don't update valid lifetime.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1001, 6202, 6202);
// Increase valid lifetime twice while it is less than 2 hours.
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1001, 6203, 6203);
inner_test(&mut ctx, &device, src_ip, subnet, expected_addr_sub, 1001, 6204, 6204);
// Clean up device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
}
#[test]
fn test_host_temporary_slaac_regenerates_address_on_dad_failure() {
// Check that when a tentative temporary address is detected as a
// duplicate, a new address gets created.
set_logger_for_test();
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
const MAX_PREFERRED_LIFETIME: Duration = Duration::from_secs(5000);
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
ctx.bindings_ctx.rng(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(MAX_PREFERRED_LIFETIME).unwrap(),
idgen_retries,
);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
// Doesn't matter as long as we perform DAD.
dad_transmits: Some(NonZeroU16::new(1)),
slaac_config: Some(slaac_config),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
// Send an update with lifetimes that are smaller than the ones specified in the preferences.
let valid_lifetime = 10000;
let preferred_lifetime = 4000;
receive_prefix_update(
&mut ctx,
&device,
router_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let first_addr_entry = get_matching_slaac_address_entry(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.unwrap();
assert!(!first_addr_entry.flags.assigned);
receive_neighbor_advertisement_for_duplicate_address(
&mut ctx,
&device,
first_addr_entry.addr_sub().addr(),
);
// In response to the advertisement with the duplicate address, a
// different address should be selected.
let second_addr_entry = get_matching_slaac_address_entry(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.unwrap();
let first_addr_entry_valid = assert_matches!(first_addr_entry.config,
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
valid_until, creation_time: _, desync_factor: _, dad_counter: 0})) => {valid_until});
let first_addr_sub = first_addr_entry.addr_sub();
let second_addr_sub = second_addr_entry.addr_sub();
assert_eq!(first_addr_sub.subnet(), second_addr_sub.subnet());
assert_ne!(first_addr_sub.addr(), second_addr_sub.addr());
assert_matches!(second_addr_entry.config, Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(
TemporarySlaacConfig {
valid_until,
creation_time,
desync_factor: _,
dad_counter: 1,
})) => {
assert_eq!(creation_time, ctx.bindings_ctx.now());
assert_eq!(valid_until, first_addr_entry_valid);
});
// Clean up device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
}
fn receive_neighbor_advertisement_for_duplicate_address(
ctx: &mut crate::testutil::FakeCtx,
device: &DeviceId<crate::testutil::FakeBindingsCtx>,
source_ip: Ipv6DeviceAddr,
) {
let peer_mac = mac!("00:11:22:33:44:55");
let dest_ip = Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get();
let router_flag = false;
let solicited_flag = false;
let override_flag = true;
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
testutil::neighbor_advertisement_ip_packet(
source_ip.into(),
dest_ip,
router_flag,
solicited_flag,
override_flag,
peer_mac,
),
)
}
#[test]
fn test_host_temporary_slaac_gives_up_after_dad_failures() {
// Check that when the chosen tentative temporary addresses are detected
// as duplicates enough times, the system gives up.
set_logger_for_test();
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
const MAX_PREFERRED_LIFETIME: Duration = Duration::from_secs(5000);
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
ctx.bindings_ctx.rng(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(MAX_PREFERRED_LIFETIME).unwrap(),
idgen_retries,
);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
// Doesn't matter as long as we perform DAD.
dad_transmits: Some(NonZeroU16::new(1)),
slaac_config: Some(slaac_config),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
receive_prefix_update(
&mut ctx,
&device,
router_ip,
subnet,
MAX_PREFERRED_LIFETIME.as_secs() as u32,
MAX_VALID_LIFETIME.as_secs() as u32,
);
let match_temporary_address = |entry: &GlobalIpv6Addr<FakeInstant>| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
};
// The system should try several (1 initial + # retries) times to
// generate an address. In the loop below, each generated address is
// detected as a duplicate.
let attempted_addresses: Vec<_> = (0..=idgen_retries)
.map(|_| {
// An address should be selected. This must be checked using DAD
// against other hosts on the network.
let addr_entry =
get_matching_slaac_address_entry(&ctx, &device, match_temporary_address)
.unwrap();
assert!(!addr_entry.flags.assigned);
// A response is received to the DAD request indicating that it
// is a duplicate.
receive_neighbor_advertisement_for_duplicate_address(
&mut ctx,
&device,
addr_entry.addr_sub().addr(),
);
// The address should be unassigned from the device.
assert_eq!(get_slaac_address_entry(&ctx, &device, *addr_entry.addr_sub()), None);
*addr_entry.addr_sub()
})
.collect();
// After the last failed try, the system should have given up, and there
// should be no temporary address for the subnet.
assert_eq!(get_matching_slaac_address_entry(&ctx, &device, match_temporary_address), None);
// All the attempted addresses should be unique.
let unique_addresses = attempted_addresses.iter().collect::<HashSet<_>>();
assert_eq!(
unique_addresses.len(),
(1 + idgen_retries).into(),
"not all addresses are unique: {attempted_addresses:?}"
);
}
#[test]
fn test_host_temporary_slaac_deprecate_before_regen() {
// Check that if there are multiple non-deprecated addresses in a subnet
// and the regen timer goes off, no new address is generated. This tests
// the following scenario:
//
// 1. At time T, an address A is created for a subnet whose preferred
// lifetime is PA. This results in a regen timer set at T + PA - R.
// 2. At time T + PA - R, a new address B is created for the same
// subnet when the regen timer for A expires, with a preferred
// lifetime of PB (PA != PB because of the desync values).
// 3. Before T + PA, an advertisement is received for the prefix with
// preferred lifetime X. Address A is now preferred until T + PA + X
// and regenerated at T + PA + X - R and address B is preferred until
// (T + PA - R) + PB + X.
//
// Since both addresses are preferred, we expect that when the regen
// timer for address A goes off, it is ignored since there is already
// another preferred address (namely B) for the subnet.
set_logger_for_test();
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
const MAX_PREFERRED_LIFETIME: Duration = Duration::from_secs(5000);
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
ctx.bindings_ctx.rng(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(MAX_PREFERRED_LIFETIME).unwrap(),
0,
);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(slaac_config),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
// The prefix updates contains a shorter preferred lifetime than
// the preferences allow.
let prefix_preferred_for: Duration = MAX_PREFERRED_LIFETIME * 2 / 3;
receive_prefix_update(
&mut ctx,
&device,
router_ip,
subnet,
prefix_preferred_for.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
let first_addr_entry = get_matching_slaac_address_entry(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.unwrap();
let regen_timer_id: TimerId<_> = SlaacTimerId::new_regenerate_temporary_slaac_address(
device.clone(),
*first_addr_entry.addr_sub(),
)
.into();
trace!("advancing to regen for first address");
assert_eq!(ctx.trigger_next_timer(), Some(regen_timer_id.clone()));
// The regeneration timer should cause a new address to be created in
// the same subnet.
assert_matches!(
get_matching_slaac_address_entry(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& entry.addr_sub() != first_addr_entry.addr_sub()
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
}),
Some(_)
);
// Now the router sends a new update that extends the preferred lifetime
// of addresses.
receive_prefix_update(
&mut ctx,
&device,
router_ip,
subnet,
prefix_preferred_for.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
let addresses = get_matching_slaac_address_entries(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.map(|entry| entry.addr_sub().addr())
.collect::<Vec<_>>();
for address in &addresses {
assert_matches!(
ctx.bindings_ctx.scheduled_instant(SlaacTimerId::new_deprecate_slaac_address(
device.clone(),
*address,
)),
Some(deprecate_at) => {
let preferred_for = deprecate_at - ctx.bindings_ctx.now();
assert!(preferred_for <= prefix_preferred_for, "{:?} <= {:?}", preferred_for, prefix_preferred_for);
}
);
}
trace!("advancing to new regen for first address");
// Running the context forward until the first address is again eligible
// for regeneration doesn't result in a new address being created.
assert_eq!(ctx.trigger_next_timer(), Some(regen_timer_id));
assert_eq!(
get_matching_slaac_address_entries(&ctx, &device, |entry| entry.addr_sub().subnet()
== subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
})
.map(|entry| entry.addr_sub().addr())
.collect::<HashSet<_>>(),
addresses.iter().cloned().collect()
);
trace!("advancing to deprecation for first address");
// If we continue on until the first address is deprecated, we still
// shouldn't regenerate since the second address is active.
assert_eq!(
ctx.trigger_next_timer(),
Some(
SlaacTimerId::new_deprecate_slaac_address(
device.clone(),
first_addr_entry.addr_sub().addr()
)
.into()
)
);
let remaining_addresses = addresses
.into_iter()
.filter(|addr| addr != &first_addr_entry.addr_sub().addr())
.collect::<HashSet<_>>();
assert_eq!(
get_matching_slaac_address_entries(&ctx, &device, |entry| entry.addr_sub().subnet()
== subnet
&& !entry.flags.deprecated
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
})
.map(|entry| entry.addr_sub().addr())
.collect::<HashSet<_>>(),
remaining_addresses
);
// Clean up device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
}
#[test]
fn test_host_temporary_slaac_config_update_skips_regen() {
// If the NDP configuration gets updated such that the target regen time
// for an address is moved earlier than the current time, the address
// should be regenerated immediately.
set_logger_for_test();
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let eth_device_id =
ctx.core_api().device::<EthernetLinkDevice>().add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
);
let device = eth_device_id.clone().into();
// No DAD for the auto-generated link-local address.
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
dad_transmits: Some(None),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let router_mac = config.remote_mac;
let router_ip = router_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let subnet = Subnet::new(prefix, prefix_length).unwrap();
const MAX_VALID_LIFETIME: Duration = Duration::from_secs(15000);
let max_preferred_lifetime = Duration::from_secs(5000);
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
ctx.bindings_ctx.rng(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(max_preferred_lifetime).unwrap(),
1,
);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
// Perform DAD for later addresses.
dad_transmits: Some(NonZeroU16::new(1)),
slaac_config: Some(slaac_config),
..Default::default()
},
)
.unwrap();
let Ctx { core_ctx, bindings_ctx } = &mut ctx;
// Set a large value for the retransmit period. This forces
// REGEN_ADVANCE to be large, which increases the window between when an
// address is regenerated and when it becomes deprecated.
Ipv6DeviceHandler::set_discovered_retrans_timer(
&mut core_ctx.context(),
bindings_ctx,
&device,
NonZeroDuration::new(max_preferred_lifetime / 4).unwrap(),
);
receive_prefix_update(
&mut ctx,
&device,
router_ip,
subnet,
max_preferred_lifetime.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
let first_addr_entry = get_matching_slaac_address_entry(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.unwrap();
let regen_at = ctx
.bindings_ctx
.scheduled_instant(SlaacTimerId::new_regenerate_temporary_slaac_address(
device.clone(),
*first_addr_entry.addr_sub(),
))
.unwrap();
let before_regen = regen_at - Duration::from_secs(10);
// The only events that run before regen should be the DAD timers for
// the static and temporary address that were created earlier.
let dad_timer_ids = get_matching_slaac_address_entries(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
})
.map(|entry| dad_timer_id(eth_device_id.clone(), entry.addr_sub().addr()))
.collect::<Vec<_>>();
ctx.trigger_timers_until_and_expect_unordered(before_regen, dad_timer_ids);
let preferred_until = ctx
.bindings_ctx
.scheduled_instant(SlaacTimerId::new_deprecate_slaac_address(
device.clone(),
first_addr_entry.addr_sub().addr(),
))
.unwrap();
let max_preferred_lifetime = max_preferred_lifetime * 4 / 5;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
ctx.bindings_ctx.rng(),
NonZeroDuration::new(MAX_VALID_LIFETIME).unwrap(),
NonZeroDuration::new(max_preferred_lifetime).unwrap(),
1,
);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(slaac_config),
..Default::default()
},
)
.unwrap();
// Receiving this update should result in requiring a regen time that is
// before the current time. The address should be regenerated
// immediately.
let prefix_preferred_for = preferred_until - ctx.bindings_ctx.now();
receive_prefix_update(
&mut ctx,
&device,
router_ip,
subnet,
prefix_preferred_for.as_secs().try_into().unwrap(),
MAX_VALID_LIFETIME.as_secs().try_into().unwrap(),
);
// The regeneration is still handled by timer, so handle any pending
// events.
assert_eq!(
ctx.trigger_timers_for(Duration::ZERO),
vec![SlaacTimerId::new_regenerate_temporary_slaac_address(
device.clone(),
*first_addr_entry.addr_sub()
)
.into()]
);
let addresses = get_matching_slaac_address_entries(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.map(|entry| entry.addr_sub().addr())
.collect::<HashSet<_>>();
assert!(addresses.contains(&first_addr_entry.addr_sub().addr()));
assert_eq!(addresses.len(), 2);
// Clean up device references.
core::mem::drop(device);
ctx.core_api().device().remove_device(eth_device_id).into_removed();
}
#[test]
fn test_host_temporary_slaac_lifetime_updates_respect_max() {
// Make sure that the preferred and valid lifetimes of the NDP
// configuration are respected.
let src_mac = Ipv6::FAKE_CONFIG.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let subnet = subnet_v6!("0102:0304:0506:0708::/64");
let (mut ctx, device, config) = initialize_with_temporary_addresses_enabled();
let now = ctx.bindings_ctx.now();
let start = now;
let temporary_address_config = config.temporary_address_configuration.unwrap();
let max_valid_lifetime = temporary_address_config.temp_valid_lifetime;
let max_valid_until = now.checked_add(max_valid_lifetime.get()).unwrap();
let max_preferred_lifetime = temporary_address_config.temp_preferred_lifetime;
let max_preferred_until = now.checked_add(max_preferred_lifetime.get()).unwrap();
let secret_key = temporary_address_config.secret_key;
let interface_identifier = generate_opaque_interface_identifier(
subnet,
&Ipv6::FAKE_CONFIG.local_mac.to_eui64()[..],
[],
// Clone the RNG so we can see what the next value (which will be
// used to generate the temporary address) will be.
OpaqueIidNonce::Random(ctx.bindings_ctx.rng().deep_clone().next_u64()),
&secret_key,
);
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&interface_identifier.to_be_bytes()[..8]);
let expected_addr =
NonMappedAddr::new(UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap()).unwrap();
let expected_addr_sub = AddrSubnet::from_witness(expected_addr, subnet.prefix()).unwrap();
// Send an update with lifetimes that are smaller than the ones specified in the preferences.
let valid_lifetime = 2000;
let preferred_lifetime = 1500;
assert!(u64::from(valid_lifetime) < max_valid_lifetime.get().as_secs());
assert!(u64::from(preferred_lifetime) < max_preferred_lifetime.get().as_secs());
receive_prefix_update(
&mut ctx,
&device,
src_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let entry = get_slaac_address_entry(&ctx, &device, expected_addr_sub).unwrap();
let expected_valid_until =
now.checked_add(Duration::from_secs(valid_lifetime.into())).unwrap();
let expected_preferred_until =
now.checked_add(Duration::from_secs(preferred_lifetime.into())).unwrap();
assert!(
expected_valid_until < max_valid_until,
"expected {:?} < {:?}",
expected_valid_until,
max_valid_until
);
assert!(expected_preferred_until < max_preferred_until);
assert_slaac_lifetimes_enforced(
&ctx.bindings_ctx,
&device,
entry,
expected_valid_until,
expected_preferred_until,
);
// After some time passes, another update is received with the same lifetimes for the
// prefix. Per RFC 8981 Section 3.4.1, the lifetimes for the address should obey the
// overall constraints expressed in the preferences.
assert_eq!(ctx.trigger_timers_for(Duration::from_secs(1000)), []);
let now = ctx.bindings_ctx.now();
let expected_valid_until =
now.checked_add(Duration::from_secs(valid_lifetime.into())).unwrap();
let expected_preferred_until =
now.checked_add(Duration::from_secs(preferred_lifetime.into())).unwrap();
// The preferred lifetime advertised by the router is now past the max allowed by
// the NDP configuration.
assert!(expected_preferred_until > max_preferred_until);
receive_prefix_update(
&mut ctx,
&device,
src_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let entry = get_matching_slaac_address_entry(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.unwrap();
let desync_factor = match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(TemporarySlaacConfig {
desync_factor,
creation_time: _,
valid_until: _,
dad_counter: _,
})) => desync_factor,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => {
unreachable!("temporary address")
}
Ipv6AddrConfig::Manual(_manual_config) => unreachable!("temporary slaac address"),
};
assert_slaac_lifetimes_enforced(
&ctx.bindings_ctx,
&device,
entry,
expected_valid_until,
max_preferred_until - desync_factor,
);
// Update the max allowed lifetime in the NDP configuration. This won't take effect until
// the next router advertisement is reeived.
let max_valid_lifetime = max_preferred_lifetime;
let idgen_retries = 3;
let mut slaac_config = SlaacConfiguration::default();
enable_temporary_addresses(
&mut slaac_config,
ctx.bindings_ctx.rng(),
max_valid_lifetime,
max_preferred_lifetime,
idgen_retries,
);
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(slaac_config),
..Default::default()
},
)
.unwrap();
// The new valid time is measured from the time at which the address was created (`start`),
// not the current time (`now`). That means the max valid lifetime takes precedence over
// the router's advertised valid lifetime.
let max_valid_until = start.checked_add(max_valid_lifetime.get()).unwrap();
assert!(expected_valid_until > max_valid_until);
receive_prefix_update(
&mut ctx,
&device,
src_ip,
subnet,
preferred_lifetime,
valid_lifetime,
);
let entry = get_matching_slaac_address_entry(&ctx, &device, |entry| {
entry.addr_sub().subnet() == subnet
&& match entry.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}
})
.unwrap();
assert_slaac_lifetimes_enforced(
&ctx.bindings_ctx,
&device,
entry,
max_valid_until,
max_preferred_until - desync_factor,
);
// Clean up device references.
ctx.core_api().device().remove_device(device.unwrap_ethernet()).into_removed();
}
#[test]
fn test_remove_stable_slaac_address() {
let config = Ipv6::FAKE_CONFIG;
let mut ctx = crate::testutil::FakeCtx::default();
let device = ctx
.core_api()
.device::<EthernetLinkDevice>()
.add_device_with_default_state(
EthernetCreationProperties {
mac: config.local_mac,
max_frame_size: IPV6_MIN_IMPLIED_MAX_FRAME_SIZE,
},
DEFAULT_INTERFACE_METRIC,
)
.into();
let _: Ipv6DeviceConfigurationUpdate = ctx
.core_api()
.device_ip::<Ipv6>()
.update_configuration(
&device,
Ipv6DeviceConfigurationUpdate {
slaac_config: Some(SlaacConfiguration {
enable_stable_addresses: true,
..Default::default()
}),
ip_config: IpDeviceConfigurationUpdate {
ip_enabled: Some(true),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let src_mac = config.remote_mac;
let src_ip = src_mac.to_ipv6_link_local().addr().get();
let prefix = Ipv6Addr::from([1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0]);
let prefix_length = 64;
let mut expected_addr = [1, 2, 3, 4, 5, 6, 7, 8, 0, 0, 0, 0, 0, 0, 0, 0];
expected_addr[8..].copy_from_slice(&config.local_mac.to_eui64()[..]);
let expected_addr =
NonMappedAddr::new(UnicastAddr::new(Ipv6Addr::from(expected_addr)).unwrap()).unwrap();
// Receive a new RA with new prefix (autonomous).
//
// Should get a new IP.
const VALID_LIFETIME_SECS: u32 = 10000;
const PREFERRED_LIFETIME_SECS: u32 = 9000;
let icmpv6_packet_buf = slaac_packet_buf(
src_ip,
Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
prefix,
prefix_length,
false,
true,
VALID_LIFETIME_SECS,
PREFERRED_LIFETIME_SECS,
);
ctx.test_api().receive_ip_packet::<Ipv6, _>(
&device,
Some(FrameDestination::Multicast),
icmpv6_packet_buf,
);
// Should have gotten a new IP.
let now = ctx.bindings_ctx.now();
let valid_until = now + Duration::from_secs(VALID_LIFETIME_SECS.into());
let expected_address_entry = GlobalIpv6Addr {
addr_sub: AddrSubnet::<Ipv6Addr, _>::new(expected_addr.into(), prefix_length).unwrap(),
config: Ipv6AddrConfig::Slaac(SlaacConfig::Static {
valid_until: Lifetime::Finite(valid_until),
}),
flags: Ipv6AddressFlags { deprecated: false, assigned: true },
};
assert_eq!(get_global_ipv6_addrs(&ctx, &device), [expected_address_entry]);
// Make sure deprecate and invalidation timers are set.
ctx.bindings_ctx.timer_ctx().assert_some_timers_installed([
(
SlaacTimerId::new_deprecate_slaac_address(device.clone(), expected_addr).into(),
now + Duration::from_secs(PREFERRED_LIFETIME_SECS.into()),
),
(
SlaacTimerId::new_invalidate_slaac_address(device.clone(), expected_addr).into(),
valid_until,
),
]);
// Deleting the address should cancel its SLAAC timers.
ctx.core_api()
.device_ip::<Ipv6>()
.del_ip_addr(&device, expected_addr.into_specified())
.unwrap();
assert_empty(get_global_ipv6_addrs(&ctx, &device));
ctx.bindings_ctx.timer_ctx().assert_no_timers_installed();
}
#[test]
fn test_remove_temporary_slaac_address() {
// We use the infinite lifetime so that the stable address does not have
// any timers as it is valid and preferred forever. As a result, we will
// only observe timers for temporary addresses.
let (mut ctx, device, expected_addr) =
test_host_generate_temporary_slaac_address(INFINITE_LIFETIME, INFINITE_LIFETIME);
// Deleting the address should cancel its SLAAC timers.
ctx.core_api()
.device_ip::<Ipv6>()
.del_ip_addr(&device, expected_addr.into_specified())
.unwrap();
assert_empty(get_global_ipv6_addrs(&ctx, &device).into_iter().filter(|e| match e.config {
Ipv6AddrConfig::Slaac(SlaacConfig::Temporary(_)) => true,
Ipv6AddrConfig::Slaac(SlaacConfig::Static { valid_until: _ }) => false,
Ipv6AddrConfig::Manual(_manual_config) => false,
}));
ctx.bindings_ctx.timer_ctx().assert_no_timers_installed();
}
}