blob: 609fe7f55ecd53229097b70413fb0f7b572c87d9 [file] [log] [blame]
// Copyright 2020 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.
//! Core DHCPv6 client state transitions.
use assert_matches::assert_matches;
use derivative::Derivative;
use net_types::ip::{Ipv6Addr, Subnet};
use num::rational::Ratio;
use num::CheckedMul;
use packet::serialize::InnerPacketBuilder;
use packet_formats_dhcp::v6;
use rand::{thread_rng, Rng};
use std::cmp::{Eq, Ord, PartialEq, PartialOrd};
use std::collections::hash_map::Entry;
use std::collections::{BinaryHeap, HashMap, HashSet};
use std::fmt::Debug;
use std::hash::Hash;
use std::marker::PhantomData;
use std::time::Duration;
use tracing::{debug, info, warn};
use zerocopy::ByteSlice;
use crate::{ClientDuid, Instant, InstantExt as _};
/// Initial Information-request timeout `INF_TIMEOUT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const INITIAL_INFO_REQ_TIMEOUT: Duration = Duration::from_secs(1);
/// Max Information-request timeout `INF_MAX_RT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const MAX_INFO_REQ_TIMEOUT: Duration = Duration::from_secs(3600);
/// Default information refresh time from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const IRT_DEFAULT: Duration = Duration::from_secs(86400);
/// The max duration in seconds `std::time::Duration` supports.
///
/// NOTE: it is possible for `Duration` to be bigger by filling in the nanos
/// field, but this value is good enough for the purpose of this crate.
const MAX_DURATION: Duration = Duration::from_secs(u64::MAX);
/// Initial Solicit timeout `SOL_TIMEOUT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const INITIAL_SOLICIT_TIMEOUT: Duration = Duration::from_secs(1);
/// Max Solicit timeout `SOL_MAX_RT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const MAX_SOLICIT_TIMEOUT: Duration = Duration::from_secs(3600);
/// The valid range for `SOL_MAX_RT`, as defined in [RFC 8415, Section 21.24].
///
/// [RFC 8415, Section 21.24](https://datatracker.ietf.org/doc/html/rfc8415#section-21.24)
const VALID_MAX_SOLICIT_TIMEOUT_RANGE: std::ops::RangeInclusive<u32> = 60..=86400;
/// The maximum [Preference option] value that can be present in an advertise,
/// as described in [RFC 8415, Section 18.2.1].
///
/// [RFC 8415, Section 18.2.1]: https://datatracker.ietf.org/doc/html/rfc8415#section-18.2.1
/// [Preference option]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.8
const ADVERTISE_MAX_PREFERENCE: u8 = std::u8::MAX;
/// Denominator used for transforming the elapsed time from milliseconds to
/// hundredths of a second.
///
/// [RFC 8415, Section 21.9]: https://tools.ietf.org/html/rfc8415#section-21.9
const ELAPSED_TIME_DENOMINATOR: u128 = 10;
/// The minimum value for the randomization factor `RAND` used in calculating
/// retransmission timeout, as specified in [RFC 8415, Section 15].
///
/// [RFC 8415, Section 15](https://datatracker.ietf.org/doc/html/rfc8415#section-15)
const RANDOMIZATION_FACTOR_MIN: f64 = -0.1;
/// The maximum value for the randomization factor `RAND` used in calculating
/// retransmission timeout, as specified in [RFC 8415, Section 15].
///
/// [RFC 8415, Section 15](https://datatracker.ietf.org/doc/html/rfc8415#section-15)
const RANDOMIZATION_FACTOR_MAX: f64 = 0.1;
/// Initial Request timeout `REQ_TIMEOUT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const INITIAL_REQUEST_TIMEOUT: Duration = Duration::from_secs(1);
/// Max Request timeout `REQ_MAX_RT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const MAX_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
/// Max Request retry attempts `REQ_MAX_RC` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const REQUEST_MAX_RC: u8 = 10;
/// The ratio used for calculating T1 based on the shortest preferred lifetime,
/// when the T1 value received from the server is 0.
///
/// When T1 is set to 0 by the server, the value is left to the discretion of
/// the client, as described in [RFC 8415, Section 14.2]. The client computes
/// T1 using the recommended ratio from [RFC 8415, Section 21.4]:
/// T1 = shortest lifetime * 0.5
///
/// [RFC 8415, Section 14.2]: https://datatracker.ietf.org/doc/html/rfc8415#section-14.2
/// [RFC 8415, Section 21.4]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.4
const T1_MIN_LIFETIME_RATIO: Ratio<u32> = Ratio::new_raw(1, 2);
/// The ratio used for calculating T2 based on T1, when the T2 value received
/// from the server is 0.
///
/// When T2 is set to 0 by the server, the value is left to the discretion of
/// the client, as described in [RFC 8415, Section 14.2]. The client computes
/// T2 using the recommended ratios from [RFC 8415, Section 21.4]:
/// T2 = T1 * 0.8 / 0.5
///
/// [RFC 8415, Section 14.2]: https://datatracker.ietf.org/doc/html/rfc8415#section-14.2
/// [RFC 8415, Section 21.4]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.4
const T2_T1_RATIO: Ratio<u32> = Ratio::new_raw(8, 5);
/// Initial Renew timeout `REN_TIMEOUT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const INITIAL_RENEW_TIMEOUT: Duration = Duration::from_secs(10);
/// Max Renew timeout `REN_MAX_RT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const MAX_RENEW_TIMEOUT: Duration = Duration::from_secs(600);
/// Initial Rebind timeout `REB_TIMEOUT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const INITIAL_REBIND_TIMEOUT: Duration = Duration::from_secs(10);
/// Max Rebind timeout `REB_MAX_RT` from [RFC 8415, Section 7.6].
///
/// [RFC 8415, Section 7.6]: https://tools.ietf.org/html/rfc8415#section-7.6
const MAX_REBIND_TIMEOUT: Duration = Duration::from_secs(600);
const IA_NA_NAME: &'static str = "IA_NA";
const IA_PD_NAME: &'static str = "IA_PD";
/// Calculates retransmission timeout based on formulas defined in [RFC 8415, Section 15].
/// A zero `prev_retrans_timeout` indicates this is the first transmission, so
/// `initial_retrans_timeout` will be used.
///
/// Relevant formulas from [RFC 8415, Section 15]:
///
/// ```text
/// RT Retransmission timeout
/// IRT Initial retransmission time
/// MRT Maximum retransmission time
/// RAND Randomization factor
///
/// RT for the first message transmission is based on IRT:
///
/// RT = IRT + RAND*IRT
///
/// RT for each subsequent message transmission is based on the previous value of RT:
///
/// RT = 2*RTprev + RAND*RTprev
///
/// MRT specifies an upper bound on the value of RT (disregarding the randomization added by
/// the use of RAND). If MRT has a value of 0, there is no upper limit on the value of RT.
/// Otherwise:
///
/// if (RT > MRT)
/// RT = MRT + RAND*MRT
/// ```
///
/// [RFC 8415, Section 15]: https://tools.ietf.org/html/rfc8415#section-15
fn retransmission_timeout<R: Rng>(
prev_retrans_timeout: Duration,
initial_retrans_timeout: Duration,
max_retrans_timeout: Duration,
rng: &mut R,
) -> Duration {
let rand = rng.gen_range(RANDOMIZATION_FACTOR_MIN..RANDOMIZATION_FACTOR_MAX);
let next_rt = if prev_retrans_timeout.as_nanos() == 0 {
let irt = initial_retrans_timeout.as_secs_f64();
irt + rand * irt
} else {
let rt = prev_retrans_timeout.as_secs_f64();
2. * rt + rand * rt
};
if max_retrans_timeout.as_nanos() == 0 || next_rt < max_retrans_timeout.as_secs_f64() {
clipped_duration(next_rt)
} else {
let mrt = max_retrans_timeout.as_secs_f64();
clipped_duration(mrt + rand * mrt)
}
}
/// Clips overflow and returns a duration using the input seconds.
fn clipped_duration(secs: f64) -> Duration {
if secs <= 0. {
Duration::from_nanos(0)
} else if secs >= MAX_DURATION.as_secs_f64() {
MAX_DURATION
} else {
Duration::from_secs_f64(secs)
}
}
/// Creates a transaction ID used by the client to match outgoing messages with
/// server replies, as defined in [RFC 8415, Section 16.1].
///
/// [RFC 8415, Section 16.1]: https://tools.ietf.org/html/rfc8415#section-16.1
pub fn transaction_id() -> [u8; 3] {
let mut id = [0u8; 3];
thread_rng().fill(&mut id[..]);
id
}
/// Identifies what event should be triggered when a timer fires.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum ClientTimerType {
Retransmission,
Refresh,
Renew,
Rebind,
RestartServerDiscovery,
}
/// Possible actions that need to be taken for a state transition to happen successfully.
#[derive(Debug, PartialEq, Clone)]
pub enum Action<I> {
SendMessage(Vec<u8>),
/// Schedules a timer to fire at a specified time instant.
///
/// If the timer is already scheduled to fire at some time, this action
/// will result in the timer being rescheduled to the new time.
ScheduleTimer(ClientTimerType, I),
/// Cancels a timer.
///
/// If the timer is not scheduled, this action should effectively be a
/// no-op.
CancelTimer(ClientTimerType),
UpdateDnsServers(Vec<Ipv6Addr>),
/// The updates for IA_NA bindings.
///
/// Only changes to an existing bindings is conveyed through this
/// variant. That is, an update missing for an (`IAID`, `Ipv6Addr`) means
/// no new change for the address.
///
/// Updates include the preferred/valid lifetimes for an address and it
/// is up to the action-taker to deprecate/invalidate addresses after the
/// appropriate lifetimes. That is, there will be no dedicated update
/// for preferred/valid lifetime expiration.
IaNaUpdates(HashMap<v6::IAID, HashMap<Ipv6Addr, IaValueUpdateKind>>),
/// The updates for IA_PD bindings.
///
/// Only changes to an existing bindings is conveyed through this
/// variant. That is, an update missing for an (`IAID`, `Subnet<Ipv6Addr>`)
/// means no new change for the prefix.
///
/// Updates include the preferred/valid lifetimes for a prefix and it
/// is up to the action-taker to deprecate/invalidate prefixes after the
/// appropriate lifetimes. That is, there will be no dedicated update
/// for preferred/valid lifetime expiration.
IaPdUpdates(HashMap<v6::IAID, HashMap<Subnet<Ipv6Addr>, IaValueUpdateKind>>),
}
pub type Actions<I> = Vec<Action<I>>;
/// Holds data and provides methods for handling state transitions from information requesting
/// state.
#[derive(Debug)]
struct InformationRequesting<I> {
retrans_timeout: Duration,
_marker: PhantomData<I>,
}
impl<I: Instant> InformationRequesting<I> {
/// Starts in information requesting state following [RFC 8415, Section 18.2.6].
///
/// [RFC 8415, Section 18.2.6]: https://tools.ietf.org/html/rfc8415#section-18.2.6
fn start<R: Rng>(
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let info_req = Self { retrans_timeout: Default::default(), _marker: Default::default() };
info_req.send_and_schedule_retransmission(transaction_id, options_to_request, rng, now)
}
/// Calculates timeout for retransmitting information requests using parameters specified in
/// [RFC 8415, Section 18.2.6].
///
/// [RFC 8415, Section 18.2.6]: https://tools.ietf.org/html/rfc8415#section-18.2.6
fn retransmission_timeout<R: Rng>(&self, rng: &mut R) -> Duration {
let Self { retrans_timeout, _marker } = self;
retransmission_timeout(
*retrans_timeout,
INITIAL_INFO_REQ_TIMEOUT,
MAX_INFO_REQ_TIMEOUT,
rng,
)
}
/// A helper function that returns a transition to stay in `InformationRequesting`,
/// with actions to send an information request and schedules retransmission.
fn send_and_schedule_retransmission<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let options_array = [v6::DhcpOption::Oro(options_to_request)];
let options = if options_to_request.is_empty() { &[][..] } else { &options_array[..] };
let builder =
v6::MessageBuilder::new(v6::MessageType::InformationRequest, transaction_id, options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let retrans_timeout = self.retransmission_timeout(rng);
Transition {
state: ClientState::InformationRequesting(InformationRequesting {
retrans_timeout,
_marker: Default::default(),
}),
actions: vec![
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, now.add(retrans_timeout)),
],
transaction_id: None,
}
}
/// Retransmits information request.
fn retransmission_timer_expired<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
self.send_and_schedule_retransmission(transaction_id, options_to_request, rng, now)
}
/// Handles reply to information requests based on [RFC 8415, Section 18.2.10.4].
///
/// [RFC 8415, Section 18.2.10.4]: https://tools.ietf.org/html/rfc8415#section-18.2.10.4
fn reply_message_received<B: ByteSlice>(
self,
msg: v6::Message<'_, B>,
now: I,
) -> Transition<I> {
// Note that although RFC 8415 states that SOL_MAX_RT must be handled,
// we never send Solicit messages when running in stateless mode, so
// there is no point in storing or doing anything with it.
let ProcessedOptions { server_id, solicit_max_rt_opt: _, result } = match process_options(
&msg,
ExchangeType::ReplyToInformationRequest,
None,
&NoIaRequested,
&NoIaRequested,
) {
Ok(processed_options) => processed_options,
Err(e) => {
warn!("ignoring Reply to Information-Request: {}", e);
return Transition {
state: ClientState::InformationRequesting(self),
actions: Vec::new(),
transaction_id: None,
};
}
};
let Options {
success_status_message,
next_contact_time,
preference: _,
non_temporary_addresses: _,
delegated_prefixes: _,
dns_servers,
} = match result {
Ok(options) => options,
Err(e) => {
warn!(
"Reply to Information-Request from server {:?} error status code: {}",
server_id, e
);
return Transition {
state: ClientState::InformationRequesting(self),
actions: Vec::new(),
transaction_id: None,
};
}
};
// Per RFC 8415 section 21.23:
//
// If the Reply to an Information-request message does not contain this
// option, the client MUST behave as if the option with the value
// IRT_DEFAULT was provided.
let information_refresh_time = assert_matches!(
next_contact_time,
NextContactTime::InformationRefreshTime(option) => option
)
.map(|t| Duration::from_secs(t.into()))
.unwrap_or(IRT_DEFAULT);
if let Some(success_status_message) = success_status_message {
if !success_status_message.is_empty() {
info!(
"Reply to Information-Request from server {:?} \
contains success status code message: {}",
server_id, success_status_message,
);
}
}
let actions = [
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Refresh, now.add(information_refresh_time)),
]
.into_iter()
.chain(dns_servers.clone().map(|server_addrs| Action::UpdateDnsServers(server_addrs)))
.collect::<Vec<_>>();
Transition {
state: ClientState::InformationReceived(InformationReceived {
dns_servers: dns_servers.unwrap_or(Vec::new()),
_marker: Default::default(),
}),
actions,
transaction_id: None,
}
}
}
/// Provides methods for handling state transitions from information received state.
#[derive(Debug)]
struct InformationReceived<I> {
/// Stores the DNS servers received from the reply.
dns_servers: Vec<Ipv6Addr>,
_marker: PhantomData<I>,
}
impl<I: Instant> InformationReceived<I> {
/// Refreshes information by starting another round of information request.
fn refresh_timer_expired<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
InformationRequesting::start(transaction_id, options_to_request, rng, now)
}
}
enum IaKind {
Address,
Prefix,
}
trait IaValue: Copy + Clone + Debug + PartialEq + Eq + Hash {
const KIND: IaKind;
}
impl IaValue for Ipv6Addr {
const KIND: IaKind = IaKind::Address;
}
impl IaValue for Subnet<Ipv6Addr> {
const KIND: IaKind = IaKind::Prefix;
}
// Holds the information received in an Advertise message.
#[derive(Debug, Clone)]
struct AdvertiseMessage<I> {
server_id: Vec<u8>,
/// The advertised non-temporary addresses.
///
/// Each IA has at least one address.
non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>>,
/// The advertised delegated prefixes.
///
/// Each IA has at least one prefix.
delegated_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
dns_servers: Vec<Ipv6Addr>,
preference: u8,
receive_time: I,
preferred_non_temporary_addresses_count: usize,
preferred_delegated_prefixes_count: usize,
}
impl<I> AdvertiseMessage<I> {
fn has_ias(&self) -> bool {
let Self {
server_id: _,
non_temporary_addresses,
delegated_prefixes,
dns_servers: _,
preference: _,
receive_time: _,
preferred_non_temporary_addresses_count: _,
preferred_delegated_prefixes_count: _,
} = self;
// We know we are performing stateful DHCPv6 since we are performing
// Server Discovery/Selection as stateless DHCPv6 does not use Advertise
// messages.
//
// We consider an Advertisement acceptable if at least one requested IA
// is available.
!(non_temporary_addresses.is_empty() && delegated_prefixes.is_empty())
}
}
// Orders Advertise by address count, then preference, dns servers count, and
// earliest receive time. This ordering gives precedence to higher address
// count over preference, to maximise the number of assigned addresses, as
// described in RFC 8415, section 18.2.9:
//
// Those Advertise messages with the highest server preference value SHOULD
// be preferred over all other Advertise messages. The client MAY choose a
// less preferred server if that server has a better set of advertised
// parameters, such as the available set of IAs.
impl<I: Instant> Ord for AdvertiseMessage<I> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct Candidate<I> {
// First prefer the advertisement with at least one IA_NA.
has_ia_na: bool,
// Then prefer the advertisement with at least one IA_PD.
has_ia_pd: bool,
// Then prefer the advertisement with the most IA_NAs.
ia_na_count: usize,
// Then prefer the advertisement with the most IA_PDs.
ia_pd_count: usize,
// Then prefer the advertisement with the most addresses in IA_NAs
// that match the provided hint(s).
preferred_ia_na_address_count: usize,
// Then prefer the advertisement with the most prefixes IA_PDs that
// match the provided hint(s).
preferred_ia_pd_prefix_count: usize,
// Then prefer the advertisement with the highest preference value.
server_preference: u8,
// Then prefer the advertisement with the most number of DNS
// servers.
dns_server_count: usize,
// Then prefer the advertisement received first.
other_candidate_rcv_time: I,
}
impl<I: Instant> Candidate<I> {
fn from_advertisements(
candidate: &AdvertiseMessage<I>,
other_candidate: &AdvertiseMessage<I>,
) -> Self {
let AdvertiseMessage {
server_id: _,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
preference,
receive_time: _,
preferred_non_temporary_addresses_count,
preferred_delegated_prefixes_count,
} = candidate;
let AdvertiseMessage {
server_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
dns_servers: _,
preference: _,
receive_time: other_receive_time,
preferred_non_temporary_addresses_count: _,
preferred_delegated_prefixes_count: _,
} = other_candidate;
Self {
has_ia_na: !non_temporary_addresses.is_empty(),
has_ia_pd: !delegated_prefixes.is_empty(),
ia_na_count: non_temporary_addresses.len(),
ia_pd_count: delegated_prefixes.len(),
preferred_ia_na_address_count: *preferred_non_temporary_addresses_count,
preferred_ia_pd_prefix_count: *preferred_delegated_prefixes_count,
server_preference: *preference,
dns_server_count: dns_servers.len(),
other_candidate_rcv_time: *other_receive_time,
}
}
}
Candidate::from_advertisements(self, other)
.cmp(&Candidate::from_advertisements(other, self))
}
}
impl<I: Instant> PartialOrd for AdvertiseMessage<I> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<I: Instant> PartialEq for AdvertiseMessage<I> {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == std::cmp::Ordering::Equal
}
}
impl<I: Instant> Eq for AdvertiseMessage<I> {}
// Returns a count of entries in where the value matches the configured value
// with the same IAID.
fn compute_preferred_ia_count<V: IaValue>(
got: &HashMap<v6::IAID, HashSet<V>>,
configured: &HashMap<v6::IAID, HashSet<V>>,
) -> usize {
got.iter()
.map(|(iaid, got_values)| {
configured
.get(iaid)
.map_or(0, |configured_values| got_values.intersection(configured_values).count())
})
.sum()
}
// Calculates the elapsed time since `start_time`, in centiseconds.
fn elapsed_time_in_centisecs<I: Instant>(start_time: I, now: I) -> u16 {
u16::try_from(
now.duration_since(start_time)
.as_millis()
.checked_div(ELAPSED_TIME_DENOMINATOR)
.expect("division should succeed, denominator is non-zero"),
)
.unwrap_or(u16::MAX)
}
// Returns the common value in `values` if all the values are equal, or None
// otherwise.
fn get_common_value(values: &Vec<u32>) -> Option<Duration> {
if !values.is_empty() && values.iter().all(|value| *value == values[0]) {
return Some(Duration::from_secs(values[0].into()));
}
None
}
#[derive(thiserror::Error, Copy, Clone, Debug)]
#[cfg_attr(test, derive(PartialEq))]
enum LifetimesError {
#[error("valid lifetime is zero")]
ValidLifetimeZero,
#[error("preferred lifetime greater than valid lifetime: {0:?}")]
PreferredLifetimeGreaterThanValidLifetime(Lifetimes),
}
/// The valid and preferred lifetimes.
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Lifetimes {
pub preferred_lifetime: v6::TimeValue,
pub valid_lifetime: v6::NonZeroTimeValue,
}
#[derive(Debug)]
struct IaValueOption<V> {
value: V,
lifetimes: Result<Lifetimes, LifetimesError>,
}
#[derive(thiserror::Error, Debug)]
enum IaOptionError<V: IaValue> {
#[error("T1={t1:?} greater than T2={t2:?}")]
T1GreaterThanT2 { t1: v6::TimeValue, t2: v6::TimeValue },
#[error("status code error: {0}")]
StatusCode(#[from] StatusCodeError),
// TODO(https://fxbug.dev/42055437): Use an owned option type rather
// than a string of the debug representation of the invalid option.
#[error("invalid option: {0:?}")]
InvalidOption(String),
#[error("IA value={value:?} appeared twice with first={first_lifetimes:?} and second={second_lifetimes:?}")]
DuplicateIaValue {
value: V,
first_lifetimes: Result<Lifetimes, LifetimesError>,
second_lifetimes: Result<Lifetimes, LifetimesError>,
},
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
enum IaOption<V: IaValue> {
Success {
status_message: Option<String>,
t1: v6::TimeValue,
t2: v6::TimeValue,
ia_values: HashMap<V, Result<Lifetimes, LifetimesError>>,
},
Failure(ErrorStatusCode),
}
type IaNaOption = IaOption<Ipv6Addr>;
#[derive(thiserror::Error, Debug)]
enum StatusCodeError {
#[error("unknown status code {0}")]
InvalidStatusCode(u16),
#[error("duplicate Status Code option {0:?} and {1:?}")]
DuplicateStatusCode((v6::StatusCode, String), (v6::StatusCode, String)),
}
fn check_lifetimes(
valid_lifetime: v6::TimeValue,
preferred_lifetime: v6::TimeValue,
) -> Result<Lifetimes, LifetimesError> {
match valid_lifetime {
v6::TimeValue::Zero => Err(LifetimesError::ValidLifetimeZero),
vl @ v6::TimeValue::NonZero(valid_lifetime) => {
// Ignore IA {Address,Prefix} options with invalid preferred or
// valid lifetimes.
//
// Per RFC 8415 section 21.6,
//
// The client MUST discard any addresses for which the preferred
// lifetime is greater than the valid lifetime.
//
// Per RFC 8415 section 21.22,
//
// The client MUST discard any prefixes for which the preferred
// lifetime is greater than the valid lifetime.
if preferred_lifetime > vl {
Err(LifetimesError::PreferredLifetimeGreaterThanValidLifetime(Lifetimes {
preferred_lifetime,
valid_lifetime,
}))
} else {
Ok(Lifetimes { preferred_lifetime, valid_lifetime })
}
}
}
}
// TODO(https://fxbug.dev/42055684): Move this function and associated types
// into packet-formats-dhcp.
fn process_ia<
'a,
V: IaValue,
E: From<IaOptionError<V>> + Debug,
F: Fn(&v6::ParsedDhcpOption<'a>) -> Result<IaValueOption<V>, E>,
>(
t1: v6::TimeValue,
t2: v6::TimeValue,
options: impl Iterator<Item = v6::ParsedDhcpOption<'a>>,
check: F,
) -> Result<IaOption<V>, E> {
// Ignore IA_{NA,PD} options, with invalid T1/T2 values.
//
// Per RFC 8415, section 21.4:
//
// If a client receives an IA_NA with T1 greater than T2 and both T1
// and T2 are greater than 0, the client discards the IA_NA option
// and processes the remainder of the message as though the server
// had not included the invalid IA_NA option.
//
// Per RFC 8415, section 21.21:
//
// If a client receives an IA_PD with T1 greater than T2 and both T1 and
// T2 are greater than 0, the client discards the IA_PD option and
// processes the remainder of the message as though the server had not
// included the IA_PD option.
match (t1, t2) {
(v6::TimeValue::Zero, _) | (_, v6::TimeValue::Zero) => {}
(t1, t2) => {
if t1 > t2 {
return Err(IaOptionError::T1GreaterThanT2 { t1, t2 }.into());
}
}
}
let mut ia_values = HashMap::new();
let mut success_status_message = None;
for opt in options {
match opt {
v6::ParsedDhcpOption::StatusCode(code, msg) => {
let mut status_code = || {
let status_code = code.get().try_into().map_err(|e| match e {
v6::ParseError::InvalidStatusCode(code) => {
StatusCodeError::InvalidStatusCode(code)
}
e => unreachable!("unreachable status code parse error: {}", e),
})?;
if let Some(existing) = success_status_message.take() {
return Err(StatusCodeError::DuplicateStatusCode(
(v6::StatusCode::Success, existing),
(status_code, msg.to_string()),
));
}
Ok(status_code)
};
let status_code = status_code().map_err(IaOptionError::StatusCode)?;
match status_code.into_result() {
Ok(()) => {
success_status_message = Some(msg.to_string());
}
Err(error_status_code) => {
return Ok(IaOption::Failure(ErrorStatusCode(
error_status_code,
msg.to_string(),
)))
}
}
}
opt @ (v6::ParsedDhcpOption::IaAddr(_) | v6::ParsedDhcpOption::IaPrefix(_)) => {
let IaValueOption { value, lifetimes } = check(&opt)?;
if let Some(first_lifetimes) = ia_values.insert(value, lifetimes) {
return Err(IaOptionError::DuplicateIaValue {
value,
first_lifetimes,
second_lifetimes: lifetimes,
}
.into());
}
}
v6::ParsedDhcpOption::ClientId(_)
| v6::ParsedDhcpOption::ServerId(_)
| v6::ParsedDhcpOption::SolMaxRt(_)
| v6::ParsedDhcpOption::Preference(_)
| v6::ParsedDhcpOption::Iana(_)
| v6::ParsedDhcpOption::InformationRefreshTime(_)
| v6::ParsedDhcpOption::IaPd(_)
| v6::ParsedDhcpOption::Oro(_)
| v6::ParsedDhcpOption::ElapsedTime(_)
| v6::ParsedDhcpOption::DnsServers(_)
| v6::ParsedDhcpOption::DomainList(_) => {
return Err(IaOptionError::InvalidOption(format!("{:?}", opt)).into());
}
}
}
// Missing status code option means success per RFC 8415 section 7.5:
//
// If the Status Code option (see Section 21.13) does not appear
// in a message in which the option could appear, the status
// of the message is assumed to be Success.
Ok(IaOption::Success { status_message: success_status_message, t1, t2, ia_values })
}
// TODO(https://fxbug.dev/42055684): Move this function and associated types
// into packet-formats-dhcp.
fn process_ia_na(
ia_na_data: &v6::IanaData<&'_ [u8]>,
) -> Result<IaNaOption, IaOptionError<Ipv6Addr>> {
process_ia(ia_na_data.t1(), ia_na_data.t2(), ia_na_data.iter_options(), |opt| match opt {
v6::ParsedDhcpOption::IaAddr(ia_addr_data) => Ok(IaValueOption {
value: ia_addr_data.addr(),
lifetimes: check_lifetimes(
ia_addr_data.valid_lifetime(),
ia_addr_data.preferred_lifetime(),
),
}),
opt @ v6::ParsedDhcpOption::IaPrefix(_) => {
Err(IaOptionError::InvalidOption(format!("{:?}", opt)))
}
opt => unreachable!(
"other options should be handled before this fn is called; got = {:?}",
opt
),
})
}
#[derive(thiserror::Error, Debug)]
enum IaPdOptionError {
#[error("generic IA Option error: {0}")]
IaOptionError(#[from] IaOptionError<Subnet<Ipv6Addr>>),
#[error("invalid subnet")]
InvalidSubnet,
}
type IaPdOption = IaOption<Subnet<Ipv6Addr>>;
// TODO(https://fxbug.dev/42055684): Move this function and associated types
// into packet-formats-dhcp.
fn process_ia_pd(ia_pd_data: &v6::IaPdData<&'_ [u8]>) -> Result<IaPdOption, IaPdOptionError> {
process_ia(ia_pd_data.t1(), ia_pd_data.t2(), ia_pd_data.iter_options(), |opt| match opt {
v6::ParsedDhcpOption::IaPrefix(ia_prefix_data) => ia_prefix_data
.prefix()
.map_err(|_| IaPdOptionError::InvalidSubnet)
.map(|prefix| IaValueOption {
value: prefix,
lifetimes: check_lifetimes(
ia_prefix_data.valid_lifetime(),
ia_prefix_data.preferred_lifetime(),
),
}),
opt @ v6::ParsedDhcpOption::IaAddr(_) => {
Err(IaOptionError::InvalidOption(format!("{:?}", opt)).into())
}
opt => unreachable!(
"other options should be handled before this fn is called; got = {:?}",
opt
),
})
}
#[derive(Debug)]
enum NextContactTime {
InformationRefreshTime(Option<u32>),
RenewRebind { t1: v6::NonZeroTimeValue, t2: v6::NonZeroTimeValue },
}
#[derive(Debug)]
struct Options {
success_status_message: Option<String>,
next_contact_time: NextContactTime,
preference: Option<u8>,
non_temporary_addresses: HashMap<v6::IAID, IaNaOption>,
delegated_prefixes: HashMap<v6::IAID, IaPdOption>,
dns_servers: Option<Vec<Ipv6Addr>>,
}
#[derive(Debug)]
struct ProcessedOptions {
server_id: Vec<u8>,
solicit_max_rt_opt: Option<u32>,
result: Result<Options, ErrorStatusCode>,
}
#[derive(thiserror::Error, Debug)]
#[cfg_attr(test, derive(PartialEq))]
#[error("error status code={0}, message='{1}'")]
struct ErrorStatusCode(v6::ErrorStatusCode, String);
#[derive(thiserror::Error, Debug)]
enum OptionsError {
// TODO(https://fxbug.dev/42055437): Use an owned option type rather
// than a string of the debug representation of the invalid option.
#[error("duplicate option with code {0:?} {1} and {2}")]
DuplicateOption(v6::OptionCode, String, String),
#[error("unknown status code {0} with message '{1}'")]
InvalidStatusCode(u16, String),
#[error("IA_NA option error")]
IaNaError(#[from] IaOptionError<Ipv6Addr>),
#[error("IA_PD option error")]
IaPdError(#[from] IaPdOptionError),
#[error("duplicate IA_NA option with IAID={0:?} {1:?} and {2:?}")]
DuplicateIaNaId(v6::IAID, IaNaOption, IaNaOption),
#[error("duplicate IA_PD option with IAID={0:?} {1:?} and {2:?}")]
DuplicateIaPdId(v6::IAID, IaPdOption, IaPdOption),
#[error("IA_NA with unexpected IAID")]
UnexpectedIaNa(v6::IAID, IaNaOption),
#[error("IA_PD with unexpected IAID")]
UnexpectedIaPd(v6::IAID, IaPdOption),
#[error("missing Server Id option")]
MissingServerId,
#[error("missing Client Id option")]
MissingClientId,
#[error("got Client ID option {got:?} but want {want:?}")]
MismatchedClientId { got: Vec<u8>, want: Vec<u8> },
#[error("unexpected Client ID in Reply to anonymous Information-Request: {0:?}")]
UnexpectedClientId(Vec<u8>),
// TODO(https://fxbug.dev/42055437): Use an owned option type rather
// than a string of the debug representation of the invalid option.
#[error("invalid option found: {0:?}")]
InvalidOption(String),
}
/// Message types sent by the client for which a Reply from the server
/// contains IA options with assigned leases.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
enum RequestLeasesMessageType {
Request,
Renew,
Rebind,
}
impl std::fmt::Display for RequestLeasesMessageType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Request => write!(f, "Request"),
Self::Renew => write!(f, "Renew"),
Self::Rebind => write!(f, "Rebind"),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
enum ExchangeType {
ReplyToInformationRequest,
AdvertiseToSolicit,
ReplyWithLeases(RequestLeasesMessageType),
}
trait IaChecker {
/// Returns true ifff the IA was requested
fn was_ia_requested(&self, id: &v6::IAID) -> bool;
}
struct NoIaRequested;
impl IaChecker for NoIaRequested {
fn was_ia_requested(&self, _id: &v6::IAID) -> bool {
false
}
}
impl<V> IaChecker for HashMap<v6::IAID, V> {
fn was_ia_requested(&self, id: &v6::IAID) -> bool {
self.get(id).is_some()
}
}
// TODO(https://fxbug.dev/42055137): Make the choice between ignoring invalid
// options and discarding the entire message configurable.
// TODO(https://fxbug.dev/42055684): Move this function and associated types
// into packet-formats-dhcp.
/// Process options.
///
/// If any singleton options appears more than once, or there are multiple
/// IA options of the same type with duplicate ID's, the entire message will
/// be ignored as if it was never received.
///
/// Per RFC 8415, section 16:
///
/// This section describes which options are valid in which kinds of
/// message types and explains what to do when a client or server
/// receives a message that contains known options that are invalid for
/// that message. [...]
///
/// Clients and servers MAY choose to either (1) extract information from
/// such a message if the information is of use to the recipient or
/// (2) ignore such a message completely and just discard it.
///
/// The choice made by this function is (2): an error will be returned in such
/// cases to inform callers that they should ignore the entire message.
fn process_options<B: ByteSlice, IaNaChecker: IaChecker, IaPdChecker: IaChecker>(
msg: &v6::Message<'_, B>,
exchange_type: ExchangeType,
want_client_id: Option<&[u8]>,
iana_checker: &IaNaChecker,
iapd_checker: &IaPdChecker,
) -> Result<ProcessedOptions, OptionsError> {
let mut solicit_max_rt_option = None;
let mut server_id_option = None;
let mut client_id_option = None;
let mut preference = None;
let mut non_temporary_addresses = HashMap::new();
let mut delegated_prefixes = HashMap::new();
let mut status_code_option = None;
let mut dns_servers = None;
let mut refresh_time_option = None;
let mut min_t1 = v6::TimeValue::Zero;
let mut min_t2 = v6::TimeValue::Zero;
let mut min_preferred_lifetime = v6::TimeValue::Zero;
// Ok to initialize with Infinity, `get_nonzero_min` will pick a
// smaller value once we see an IA with a valid lifetime less than
// Infinity.
let mut min_valid_lifetime = v6::NonZeroTimeValue::Infinity;
// Updates the minimum preferred/valid and T1/T2 (life)times in response
// to an IA option.
let mut update_min_preferred_valid_lifetimes = |preferred_lifetime, valid_lifetime| {
min_preferred_lifetime = maybe_get_nonzero_min(min_preferred_lifetime, preferred_lifetime);
min_valid_lifetime = std::cmp::min(min_valid_lifetime, valid_lifetime);
};
let mut update_min_t1_t2 = |t1, t2| {
// If T1/T2 are set by the server to values greater than 0,
// compute the minimum T1 and T2 values, per RFC 8415,
// section 18.2.4:
//
// [..] the client SHOULD renew/rebind all IAs from the
// server at the same time, the client MUST select T1 and
// T2 times from all IA options that will guarantee that
// the client initiates transmissions of Renew/Rebind
// messages not later than at the T1/T2 times associated
// with any of the client's bindings (earliest T1/T2).
//
// Only IAs that with success status are included in the earliest
// T1/T2 calculation.
min_t1 = maybe_get_nonzero_min(min_t1, t1);
min_t2 = maybe_get_nonzero_min(min_t2, t2);
};
struct AllowedOptions {
preference: bool,
information_refresh_time: bool,
identity_association: bool,
}
// See RFC 8415 appendix B for a summary of which options are allowed in
// which message types.
let AllowedOptions {
preference: preference_allowed,
information_refresh_time: information_refresh_time_allowed,
identity_association: identity_association_allowed,
} = match exchange_type {
ExchangeType::ReplyToInformationRequest => AllowedOptions {
preference: false,
information_refresh_time: true,
// Per RFC 8415, section 16.12:
//
// Servers MUST discard any received Information-request message that
// meets any of the following conditions:
//
// - the message includes an IA option.
//
// Since it's invalid to include IA options in an Information-request message,
// it is also invalid to receive IA options in a Reply in response to an
// Information-request message.
identity_association: false,
},
ExchangeType::AdvertiseToSolicit => AllowedOptions {
preference: true,
information_refresh_time: false,
identity_association: true,
},
ExchangeType::ReplyWithLeases(
RequestLeasesMessageType::Request
| RequestLeasesMessageType::Renew
| RequestLeasesMessageType::Rebind,
) => {
AllowedOptions {
preference: false,
// Per RFC 8415, section 21.23
//
// Information Refresh Time Option
//
// [...] It is only used in Reply messages in response
// to Information-request messages.
information_refresh_time: false,
identity_association: true,
}
}
};
for opt in msg.options() {
match opt {
v6::ParsedDhcpOption::ClientId(client_id) => {
if let Some(existing) = client_id_option {
return Err(OptionsError::DuplicateOption(
v6::OptionCode::ClientId,
format!("{:?}", existing),
format!("{:?}", client_id.to_vec()),
));
}
client_id_option = Some(client_id.to_vec());
}
v6::ParsedDhcpOption::ServerId(server_id_opt) => {
if let Some(existing) = server_id_option {
return Err(OptionsError::DuplicateOption(
v6::OptionCode::ServerId,
format!("{:?}", existing),
format!("{:?}", server_id_opt.to_vec()),
));
}
server_id_option = Some(server_id_opt.to_vec());
}
v6::ParsedDhcpOption::SolMaxRt(sol_max_rt_opt) => {
if let Some(existing) = solicit_max_rt_option {
return Err(OptionsError::DuplicateOption(
v6::OptionCode::SolMaxRt,
format!("{:?}", existing),
format!("{:?}", sol_max_rt_opt.get()),
));
}
// Per RFC 8415, section 21.24:
//
// SOL_MAX_RT value MUST be in this range: 60 <= "value" <= 86400
//
// A DHCP client MUST ignore any SOL_MAX_RT option values that are
// less than 60 or more than 86400.
if !VALID_MAX_SOLICIT_TIMEOUT_RANGE.contains(&sol_max_rt_opt.get()) {
warn!(
"{:?}: ignoring SOL_MAX_RT value {} outside of range {:?}",
exchange_type,
sol_max_rt_opt.get(),
VALID_MAX_SOLICIT_TIMEOUT_RANGE,
);
} else {
// TODO(https://fxbug.dev/42054450): Use a bounded type to
// store SOL_MAX_RT.
solicit_max_rt_option = Some(sol_max_rt_opt.get());
}
}
v6::ParsedDhcpOption::Preference(preference_opt) => {
if !preference_allowed {
return Err(OptionsError::InvalidOption(format!("{:?}", opt)));
}
if let Some(existing) = preference {
return Err(OptionsError::DuplicateOption(
v6::OptionCode::Preference,
format!("{:?}", existing),
format!("{:?}", preference_opt),
));
}
preference = Some(preference_opt);
}
v6::ParsedDhcpOption::Iana(ref iana_data) => {
if !identity_association_allowed {
return Err(OptionsError::InvalidOption(format!("{:?}", opt)));
}
let iaid = v6::IAID::new(iana_data.iaid());
let processed_ia_na = match process_ia_na(iana_data) {
Ok(o) => o,
Err(IaOptionError::T1GreaterThanT2 { t1: _, t2: _ }) => {
// As per RFC 8415 section 21.4,
//
// If a client receives an IA_NA with T1 greater than
// T2 and both T1 and T2 are greater than 0, the
// client discards the IA_NA option and processes the
// remainder of the message as though the server had
// not included the invalid IA_NA option.
continue;
}
Err(
e @ IaOptionError::StatusCode(_)
| e @ IaOptionError::InvalidOption(_)
| e @ IaOptionError::DuplicateIaValue {
value: _,
first_lifetimes: _,
second_lifetimes: _,
},
) => {
return Err(OptionsError::IaNaError(e));
}
};
if !iana_checker.was_ia_requested(&iaid) {
// The RFC does not explicitly call out what to do with
// IAs that were not requested by the client.
//
// Return an error to cause the entire message to be
// ignored.
return Err(OptionsError::UnexpectedIaNa(iaid, processed_ia_na));
}
match processed_ia_na {
IaNaOption::Failure(_) => {}
IaNaOption::Success { status_message: _, t1, t2, ref ia_values } => {
let mut update_t1_t2 = false;
for (_value, lifetimes) in ia_values {
match lifetimes {
Err(_) => {}
Ok(Lifetimes { preferred_lifetime, valid_lifetime }) => {
update_min_preferred_valid_lifetimes(
*preferred_lifetime,
*valid_lifetime,
);
update_t1_t2 = true;
}
}
}
if update_t1_t2 {
update_min_t1_t2(t1, t2);
}
}
}
// Per RFC 8415, section 21.4, IAIDs are expected to be
// unique.
//
// A DHCP message may contain multiple IA_NA options
// (though each must have a unique IAID).
match non_temporary_addresses.entry(iaid) {
Entry::Occupied(entry) => {
return Err(OptionsError::DuplicateIaNaId(
iaid,
entry.remove(),
processed_ia_na,
));
}
Entry::Vacant(entry) => {
let _: &mut IaNaOption = entry.insert(processed_ia_na);
}
};
}
v6::ParsedDhcpOption::StatusCode(code, message) => {
let status_code = match v6::StatusCode::try_from(code.get()) {
Ok(status_code) => status_code,
Err(v6::ParseError::InvalidStatusCode(invalid)) => {
return Err(OptionsError::InvalidStatusCode(invalid, message.to_string()));
}
Err(e) => {
unreachable!("unreachable status code parse error: {}", e);
}
};
if let Some(existing) = status_code_option {
return Err(OptionsError::DuplicateOption(
v6::OptionCode::StatusCode,
format!("{:?}", existing),
format!("{:?}", (status_code, message.to_string())),
));
}
status_code_option = Some((status_code, message.to_string()));
}
v6::ParsedDhcpOption::IaPd(ref iapd_data) => {
if !identity_association_allowed {
return Err(OptionsError::InvalidOption(format!("{:?}", opt)));
}
let iaid = v6::IAID::new(iapd_data.iaid());
let processed_ia_pd = match process_ia_pd(iapd_data) {
Ok(o) => o,
Err(IaPdOptionError::IaOptionError(IaOptionError::T1GreaterThanT2 {
t1: _,
t2: _,
})) => {
// As per RFC 8415 section 21.4,
//
// If a client receives an IA_NA with T1 greater than
// T2 and both T1 and T2 are greater than 0, the
// client discards the IA_NA option and processes the
// remainder of the message as though the server had
// not included the invalid IA_NA option.
continue;
}
Err(
e @ IaPdOptionError::IaOptionError(IaOptionError::StatusCode(_))
| e @ IaPdOptionError::IaOptionError(IaOptionError::InvalidOption(_))
| e @ IaPdOptionError::IaOptionError(IaOptionError::DuplicateIaValue {
value: _,
first_lifetimes: _,
second_lifetimes: _,
})
| e @ IaPdOptionError::InvalidSubnet,
) => {
return Err(OptionsError::IaPdError(e));
}
};
if !iapd_checker.was_ia_requested(&iaid) {
// The RFC does not explicitly call out what to do with
// IAs that were not requested by the client.
//
// Return an error to cause the entire message to be
// ignored.
return Err(OptionsError::UnexpectedIaPd(iaid, processed_ia_pd));
}
match processed_ia_pd {
IaPdOption::Failure(_) => {}
IaPdOption::Success { status_message: _, t1, t2, ref ia_values } => {
let mut update_t1_t2 = false;
for (_value, lifetimes) in ia_values {
match lifetimes {
Err(_) => {}
Ok(Lifetimes { preferred_lifetime, valid_lifetime }) => {
update_min_preferred_valid_lifetimes(
*preferred_lifetime,
*valid_lifetime,
);
update_t1_t2 = true;
}
}
}
if update_t1_t2 {
update_min_t1_t2(t1, t2);
}
}
}
// Per RFC 8415, section 21.21, IAIDs are expected to be unique.
//
// A DHCP message may contain multiple IA_PD options (though
// each must have a unique IAID).
match delegated_prefixes.entry(iaid) {
Entry::Occupied(entry) => {
return Err(OptionsError::DuplicateIaPdId(
iaid,
entry.remove(),
processed_ia_pd,
));
}
Entry::Vacant(entry) => {
let _: &mut IaPdOption = entry.insert(processed_ia_pd);
}
};
}
v6::ParsedDhcpOption::InformationRefreshTime(information_refresh_time) => {
if !information_refresh_time_allowed {
return Err(OptionsError::InvalidOption(format!("{:?}", opt)));
}
if let Some(existing) = refresh_time_option {
return Err(OptionsError::DuplicateOption(
v6::OptionCode::InformationRefreshTime,
format!("{:?}", existing),
format!("{:?}", information_refresh_time),
));
}
refresh_time_option = Some(information_refresh_time);
}
v6::ParsedDhcpOption::IaAddr(_)
| v6::ParsedDhcpOption::IaPrefix(_)
| v6::ParsedDhcpOption::Oro(_)
| v6::ParsedDhcpOption::ElapsedTime(_) => {
return Err(OptionsError::InvalidOption(format!("{:?}", opt)));
}
v6::ParsedDhcpOption::DnsServers(server_addrs) => {
if let Some(existing) = dns_servers {
return Err(OptionsError::DuplicateOption(
v6::OptionCode::DnsServers,
format!("{:?}", existing),
format!("{:?}", server_addrs),
));
}
dns_servers = Some(server_addrs);
}
v6::ParsedDhcpOption::DomainList(_domains) => {
// TODO(https://fxbug.dev/42168268) implement domain list.
}
}
}
// For all three message types the server sends to the client (Advertise, Reply,
// and Reconfigue), RFC 8415 sections 16.3, 16.10, and 16.11 respectively state
// that:
//
// Clients MUST discard any received ... message that meets
// any of the following conditions:
// - the message does not include a Server Identifier option (see
// Section 21.3).
let server_id = server_id_option.ok_or(OptionsError::MissingServerId)?;
// For all three message types the server sends to the client (Advertise, Reply,
// and Reconfigue), RFC 8415 sections 16.3, 16.10, and 16.11 respectively state
// that:
//
// Clients MUST discard any received ... message that meets
// any of the following conditions:
// - the message does not include a Client Identifier option (see
// Section 21.2).
// - the contents of the Client Identifier option do not match the
// client's DUID.
//
// The exception is that clients may send Information-Request messages
// without a client ID per RFC 8415 section 18.2.6:
//
// The client SHOULD include a Client Identifier option (see
// Section 21.2) to identify itself to the server (however, see
// Section 4.3.1 of [RFC7844] for reasons why a client may not want to
// include this option).
match (client_id_option, want_client_id) {
(None, None) => {}
(Some(got), None) => return Err(OptionsError::UnexpectedClientId(got)),
(None, Some::<&[u8]>(_)) => return Err(OptionsError::MissingClientId),
(Some(got), Some(want)) => {
if got != want {
return Err(OptionsError::MismatchedClientId {
want: want.to_vec(),
got: got.to_vec(),
});
}
}
}
let success_status_message = match status_code_option {
Some((status_code, message)) => match status_code.into_result() {
Ok(()) => Some(message),
Err(error_code) => {
return Ok(ProcessedOptions {
server_id,
solicit_max_rt_opt: solicit_max_rt_option,
result: Err(ErrorStatusCode(error_code, message)),
});
}
},
// Missing status code option means success per RFC 8415 section 7.5:
//
// If the Status Code option (see Section 21.13) does not appear
// in a message in which the option could appear, the status
// of the message is assumed to be Success.
None => None,
};
let next_contact_time = match exchange_type {
ExchangeType::ReplyToInformationRequest => {
NextContactTime::InformationRefreshTime(refresh_time_option)
}
ExchangeType::AdvertiseToSolicit
| ExchangeType::ReplyWithLeases(
RequestLeasesMessageType::Request
| RequestLeasesMessageType::Renew
| RequestLeasesMessageType::Rebind,
) => {
// If not set or 0, choose a value for T1 and T2, per RFC 8415, section
// 18.2.4:
//
// If T1 or T2 had been set to 0 by the server (for an
// IA_NA or IA_PD) or there are no T1 or T2 times (for an
// IA_TA) in a previous Reply, the client may, at its
// discretion, send a Renew or Rebind message,
// respectively. The client MUST follow the rules
// defined in Section 14.2.
//
// Per RFC 8415, section 14.2:
//
// When T1 and/or T2 values are set to 0, the client MUST choose a
// time to avoid packet storms. In particular, it MUST NOT transmit
// immediately.
//
// When left to the client's discretion, the client chooses T1/T1 values
// following the recommentations in RFC 8415, section 21.4:
//
// Recommended values for T1 and T2 are 0.5 and 0.8 times the
// shortest preferred lifetime of the addresses in the IA that the
// server is willing to extend, respectively. If the "shortest"
// preferred lifetime is 0xffffffff ("infinity"), the recommended T1
// and T2 values are also 0xffffffff.
//
// The RFC does not specify how to compute T1 if the shortest preferred
// lifetime is zero and T1 is zero. In this case, T1 is calculated as a
// fraction of the shortest valid lifetime.
let t1 = match min_t1 {
v6::TimeValue::Zero => {
let min = match min_preferred_lifetime {
v6::TimeValue::Zero => min_valid_lifetime,
v6::TimeValue::NonZero(t) => t,
};
compute_t(min, T1_MIN_LIFETIME_RATIO)
}
v6::TimeValue::NonZero(t) => t,
};
// T2 must be >= T1, compute its value based on T1.
let t2 = match min_t2 {
v6::TimeValue::Zero => compute_t(t1, T2_T1_RATIO),
v6::TimeValue::NonZero(t2_val) => {
if t2_val < t1 {
compute_t(t1, T2_T1_RATIO)
} else {
t2_val
}
}
};
NextContactTime::RenewRebind { t1, t2 }
}
};
Ok(ProcessedOptions {
server_id,
solicit_max_rt_opt: solicit_max_rt_option,
result: Ok(Options {
success_status_message,
next_contact_time,
preference,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
}),
})
}
struct StatefulMessageBuilder<'a, AddrIter, PrefixIter, IaNaIter, IaPdIter> {
transaction_id: [u8; 3],
message_type: v6::MessageType,
client_id: &'a [u8],
server_id: Option<&'a [u8]>,
elapsed_time_in_centisecs: u16,
options_to_request: &'a [v6::OptionCode],
ia_nas: IaNaIter,
ia_pds: IaPdIter,
_marker: std::marker::PhantomData<(AddrIter, PrefixIter)>,
}
impl<
'a,
AddrIter: Iterator<Item = Ipv6Addr>,
PrefixIter: Iterator<Item = Subnet<Ipv6Addr>>,
IaNaIter: Iterator<Item = (v6::IAID, AddrIter)>,
IaPdIter: Iterator<Item = (v6::IAID, PrefixIter)>,
> StatefulMessageBuilder<'a, AddrIter, PrefixIter, IaNaIter, IaPdIter>
{
fn build(self) -> Vec<u8> {
let StatefulMessageBuilder {
transaction_id,
message_type,
client_id,
server_id,
elapsed_time_in_centisecs,
options_to_request,
ia_nas,
ia_pds,
_marker,
} = self;
debug_assert!(!options_to_request.contains(&v6::OptionCode::SolMaxRt));
let oro = [v6::OptionCode::SolMaxRt]
.into_iter()
.chain(options_to_request.into_iter().cloned())
.collect::<Vec<_>>();
// Adds IA_{NA,PD} options: one IA_{NA,PD} per hint, plus options
// without hints, up to the configured count, as described in
// RFC 8415, section 6.6:
//
// A client can explicitly request multiple addresses by sending
// multiple IA_NA options (and/or IA_TA options; see Section 21.5). A
// client can send multiple IA_NA (and/or IA_TA) options in its initial
// transmissions. Alternatively, it can send an extra Request message
// with additional new IA_NA (and/or IA_TA) options (or include them in
// a Renew message).
//
// The same principle also applies to prefix delegation. In principle,
// DHCP allows a client to request new prefixes to be delegated by
// sending additional IA_PD options (see Section 21.21). However, a
// typical operator usually prefers to delegate a single, larger prefix.
// In most deployments, it is recommended that the client request a
// larger prefix in its initial transmissions rather than request
// additional prefixes later on.
let iaaddr_options = ia_nas
.map(|(iaid, inner)| {
(
iaid,
inner
.map(|addr| {
v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(addr, 0, 0, &[]))
})
.collect::<Vec<_>>(),
)
})
.collect::<HashMap<_, _>>();
let iaprefix_options = ia_pds
.map(|(iaid, inner)| {
(
iaid,
inner
.map(|prefix| {
v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(0, 0, prefix, &[]))
})
.collect::<Vec<_>>(),
)
})
.collect::<HashMap<_, _>>();
let options = server_id
.into_iter()
.map(v6::DhcpOption::ServerId)
.chain([
v6::DhcpOption::ClientId(client_id),
v6::DhcpOption::ElapsedTime(elapsed_time_in_centisecs),
v6::DhcpOption::Oro(&oro),
])
.chain(iaaddr_options.iter().map(|(iaid, iaddr_opt)| {
v6::DhcpOption::Iana(v6::IanaSerializer::new(*iaid, 0, 0, iaddr_opt.as_slice()))
}))
.chain(iaprefix_options.iter().map(|(iaid, iaprefix_opt)| {
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(*iaid, 0, 0, iaprefix_opt.as_slice()))
}))
.collect::<Vec<_>>();
let builder = v6::MessageBuilder::new(message_type, transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
buf
}
}
/// Provides methods for handling state transitions from server discovery
/// state.
#[derive(Debug)]
struct ServerDiscovery<I> {
/// [Client Identifier] used for uniquely identifying the client in
/// communication with servers.
///
/// [Client Identifier]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.2
client_id: ClientDuid,
/// The non-temporary addresses the client is configured to negotiate.
configured_non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>>,
/// The delegated prefixes the client is configured to negotiate.
configured_delegated_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
/// The time of the first solicit. Used in calculating the [elapsed time].
///
/// [elapsed time]:https://datatracker.ietf.org/doc/html/rfc8415#section-21.9
first_solicit_time: I,
/// The solicit retransmission timeout.
retrans_timeout: Duration,
/// The [SOL_MAX_RT] used by the client.
///
/// [SOL_MAX_RT]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.24
solicit_max_rt: Duration,
/// The advertise collected from servers during [server discovery], with
/// the best advertise at the top of the heap.
///
/// [server discovery]: https://datatracker.ietf.org/doc/html/rfc8415#section-18
collected_advertise: BinaryHeap<AdvertiseMessage<I>>,
/// The valid SOL_MAX_RT options received from servers.
collected_sol_max_rt: Vec<u32>,
}
impl<I: Instant> ServerDiscovery<I> {
/// Starts server discovery by sending a solicit message, as described in
/// [RFC 8415, Section 18.2.1].
///
/// [RFC 8415, Section 18.2.1]: https://datatracker.ietf.org/doc/html/rfc8415#section-18.2.1
fn start<R: Rng>(
transaction_id: [u8; 3],
client_id: ClientDuid,
configured_non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>>,
configured_delegated_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
options_to_request: &[v6::OptionCode],
solicit_max_rt: Duration,
rng: &mut R,
now: I,
initial_actions: impl Iterator<Item = Action<I>>,
) -> Transition<I> {
Self {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time: now,
retrans_timeout: Duration::default(),
solicit_max_rt,
collected_advertise: BinaryHeap::new(),
collected_sol_max_rt: Vec::new(),
}
.send_and_schedule_retransmission(
transaction_id,
options_to_request,
rng,
now,
initial_actions,
)
}
/// Calculates timeout for retransmitting solicits using parameters
/// specified in [RFC 8415, Section 18.2.1].
///
/// [RFC 8415, Section 18.2.1]: https://datatracker.ietf.org/doc/html/rfc8415#section-18.2.1
fn retransmission_timeout<R: Rng>(
prev_retrans_timeout: Duration,
max_retrans_timeout: Duration,
rng: &mut R,
) -> Duration {
retransmission_timeout(
prev_retrans_timeout,
INITIAL_SOLICIT_TIMEOUT,
max_retrans_timeout,
rng,
)
}
/// Returns a transition to stay in `ServerDiscovery`, with actions to send a
/// solicit and schedule retransmission.
fn send_and_schedule_retransmission<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
initial_actions: impl Iterator<Item = Action<I>>,
) -> Transition<I> {
let Self {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
} = self;
let elapsed_time = elapsed_time_in_centisecs(first_solicit_time, now);
// Per RFC 8415, section 18.2.1:
//
// The client sets the "msg-type" field to SOLICIT. The client
// generates a transaction ID and inserts this value in the
// "transaction-id" field.
//
// The client MUST include a Client Identifier option (see Section
// 21.2) to identify itself to the server. The client includes IA
// options for any IAs to which it wants the server to assign leases.
//
// The client MUST include an Elapsed Time option (see Section 21.9)
// to indicate how long the client has been trying to complete the
// current DHCP message exchange.
//
// The client uses IA_NA options (see Section 21.4) to request the
// assignment of non-temporary addresses, IA_TA options (see
// Section 21.5) to request the assignment of temporary addresses, and
// IA_PD options (see Section 21.21) to request prefix delegation.
// IA_NA, IA_TA, or IA_PD options, or a combination of all, can be
// included in DHCP messages. In addition, multiple instances of any
// IA option type can be included.
//
// The client MAY include addresses in IA Address options (see
// Section 21.6) encapsulated within IA_NA and IA_TA options as hints
// to the server about the addresses for which the client has a
// preference.
//
// The client MAY include values in IA Prefix options (see
// Section 21.22) encapsulated within IA_PD options as hints for the
// delegated prefix and/or prefix length for which the client has a
// preference. See Section 18.2.4 for more on prefix-length hints.
//
// The client MUST include an Option Request option (ORO) (see
// Section 21.7) to request the SOL_MAX_RT option (see Section 21.24)
// and any other options the client is interested in receiving. The
// client MAY additionally include instances of those options that are
// identified in the Option Request option, with data values as hints
// to the server about parameter values the client would like to have
// returned.
//
// ...
//
// The client MUST NOT include any other options in the Solicit message,
// except as specifically allowed in the definition of individual
// options.
let buf = StatefulMessageBuilder {
transaction_id,
message_type: v6::MessageType::Solicit,
server_id: None,
client_id: &client_id,
elapsed_time_in_centisecs: elapsed_time,
options_to_request,
ia_nas: configured_non_temporary_addresses
.iter()
.map(|(iaid, ia)| (*iaid, ia.iter().cloned())),
ia_pds: configured_delegated_prefixes
.iter()
.map(|(iaid, ia)| (*iaid, ia.iter().cloned())),
_marker: Default::default(),
}
.build();
let retrans_timeout = Self::retransmission_timeout(retrans_timeout, solicit_max_rt, rng);
Transition {
state: ClientState::ServerDiscovery(ServerDiscovery {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
}),
actions: initial_actions
.chain([
Action::SendMessage(buf),
Action::ScheduleTimer(
ClientTimerType::Retransmission,
now.add(retrans_timeout),
),
])
.collect(),
transaction_id: None,
}
}
/// Selects a server, or retransmits solicit if no valid advertise were
/// received.
fn retransmission_timer_expired<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
mut collected_advertise,
collected_sol_max_rt,
} = self;
let solicit_max_rt = get_common_value(&collected_sol_max_rt).unwrap_or(solicit_max_rt);
// Update SOL_MAX_RT, per RFC 8415, section 18.2.9:
//
// A client SHOULD only update its SOL_MAX_RT [..] if all received
// Advertise messages that contained the corresponding option
// specified the same value.
if let Some(advertise) = collected_advertise.pop() {
let AdvertiseMessage {
server_id,
non_temporary_addresses: advertised_non_temporary_addresses,
delegated_prefixes: advertised_delegated_prefixes,
dns_servers: _,
preference: _,
receive_time: _,
preferred_non_temporary_addresses_count: _,
preferred_delegated_prefixes_count: _,
} = advertise;
return Requesting::start(
client_id,
server_id,
advertise_to_ia_entries(
advertised_non_temporary_addresses,
configured_non_temporary_addresses,
),
advertise_to_ia_entries(
advertised_delegated_prefixes,
configured_delegated_prefixes,
),
&options_to_request,
collected_advertise,
solicit_max_rt,
rng,
now,
);
}
ServerDiscovery {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
}
.send_and_schedule_retransmission(
transaction_id,
options_to_request,
rng,
now,
std::iter::empty(),
)
}
fn advertise_message_received<R: Rng, B: ByteSlice>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
msg: v6::Message<'_, B>,
now: I,
) -> Transition<I> {
let Self {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
} = self;
let ProcessedOptions { server_id, solicit_max_rt_opt, result } = match process_options(
&msg,
ExchangeType::AdvertiseToSolicit,
Some(&client_id),
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
) {
Ok(processed_options) => processed_options,
Err(e) => {
warn!("ignoring Advertise: {}", e);
return Transition {
state: ClientState::ServerDiscovery(ServerDiscovery {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
}),
actions: Vec::new(),
transaction_id: None,
};
}
};
// Process SOL_MAX_RT and discard invalid advertise following RFC 8415,
// section 18.2.9:
//
// The client MUST process any SOL_MAX_RT option [..] even if the
// message contains a Status Code option indicating a failure, and
// the Advertise message will be discarded by the client.
//
// The client MUST ignore any Advertise message that contains no
// addresses (IA Address options (see Section 21.6) encapsulated in
// IA_NA options (see Section 21.4) or IA_TA options (see Section 21.5))
// and no delegated prefixes (IA Prefix options (see Section 21.22)
// encapsulated in IA_PD options (see Section 21.21)), with the
// exception that the client:
//
// - MUST process an included SOL_MAX_RT option and
//
// - MUST process an included INF_MAX_RT option.
let mut collected_sol_max_rt = collected_sol_max_rt;
if let Some(solicit_max_rt) = solicit_max_rt_opt {
collected_sol_max_rt.push(solicit_max_rt);
}
let Options {
success_status_message,
next_contact_time: _,
preference,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
} = match result {
Ok(options) => options,
Err(e) => {
warn!("Advertise from server {:?} error status code: {}", server_id, e);
return Transition {
state: ClientState::ServerDiscovery(ServerDiscovery {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
}),
actions: Vec::new(),
transaction_id: None,
};
}
};
match success_status_message {
Some(success_status_message) if !success_status_message.is_empty() => {
info!(
"Advertise from server {:?} contains success status code message: {}",
server_id, success_status_message,
);
}
_ => {
info!("processing Advertise from server {:?}", server_id);
}
}
let non_temporary_addresses = non_temporary_addresses
.into_iter()
.filter_map(|(iaid, ia_na)| {
let (success_status_message, ia_addrs) = match ia_na {
IaNaOption::Success { status_message, t1: _, t2: _, ia_values } => {
(status_message, ia_values)
}
IaNaOption::Failure(e) => {
warn!(
"Advertise from server {:?} contains IA_NA with error status code: {}",
server_id, e
);
return None;
}
};
if let Some(success_status_message) = success_status_message {
if !success_status_message.is_empty() {
info!(
"Advertise from server {:?} IA_NA with IAID {:?} \
success status code message: {}",
server_id, iaid, success_status_message,
);
}
}
let ia_addrs = ia_addrs
.into_iter()
.filter_map(|(value, lifetimes)| match lifetimes {
Ok(Lifetimes { preferred_lifetime: _, valid_lifetime: _ }) => Some(value),
e @ Err(
LifetimesError::ValidLifetimeZero
| LifetimesError::PreferredLifetimeGreaterThanValidLifetime(_),
) => {
warn!(
"Advertise from server {:?}: ignoring IA Address in \
IA_NA with IAID {:?} because of invalid lifetimes: {:?}",
server_id, iaid, e
);
// Per RFC 8415 section 21.6,
//
// The client MUST discard any addresses for which
// the preferred lifetime is greater than the
// valid lifetime.
None
}
})
.collect::<HashSet<_>>();
(!ia_addrs.is_empty()).then(|| (iaid, ia_addrs))
})
.collect::<HashMap<_, _>>();
let delegated_prefixes = delegated_prefixes
.into_iter()
.filter_map(|(iaid, ia_pd)| {
let (success_status_message, ia_prefixes) = match ia_pd {
IaPdOption::Success { status_message, t1: _, t2: _, ia_values } => {
(status_message, ia_values)
}
IaPdOption::Failure(e) => {
warn!(
"Advertise from server {:?} contains IA_PD with error status code: {}",
server_id, e
);
return None;
}
};
if let Some(success_status_message) = success_status_message {
if !success_status_message.is_empty() {
info!(
"Advertise from server {:?} IA_PD with IAID {:?} \
success status code message: {}",
server_id, iaid, success_status_message,
);
}
}
let ia_prefixes = ia_prefixes
.into_iter()
.filter_map(|(value, lifetimes)| match lifetimes {
Ok(Lifetimes { preferred_lifetime: _, valid_lifetime: _ }) => Some(value),
e @ Err(
LifetimesError::ValidLifetimeZero
| LifetimesError::PreferredLifetimeGreaterThanValidLifetime(_),
) => {
warn!(
"Advertise from server {:?}: ignoring IA Prefix in \
IA_PD with IAID {:?} because of invalid lifetimes: {:?}",
server_id, iaid, e
);
// Per RFC 8415 section 21.22,
//
// The client MUST discard any prefixes for which
// the preferred lifetime is greater than the
// valid lifetime.
None
}
})
.collect::<HashSet<_>>();
(!ia_prefixes.is_empty()).then(|| (iaid, ia_prefixes))
})
.collect::<HashMap<_, _>>();
let advertise = AdvertiseMessage {
preferred_non_temporary_addresses_count: compute_preferred_ia_count(
&non_temporary_addresses,
&configured_non_temporary_addresses,
),
preferred_delegated_prefixes_count: compute_preferred_ia_count(
&delegated_prefixes,
&configured_delegated_prefixes,
),
server_id,
non_temporary_addresses,
delegated_prefixes,
dns_servers: dns_servers.unwrap_or(Vec::new()),
// Per RFC 8415, section 18.2.1:
//
// Any valid Advertise that does not include a Preference
// option is considered to have a preference value of 0.
preference: preference.unwrap_or(0),
receive_time: now,
};
if !advertise.has_ias() {
return Transition {
state: ClientState::ServerDiscovery(ServerDiscovery {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
}),
actions: Vec::new(),
transaction_id: None,
};
}
let solicit_timeout = INITIAL_SOLICIT_TIMEOUT.as_secs_f64();
let is_retransmitting = retrans_timeout.as_secs_f64()
>= solicit_timeout + solicit_timeout * RANDOMIZATION_FACTOR_MAX;
// Select server if its preference value is `255` and the advertise is
// acceptable, as described in RFC 8415, section 18.2.1:
//
// If the client receives a valid Advertise message that includes a
// Preference option with a preference value of 255, the client
// immediately begins a client-initiated message exchange (as
// described in Section 18.2.2) by sending a Request message to the
// server from which the Advertise message was received.
//
// Per RFC 8415, section 18.2.9:
//
// Those Advertise messages with the highest server preference value
// SHOULD be preferred over all other Advertise messages. The
// client MAY choose a less preferred server if that server has a
// better set of advertised parameters.
//
// During retrasmission, the client select the server that sends the
// first valid advertise, regardless of preference value or advertise
// completeness, as described in RFC 8415, section 18.2.1:
//
// The client terminates the retransmission process as soon as it
// receives any valid Advertise message, and the client acts on the
// received Advertise message without waiting for any additional
// Advertise messages.
if (advertise.preference == ADVERTISE_MAX_PREFERENCE) || is_retransmitting {
let solicit_max_rt = get_common_value(&collected_sol_max_rt).unwrap_or(solicit_max_rt);
let AdvertiseMessage {
server_id,
non_temporary_addresses: advertised_non_temporary_addresses,
delegated_prefixes: advertised_delegated_prefixes,
dns_servers: _,
preference: _,
receive_time: _,
preferred_non_temporary_addresses_count: _,
preferred_delegated_prefixes_count: _,
} = advertise;
return Requesting::start(
client_id,
server_id,
advertise_to_ia_entries(
advertised_non_temporary_addresses,
configured_non_temporary_addresses,
),
advertise_to_ia_entries(
advertised_delegated_prefixes,
configured_delegated_prefixes,
),
&options_to_request,
collected_advertise,
solicit_max_rt,
rng,
now,
);
}
let mut collected_advertise = collected_advertise;
collected_advertise.push(advertise);
Transition {
state: ClientState::ServerDiscovery(ServerDiscovery {
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
first_solicit_time,
retrans_timeout,
solicit_max_rt,
collected_advertise,
collected_sol_max_rt,
}),
actions: Vec::new(),
transaction_id: None,
}
}
}
// Returns the min value greater than zero, if the arguments are non zero. If
// the new value is zero, the old value is returned unchanged; otherwise if the
// old value is zero, the new value is returned. Used for calculating the
// minimum T1/T2 as described in RFC 8415, section 18.2.4:
//
// [..] the client SHOULD renew/rebind all IAs from the
// server at the same time, the client MUST select T1 and
// T2 times from all IA options that will guarantee that
// the client initiates transmissions of Renew/Rebind
// messages not later than at the T1/T2 times associated
// with any of the client's bindings (earliest T1/T2).
fn maybe_get_nonzero_min(old_value: v6::TimeValue, new_value: v6::TimeValue) -> v6::TimeValue {
match old_value {
v6::TimeValue::Zero => new_value,
v6::TimeValue::NonZero(old_t) => v6::TimeValue::NonZero(get_nonzero_min(old_t, new_value)),
}
}
// Returns the min value greater than zero.
fn get_nonzero_min(
old_value: v6::NonZeroTimeValue,
new_value: v6::TimeValue,
) -> v6::NonZeroTimeValue {
match new_value {
v6::TimeValue::Zero => old_value,
v6::TimeValue::NonZero(new_val) => std::cmp::min(old_value, new_val),
}
}
/// Provides methods for handling state transitions from requesting state.
#[derive(Debug)]
struct Requesting<I> {
/// [Client Identifier] used for uniquely identifying the client in
/// communication with servers.
///
/// [Client Identifier]:
/// https://datatracker.ietf.org/doc/html/rfc8415#section-21.2
client_id: ClientDuid,
/// The non-temporary addresses negotiated by the client.
non_temporary_addresses: HashMap<v6::IAID, AddressEntry<I>>,
/// The delegated prefixes negotiated by the client.
delegated_prefixes: HashMap<v6::IAID, PrefixEntry<I>>,
/// The [server identifier] of the server to which the client sends
/// requests.
///
/// [Server Identifier]:
/// https://datatracker.ietf.org/doc/html/rfc8415#section-21.3
server_id: Vec<u8>,
/// The advertise collected from servers during [server discovery].
///
/// [server discovery]:
/// https://datatracker.ietf.org/doc/html/rfc8415#section-18
collected_advertise: BinaryHeap<AdvertiseMessage<I>>,
/// The time of the first request. Used in calculating the [elapsed time].
///
/// [elapsed time]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.9
first_request_time: I,
/// The request retransmission timeout.
retrans_timeout: Duration,
/// The number of request messages transmitted.
transmission_count: u8,
/// The [SOL_MAX_RT] used by the client.
///
/// [SOL_MAX_RT]:
/// https://datatracker.ietf.org/doc/html/rfc8415#section-21.24
solicit_max_rt: Duration,
}
fn compute_t(min: v6::NonZeroTimeValue, ratio: Ratio<u32>) -> v6::NonZeroTimeValue {
match min {
v6::NonZeroTimeValue::Finite(t) => {
ratio.checked_mul(&Ratio::new_raw(t.get(), 1)).map_or(
v6::NonZeroTimeValue::Infinity,
|t| {
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(t.to_integer()).expect(
"non-zero ratio of NonZeroOrMaxU32 value should be NonZeroOrMaxU32",
))
},
)
}
v6::NonZeroTimeValue::Infinity => v6::NonZeroTimeValue::Infinity,
}
}
#[derive(Debug, thiserror::Error)]
enum ReplyWithLeasesError {
#[error("option processing error")]
OptionsError(#[from] OptionsError),
#[error("mismatched Server ID, got {got:?} want {want:?}")]
MismatchedServerId { got: Vec<u8>, want: Vec<u8> },
#[error("status code error")]
ErrorStatusCode(#[from] ErrorStatusCode),
}
#[derive(Debug, Copy, Clone)]
enum IaStatusError {
Retry { without_hints: bool },
Invalid,
Rerequest,
}
fn process_ia_error_status(
request_type: RequestLeasesMessageType,
error_status: v6::ErrorStatusCode,
ia_kind: IaKind,
) -> IaStatusError {
match (request_type, error_status, ia_kind) {
// Per RFC 8415, section 18.3.2:
//
// If any of the prefixes of the included addresses are not
// appropriate for the link to which the client is connected,
// the server MUST return the IA to the client with a Status Code
// option (see Section 21.13) with the value NotOnLink.
//
// If the client receives IA_NAs with NotOnLink status, try to obtain
// other addresses in follow-up messages.
(RequestLeasesMessageType::Request, v6::ErrorStatusCode::NotOnLink, IaKind::Address) => {
IaStatusError::Retry { without_hints: true }
}
// NotOnLink is not expected for prefixes.
//
// Per RFC 8415 section 18.3.2,
//
// For any IA_PD option (see Section 21.21) in the Request message to
// which the server cannot assign any delegated prefixes, the server
// MUST return the IA_PD option in the Reply message with no prefixes
// in the IA_PD and with a Status Code option containing status code
// NoPrefixAvail in the IA_PD.
(RequestLeasesMessageType::Request, v6::ErrorStatusCode::NotOnLink, IaKind::Prefix) => {
IaStatusError::Invalid
}
// NotOnLink is not expected in Reply to Renew/Rebind. The server
// indicates that the IA is not appropriate for the link by setting
// lifetime 0, not by using NotOnLink status.
//
// For Renewing, per RFC 8415 section 18.3.4:
//
// If the server finds that any of the addresses in the IA are
// not appropriate for the link to which the client is attached,
// the server returns the address to the client with lifetimes of 0.
//
// If the server finds that any of the delegated prefixes in the IA
// are not appropriate for the link to which the client is attached,
// the server returns the delegated prefix to the client with
// lifetimes of 0.
//
// For Rebinding, per RFC 8415 section 18.3.6:
//
// If the server finds that the client entry for the IA and any of
// the addresses or delegated prefixes are no longer appropriate for
// the link to which the client's interface is attached according to
// the server's explicit configuration information, the server
// returns those addresses or delegated prefixes to the client with
// lifetimes of 0.
(
RequestLeasesMessageType::Renew | RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::NotOnLink,
IaKind::Address | IaKind::Prefix,
) => IaStatusError::Invalid,
// Per RFC 18.2.10,
//
// If the client receives a Reply message with a status code of
// UnspecFail, the server is indicating that it was unable to process
// the client's message due to an unspecified failure condition. If
// the client retransmits the original message to the same server to
// retry the desired operation, the client MUST limit the rate at
// which it retransmits the message and limit the duration of the time
// during which it retransmits the message (see Section 14.1).
(
RequestLeasesMessageType::Request
| RequestLeasesMessageType::Renew
| RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::UnspecFail,
IaKind::Address | IaKind::Prefix,
) => IaStatusError::Retry { without_hints: false },
// When responding to Request messages, per section 18.3.2:
//
// If the server [..] cannot assign any IP addresses to an IA,
// the server MUST return the IA option in the Reply message with
// no addresses in the IA and a Status Code option containing
// status code NoAddrsAvail in the IA.
//
// When responding to Renew messages, per section 18.3.4:
//
// - If the server is configured to create new bindings as
// a result of processing Renew messages but the server will
// not assign any leases to an IA, the server returns the IA
// option containing a Status Code option with the NoAddrsAvail.
//
// When responding to Rebind messages, per section 18.3.5:
//
// - If the server is configured to create new bindings as a result
// of processing Rebind messages but the server will not assign any
// leases to an IA, the server returns the IA option containing a
// Status Code option (see Section 21.13) with the NoAddrsAvail or
// NoPrefixAvail status code and a status message for a user.
//
// Retry obtaining this IA_NA in subsequent messages.
//
// TODO(https://fxbug.dev/42161502): implement rate limiting.
(
RequestLeasesMessageType::Request
| RequestLeasesMessageType::Renew
| RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::NoAddrsAvail,
IaKind::Address,
) => IaStatusError::Retry { without_hints: false },
// NoAddrsAvail is not expected for prefixes. The equivalent error for
// prefixes is NoPrefixAvail.
(
RequestLeasesMessageType::Request
| RequestLeasesMessageType::Renew
| RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::NoAddrsAvail,
IaKind::Prefix,
) => IaStatusError::Invalid,
// When responding to Request messages, per section 18.3.2:
//
// For any IA_PD option (see Section 21.21) in the Request message to
// which the server cannot assign any delegated prefixes, the server
// MUST return the IA_PD option in the Reply message with no prefixes
// in the IA_PD and with a Status Code option containing status code
// NoPrefixAvail in the IA_PD.
//
// When responding to Renew messages, per section 18.3.4:
//
// - If the server is configured to create new bindings as
// a result of processing Renew messages but the server will
// not assign any leases to an IA, the server returns the IA
// option containing a Status Code option with the NoAddrsAvail
// or NoPrefixAvail status code and a status message for a user.
//
// When responding to Rebind messages, per section 18.3.5:
//
// - If the server is configured to create new bindings as a result
// of processing Rebind messages but the server will not assign any
// leases to an IA, the server returns the IA option containing a
// Status Code option (see Section 21.13) with the NoAddrsAvail or
// NoPrefixAvail status code and a status message for a user.
//
// Retry obtaining this IA_PD in subsequent messages.
//
// TODO(https://fxbug.dev/42161502): implement rate limiting.
(
RequestLeasesMessageType::Request
| RequestLeasesMessageType::Renew
| RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::NoPrefixAvail,
IaKind::Prefix,
) => IaStatusError::Retry { without_hints: false },
(
RequestLeasesMessageType::Request
| RequestLeasesMessageType::Renew
| RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::NoPrefixAvail,
IaKind::Address,
) => IaStatusError::Invalid,
// Per RFC 8415 section 18.2.10.1:
//
// When the client receives a Reply message in response to a Renew or
// Rebind message, the client:
//
// - Sends a Request message to the server that responded if any of
// the IAs in the Reply message contain the NoBinding status code.
// The client places IA options in this message for all IAs. The
// client continues to use other bindings for which the server did
// not return an error.
//
// The client removes the IA not found by the server, and transitions to
// Requesting after processing all the received IAs.
(
RequestLeasesMessageType::Renew | RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::NoBinding,
IaKind::Address | IaKind::Prefix,
) => IaStatusError::Rerequest,
// NoBinding is not expected in Requesting as the Request message is
// asking for a new binding, not attempting to refresh lifetimes for
// an existing binding.
(
RequestLeasesMessageType::Request,
v6::ErrorStatusCode::NoBinding,
IaKind::Address | IaKind::Prefix,
) => IaStatusError::Invalid,
// Per RFC 8415 section 18.2.10,
//
// If the client receives a Reply message with a status code of
// UseMulticast, the client records the receipt of the message and
// sends subsequent messages to the server through the interface on
// which the message was received using multicast. The client resends
// the original message using multicast.
//
// We currently always multicast our messages so we do not expect the
// UseMulticast error.
//
// TODO(https://fxbug.dev/42156704): Do not consider this an invalid error
// when unicasting messages.
(
RequestLeasesMessageType::Request | RequestLeasesMessageType::Renew,
v6::ErrorStatusCode::UseMulticast,
IaKind::Address | IaKind::Prefix,
) => IaStatusError::Invalid,
// Per RFC 8415 section 16,
//
// A server MUST discard any Solicit, Confirm, Rebind, or
// Information-request messages it receives with a Layer 3 unicast
// destination address.
//
// Since we must never unicast Rebind messages, we always multicast them
// so we consider a UseMulticast error invalid.
(
RequestLeasesMessageType::Rebind,
v6::ErrorStatusCode::UseMulticast,
IaKind::Address | IaKind::Prefix,
) => IaStatusError::Invalid,
}
}
// Possible states to move to after processing a Reply containing leases.
#[derive(Debug)]
enum StateAfterReplyWithLeases {
RequestNextServer,
Assigned,
StayRenewingRebinding,
Requesting,
}
#[derive(Debug)]
struct ProcessedReplyWithLeases<I> {
server_id: Vec<u8>,
non_temporary_addresses: HashMap<v6::IAID, AddressEntry<I>>,
delegated_prefixes: HashMap<v6::IAID, PrefixEntry<I>>,
dns_servers: Option<Vec<Ipv6Addr>>,
actions: Vec<Action<I>>,
next_state: StateAfterReplyWithLeases,
}
fn has_no_assigned_ias<V: IaValue, I>(entries: &HashMap<v6::IAID, IaEntry<V, I>>) -> bool {
entries.iter().all(|(_iaid, entry)| match entry {
IaEntry::ToRequest(_) => true,
IaEntry::Assigned(_) => false,
})
}
struct ComputeNewEntriesWithCurrentIasAndReplyResult<V: IaValue, I> {
new_entries: HashMap<v6::IAID, IaEntry<V, I>>,
go_to_requesting: bool,
missing_ias_in_reply: bool,
updates: HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>>,
all_ias_invalidates_at: Option<AllIasInvalidatesAt<I>>,
}
#[derive(Copy, Clone, Eq, Ord, PartialEq, PartialOrd)]
enum AllIasInvalidatesAt<I> {
At(I),
Never,
}
fn compute_new_entries_with_current_ias_and_reply<V: IaValue, I: Instant>(
ia_name: &str,
request_type: RequestLeasesMessageType,
ias_in_reply: HashMap<v6::IAID, IaOption<V>>,
current_entries: &HashMap<v6::IAID, IaEntry<V, I>>,
now: I,
) -> ComputeNewEntriesWithCurrentIasAndReplyResult<V, I> {
let mut go_to_requesting = false;
let mut all_ias_invalidates_at = None;
let mut update_all_ias_invalidates_at = |LifetimesInfo::<I> {
lifetimes:
Lifetimes { valid_lifetime, preferred_lifetime: _ },
updated_at,
}| {
all_ias_invalidates_at = core::cmp::max(
all_ias_invalidates_at,
Some(match valid_lifetime {
v6::NonZeroTimeValue::Finite(lifetime) => AllIasInvalidatesAt::At(
updated_at.add(Duration::from_secs(lifetime.get().into())),
),
v6::NonZeroTimeValue::Infinity => AllIasInvalidatesAt::Never,
}),
);
};
// As per RFC 8415 section 18.2.10.1:
//
// If the Reply was received in response to a Solicit (with a
// Rapid Commit option), Request, Renew, or Rebind message, the
// client updates the information it has recorded about IAs from
// the IA options contained in the Reply message:
//
// ...
//
// - Add any new leases in the IA option to the IA as recorded
// by the client.
//
// - Update lifetimes for any leases in the IA option that the
// client already has recorded in the IA.
//
// - Discard any leases from the IA, as recorded by the client,
// that have a valid lifetime of 0 in the IA Address or IA
// Prefix option.
//
// - Leave unchanged any information about leases the client has
// recorded in the IA but that were not included in the IA from
// the server
let mut updates = HashMap::new();
let mut new_entries = ias_in_reply
.into_iter()
.map(|(iaid, ia)| {
let current_entry = current_entries
.get(&iaid)
.expect("process_options should have caught unrequested IAs");
let (success_status_message, ia_values) = match ia {
IaOption::Success { status_message, t1: _, t2: _, ia_values } => {
(status_message, ia_values)
}
IaOption::Failure(ErrorStatusCode(error_code, msg)) => {
if !msg.is_empty() {
warn!(
"Reply to {}: {} with IAID {:?} status code {:?} message: {}",
request_type, ia_name, iaid, error_code, msg
);
}
let error = process_ia_error_status(request_type, error_code, V::KIND);
let without_hints = match error {
IaStatusError::Retry { without_hints } => without_hints,
IaStatusError::Invalid => {
warn!(
"Reply to {}: received unexpected status code {:?} in {} option with IAID {:?}",
request_type, error_code, ia_name, iaid,
);
false
}
IaStatusError::Rerequest => {
go_to_requesting = true;
false
}
};
// Let bindings know that the previously assigned values
// should no longer be used.
match current_entry {
IaEntry::Assigned(values) => {
assert_matches!(
updates.insert(
iaid,
values
.keys()
.cloned()
.map(|value| (value, IaValueUpdateKind::Removed))
.collect()
),
None
);
},
IaEntry::ToRequest(_) => {},
}
return (iaid, current_entry.to_request(without_hints));
}
};
if let Some(success_status_message) = success_status_message {
if !success_status_message.is_empty() {
info!(
"Reply to {}: {} with IAID {:?} success status code message: {}",
request_type, ia_name, iaid, success_status_message,
);
}
}
// The server has not included an IA Address/Prefix option in the
// IA, keep the previously recorded information,
// per RFC 8415 section 18.2.10.1:
//
// - Leave unchanged any information about leases the client
// has recorded in the IA but that were not included in the
// IA from the server.
//
// The address/prefix remains assigned until the end of its valid
// lifetime, or it is requested later if it was not assigned.
if ia_values.is_empty() {
return (iaid, current_entry.clone());
}
let mut inner_updates = HashMap::new();
let mut ia_values = ia_values
.into_iter()
.filter_map(|(value, lifetimes)| {
match lifetimes {
// Let bindings know about the assigned lease in the
// reply.
Ok(lifetimes) => {
assert_matches!(
inner_updates.insert(
value,
match current_entry {
IaEntry::Assigned(values) => {
if values.contains_key(&value) {
IaValueUpdateKind::UpdatedLifetimes(lifetimes)
} else {
IaValueUpdateKind::Added(lifetimes)
}
},
IaEntry::ToRequest(_) => IaValueUpdateKind::Added(lifetimes),
},
),
None
);
let lifetimes_info = LifetimesInfo { lifetimes, updated_at: now };
update_all_ias_invalidates_at(lifetimes_info);
Some((
value,
lifetimes_info,
))
},
Err(LifetimesError::PreferredLifetimeGreaterThanValidLifetime(Lifetimes {
preferred_lifetime,
valid_lifetime,
})) => {
// As per RFC 8415 section 21.6,
//
// The client MUST discard any addresses for which
// the preferred lifetime is greater than the
// valid lifetime.
//
// As per RFC 8415 section 21.22,
//
// The client MUST discard any prefixes for which
// the preferred lifetime is greater than the
// valid lifetime.
warn!(
"Reply to {}: {} with IAID {:?}: ignoring value={:?} because \
preferred lifetime={:?} greater than valid lifetime={:?}",
request_type, ia_name, iaid, value, preferred_lifetime, valid_lifetime
);
None
},
Err(LifetimesError::ValidLifetimeZero) => {
info!(
"Reply to {}: {} with IAID {:?}: invalidating value={:?} \
with zero lifetime",
request_type, ia_name, iaid, value
);
// Let bindings know when a previously assigned
// value should be immediately invalidated when the
// reply includes it with a zero valid lifetime.
match current_entry {
IaEntry::Assigned(values) => {
if values.contains_key(&value) {
assert_matches!(
inner_updates.insert(
value,
IaValueUpdateKind::Removed,
),
None
);
}
}
IaEntry::ToRequest(_) => {},
}
None
}
}
})
.collect::<HashMap<_, _>>();
// Merge existing values that were not present in the new IA.
match current_entry {
IaEntry::Assigned(values) => {
for (value, lifetimes) in values {
match ia_values.entry(*value) {
// If we got the value in the Reply, do nothing
// further for this value.
Entry::Occupied(_) => {},
// We are missing the value in the new IA.
//
// Either the value is missing from the IA in the
// Reply or the Reply invalidated the value.
Entry::Vacant(e) => match inner_updates.get(value) {
// If we have an update, it MUST be a removal
// since add/lifetime change events should have
// resulted in the value being present in the
// new IA's values.
Some(update) => assert_eq!(update, &IaValueUpdateKind::Removed),
// The Reply is missing this value so we just copy
// it into the new set of values.
None => {
let lifetimes = lifetimes.clone();
update_all_ias_invalidates_at(lifetimes);
let _: &mut LifetimesInfo<_> = e.insert(lifetimes);
}
}
}
}
},
IaEntry::ToRequest(_) => {},
}
assert_matches!(updates.insert(iaid, inner_updates), None);
if ia_values.is_empty() {
(iaid, IaEntry::ToRequest(current_entry.value().collect()))
} else {
// At this point we know the IA will be considered assigned.
//
// Any current values not in the replied IA should be left alone
// as per RFC 8415 section 18.2.10.1:
//
// - Leave unchanged any information about leases the client
// has recorded in the IA but that were not included in the
// IA from the server.
(iaid, IaEntry::Assigned(ia_values))
}
})
.collect::<HashMap<_, _>>();
// Add current entries that were not received in this Reply.
let mut missing_ias_in_reply = false;
for (iaid, entry) in current_entries {
match new_entries.entry(*iaid) {
Entry::Occupied(_) => {
// We got the entry in the Reply, do nothing further for this
// IA.
}
Entry::Vacant(e) => {
// We did not get this entry in the IA.
missing_ias_in_reply = true;
let _: &mut IaEntry<_, _> = e.insert(match entry {
IaEntry::ToRequest(address_to_request) => {
IaEntry::ToRequest(address_to_request.clone())
}
IaEntry::Assigned(ia) => IaEntry::Assigned(ia.clone()),
});
}
}
}
ComputeNewEntriesWithCurrentIasAndReplyResult {
new_entries,
go_to_requesting,
missing_ias_in_reply,
updates,
all_ias_invalidates_at,
}
}
/// An update for an IA value.
#[derive(Debug, PartialEq, Clone)]
pub struct IaValueUpdate<V> {
pub value: V,
pub kind: IaValueUpdateKind,
}
/// An IA Value's update kind.
#[derive(Debug, PartialEq, Clone)]
pub enum IaValueUpdateKind {
Added(Lifetimes),
UpdatedLifetimes(Lifetimes),
Removed,
}
/// An IA update.
#[derive(Debug, PartialEq, Clone)]
pub struct IaUpdate<V> {
pub iaid: v6::IAID,
pub values: Vec<IaValueUpdate<V>>,
}
// Processes a Reply to Solicit (with fast commit), Request, Renew, or Rebind.
//
// If an error is returned, the message should be ignored.
fn process_reply_with_leases<B: ByteSlice, I: Instant>(
client_id: &[u8],
server_id: &[u8],
current_non_temporary_addresses: &HashMap<v6::IAID, AddressEntry<I>>,
current_delegated_prefixes: &HashMap<v6::IAID, PrefixEntry<I>>,
solicit_max_rt: &mut Duration,
msg: &v6::Message<'_, B>,
request_type: RequestLeasesMessageType,
now: I,
) -> Result<ProcessedReplyWithLeases<I>, ReplyWithLeasesError> {
let ProcessedOptions { server_id: got_server_id, solicit_max_rt_opt, result } =
process_options(
&msg,
ExchangeType::ReplyWithLeases(request_type),
Some(client_id),
current_non_temporary_addresses,
current_delegated_prefixes,
)?;
match request_type {
RequestLeasesMessageType::Request | RequestLeasesMessageType::Renew => {
if got_server_id != server_id {
return Err(ReplyWithLeasesError::MismatchedServerId {
got: got_server_id,
want: server_id.to_vec(),
});
}
}
// Accept a message from any server if this is a reply to a rebind
// message.
RequestLeasesMessageType::Rebind => {}
}
// Always update SOL_MAX_RT, per RFC 8415, section 18.2.10:
//
// The client MUST process any SOL_MAX_RT option (see Section 21.24)
// and INF_MAX_RT option (see Section
// 21.25) present in a Reply message, even if the message contains a
// Status Code option indicating a failure.
*solicit_max_rt = solicit_max_rt_opt
.map_or(*solicit_max_rt, |solicit_max_rt| Duration::from_secs(solicit_max_rt.into()));
let Options {
success_status_message,
next_contact_time,
preference: _,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
} = result?;
let (t1, t2) = assert_matches!(
next_contact_time,
NextContactTime::RenewRebind { t1, t2 } => (t1, t2)
);
if let Some(success_status_message) = success_status_message {
if !success_status_message.is_empty() {
info!(
"Reply to {} success status code message: {}",
request_type, success_status_message
);
}
}
let (
non_temporary_addresses,
ia_na_updates,
delegated_prefixes,
ia_pd_updates,
go_to_requesting,
missing_ias_in_reply,
all_ias_invalidates_at,
) = {
let ComputeNewEntriesWithCurrentIasAndReplyResult {
new_entries: non_temporary_addresses,
go_to_requesting: go_to_requesting_iana,
missing_ias_in_reply: missing_ias_in_reply_iana,
updates: ia_na_updates,
all_ias_invalidates_at: all_ia_nas_invalidates_at,
} = compute_new_entries_with_current_ias_and_reply(
IA_NA_NAME,
request_type,
non_temporary_addresses,
current_non_temporary_addresses,
now,
);
let ComputeNewEntriesWithCurrentIasAndReplyResult {
new_entries: delegated_prefixes,
go_to_requesting: go_to_requesting_iapd,
missing_ias_in_reply: missing_ias_in_reply_iapd,
updates: ia_pd_updates,
all_ias_invalidates_at: all_ia_pds_invalidates_at,
} = compute_new_entries_with_current_ias_and_reply(
IA_PD_NAME,
request_type,
delegated_prefixes,
current_delegated_prefixes,
now,
);
(
non_temporary_addresses,
ia_na_updates,
delegated_prefixes,
ia_pd_updates,
go_to_requesting_iana || go_to_requesting_iapd,
missing_ias_in_reply_iana || missing_ias_in_reply_iapd,
core::cmp::max(all_ia_nas_invalidates_at, all_ia_pds_invalidates_at),
)
};
// Per RFC 8415, section 18.2.10.1:
//
// If the Reply message contains any IAs but the client finds no
// usable addresses and/or delegated prefixes in any of these IAs,
// the client may either try another server (perhaps restarting the
// DHCP server discovery process) or use the Information-request
// message to obtain other configuration information only.
//
// If there are no usable addresses/prefixecs and no other servers to
// select, the client restarts server discovery instead of requesting
// configuration information only. This option is preferred when the
// client operates in stateful mode, where the main goal for the client is
// to negotiate addresses/prefixes.
let next_state = if has_no_assigned_ias(&non_temporary_addresses)
&& has_no_assigned_ias(&delegated_prefixes)
{
warn!("Reply to {}: no usable lease returned", request_type);
StateAfterReplyWithLeases::RequestNextServer
} else if go_to_requesting {
StateAfterReplyWithLeases::Requesting
} else {
match request_type {
RequestLeasesMessageType::Request => StateAfterReplyWithLeases::Assigned,
RequestLeasesMessageType::Renew | RequestLeasesMessageType::Rebind => {
if missing_ias_in_reply {
// Stay in Renewing/Rebinding if any of the assigned IAs that the client
// is trying to renew are not included in the Reply, per RFC 8451 section
// 18.2.10.1:
//
// When the client receives a Reply message in response to a Renew or
// Rebind message, the client: [..] Sends a Renew/Rebind if any of
// the IAs are not in the Reply message, but as this likely indicates
// that the server that responded does not support that IA type, sending
// immediately is unlikely to produce a different result. Therefore,
// the client MUST rate-limit its transmissions (see Section 14.1) and
// MAY just wait for the normal retransmission time (as if the Reply
// message had not been received). The client continues to use other
// bindings for which the server did return information.
//
// TODO(https://fxbug.dev/42161502): implement rate limiting.
warn!(
"Reply to {}: allowing retransmit timeout to retry due to missing IA",
request_type
);
StateAfterReplyWithLeases::StayRenewingRebinding
} else {
StateAfterReplyWithLeases::Assigned
}
}
}
};
let actions = match next_state {
StateAfterReplyWithLeases::Assigned => Some(
[
Action::CancelTimer(ClientTimerType::Retransmission),
// Set timer to start renewing addresses, per RFC 8415, section
// 18.2.4:
//
// At time T1, the client initiates a Renew/Reply message
// exchange to extend the lifetimes on any leases in the IA.
//
// Addresses are not renewed if T1 is infinity, per RFC 8415,
// section 7.7:
//
// A client will never attempt to extend the lifetimes of any
// addresses in an IA with T1 set to 0xffffffff.
//
// If the Renew time (T1) is equal to the Rebind time (T2), we
// skip setting the Renew timer.
//
// This is a slight deviation from the RFC which does not
// mention any special-case when `T1 == T2`. We do this here
// so that we can strictly enforce that when a Rebind timer
// fires, no Renew timers exist, preventing a state machine
// from transitioning from `Assigned -> Rebind -> Renew`
// which is clearly wrong as Rebind is only entered _after_
// Renew (when Renewing fails). Per RFC 8415 section 18.2.5,
//
// At time T2 (which will only be reached if the server to
// which the Renew message was sent starting at time T1
// has not responded), the client initiates a Rebind/Reply
// message exchange with any available server.
//
// Note that, the alternative to this is to always schedule
// the Renew and Rebind timers at T1 and T2, respectively,
// but unconditionally cancel the Renew timer when entering
// the Rebind state. This will be _almost_ the same but
// allows for a situation where the state-machine may enter
// Renewing (and send a Renew message) then immedaitely
// transition to Rebinding (and send a Rebind message with a
// new transaction ID). In this situation, the server will
// handle the Renew message and send a Reply but this client
// would be likely to drop that message as the client would
// have almost immediately transitioned to the Rebinding state
// (at which point the transaction ID would have changed).
if t1 == t2 {
Action::CancelTimer(ClientTimerType::Renew)
} else if t1 < t2 {
assert_matches!(
t1,
v6::NonZeroTimeValue::Finite(t1_val) => Action::ScheduleTimer(
ClientTimerType::Renew,
now.add(Duration::from_secs(t1_val.get().into())),
),
"must be Finite since Infinity is the largest possible value so if T1 \
is Infinity, T2 must also be Infinity as T1 must always be less than \
or equal to T2 in which case we would have not reached this point"
)
} else {
unreachable!("should have rejected T1={:?} > T2={:?}", t1, t2);
},
// Per RFC 8415 section 18.2.5, set timer to enter rebind state:
//
// At time T2 (which will only be reached if the server to
// which the Renew message was sent starting at time T1 has
// not responded), the client initiates a Rebind/Reply message
// exchange with any available server.
//
// Per RFC 8415 section 7.7, do not enter the Rebind state if
// T2 is infinity:
//
// A client will never attempt to use a Rebind message to
// locate a different server to extend the lifetimes of any
// addresses in an IA with T2 set to 0xffffffff.
match t2 {
v6::NonZeroTimeValue::Finite(t2_val) => Action::ScheduleTimer(
ClientTimerType::Rebind,
now.add(Duration::from_secs(t2_val.get().into())),
),
v6::NonZeroTimeValue::Infinity => Action::CancelTimer(ClientTimerType::Rebind),
},
]
.into_iter()
.chain(dns_servers.clone().map(Action::UpdateDnsServers)),
),
StateAfterReplyWithLeases::RequestNextServer
| StateAfterReplyWithLeases::StayRenewingRebinding
| StateAfterReplyWithLeases::Requesting => None,
}
.into_iter()
.flatten()
.chain((!ia_na_updates.is_empty()).then(|| Action::IaNaUpdates(ia_na_updates)))
.chain((!ia_pd_updates.is_empty()).then(|| Action::IaPdUpdates(ia_pd_updates)))
.chain(all_ias_invalidates_at.into_iter().map(|all_ias_invalidates_at| {
match all_ias_invalidates_at {
AllIasInvalidatesAt::At(instant) => {
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, instant)
}
AllIasInvalidatesAt::Never => {
Action::CancelTimer(ClientTimerType::RestartServerDiscovery)
}
}
}))
.collect();
Ok(ProcessedReplyWithLeases {
server_id: got_server_id,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
actions,
next_state,
})
}
/// Create a map of IA entries to be requested, combining the IAs in the
/// Advertise with the configured IAs that are not included in the Advertise.
fn advertise_to_ia_entries<V: IaValue, I>(
mut advertised: HashMap<v6::IAID, HashSet<V>>,
configured: HashMap<v6::IAID, HashSet<V>>,
) -> HashMap<v6::IAID, IaEntry<V, I>> {
configured
.into_iter()
.map(|(iaid, configured)| {
let addresses_to_request = match advertised.remove(&iaid) {
Some(ias) => {
// Note that the advertised address/prefix for an IAID may
// be different from what was solicited by the client.
ias
}
// The configured address/prefix was not advertised; the client
// will continue to request it in subsequent messages, per
// RFC 8415 section 18.2:
//
// When possible, the client SHOULD use the best
// configuration available and continue to request the
// additional IAs in subsequent messages.
None => configured,
};
(iaid, IaEntry::ToRequest(addresses_to_request))
})
.collect()
}
impl<I: Instant> Requesting<I> {
/// Starts in requesting state following [RFC 8415, Section 18.2.2].
///
/// [RFC 8415, Section 18.2.2]: https://tools.ietf.org/html/rfc8415#section-18.2.2
fn start<R: Rng>(
client_id: ClientDuid,
server_id: Vec<u8>,
non_temporary_addresses: HashMap<v6::IAID, AddressEntry<I>>,
delegated_prefixes: HashMap<v6::IAID, PrefixEntry<I>>,
options_to_request: &[v6::OptionCode],
collected_advertise: BinaryHeap<AdvertiseMessage<I>>,
solicit_max_rt: Duration,
rng: &mut R,
now: I,
) -> Transition<I> {
Self {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
collected_advertise,
first_request_time: now,
retrans_timeout: Duration::default(),
transmission_count: 0,
solicit_max_rt,
}
.send_and_reschedule_retransmission(
transaction_id(),
options_to_request,
rng,
now,
std::iter::empty(),
)
}
/// Calculates timeout for retransmitting requests using parameters
/// specified in [RFC 8415, Section 18.2.2].
///
/// [RFC 8415, Section 18.2.2]: https://tools.ietf.org/html/rfc8415#section-18.2.2
fn retransmission_timeout<R: Rng>(prev_retrans_timeout: Duration, rng: &mut R) -> Duration {
retransmission_timeout(
prev_retrans_timeout,
INITIAL_REQUEST_TIMEOUT,
MAX_REQUEST_TIMEOUT,
rng,
)
}
/// A helper function that returns a transition to stay in `Requesting`, with
/// actions to cancel current retransmission timer, send a request and
/// schedules retransmission.
fn send_and_reschedule_retransmission<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
initial_actions: impl Iterator<Item = Action<I>>,
) -> Transition<I> {
let Transition { state, actions: request_actions, transaction_id } = self
.send_and_schedule_retransmission(
transaction_id,
options_to_request,
rng,
now,
initial_actions,
);
let actions = std::iter::once(Action::CancelTimer(ClientTimerType::Retransmission))
.chain(request_actions.into_iter())
.collect();
Transition { state, actions, transaction_id }
}
/// A helper function that returns a transition to stay in `Requesting`, with
/// actions to send a request and schedules retransmission.
///
/// # Panics
///
/// Panics if `options_to_request` contains SOLICIT_MAX_RT.
fn send_and_schedule_retransmission<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
initial_actions: impl Iterator<Item = Action<I>>,
) -> Transition<I> {
let Self {
client_id,
server_id,
non_temporary_addresses,
delegated_prefixes,
collected_advertise,
first_request_time,
retrans_timeout: prev_retrans_timeout,
transmission_count,
solicit_max_rt,
} = self;
let retrans_timeout = Self::retransmission_timeout(prev_retrans_timeout, rng);
let elapsed_time = elapsed_time_in_centisecs(first_request_time, now);
// Per RFC 8415, section 18.2.2:
//
// The client uses a Request message to populate IAs with leases and
// obtain other configuration information. The client includes one or
// more IA options in the Request message. The server then returns
// leases and other information about the IAs to the client in IA
// options in a Reply message.
//
// The client sets the "msg-type" field to REQUEST. The client
// generates a transaction ID and inserts this value in the
// "transaction-id" field.
//
// The client MUST include the identifier of the destination server in
// a Server Identifier option (see Section 21.3).
//
// The client MUST include a Client Identifier option (see Section
// 21.2) to identify itself to the server. The client adds any other
// appropriate options, including one or more IA options.
//
// The client MUST include an Elapsed Time option (see Section 21.9)
// to indicate how long the client has been trying to complete the
// current DHCP message exchange.
//
// The client MUST include an Option Request option (see Section 21.7)
// to request the SOL_MAX_RT option (see Section 21.24) and any other
// options the client is interested in receiving. The client MAY
// additionally include instances of those options that are identified
// in the Option Request option, with data values as hints to the
// server about parameter values the client would like to have
// returned.
let buf = StatefulMessageBuilder {
transaction_id,
message_type: v6::MessageType::Request,
server_id: Some(&server_id),
client_id: &client_id,
elapsed_time_in_centisecs: elapsed_time,
options_to_request,
ia_nas: non_temporary_addresses.iter().map(|(iaid, ia)| (*iaid, ia.value())),
ia_pds: delegated_prefixes.iter().map(|(iaid, ia)| (*iaid, ia.value())),
_marker: Default::default(),
}
.build();
Transition {
state: ClientState::Requesting(Requesting {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
collected_advertise,
first_request_time,
retrans_timeout,
transmission_count: transmission_count + 1,
solicit_max_rt,
}),
actions: initial_actions
.chain([
Action::SendMessage(buf),
Action::ScheduleTimer(
ClientTimerType::Retransmission,
now.add(retrans_timeout),
),
])
.collect(),
transaction_id: Some(transaction_id),
}
}
/// Retransmits request. Per RFC 8415, section 18.2.2:
///
/// The client transmits the message according to Section 15, using the
/// following parameters:
///
/// IRT REQ_TIMEOUT
/// MRT REQ_MAX_RT
/// MRC REQ_MAX_RC
/// MRD 0
///
/// Per RFC 8415, section 15:
///
/// MRC specifies an upper bound on the number of times a client may
/// retransmit a message. Unless MRC is zero, the message exchange fails
/// once the client has transmitted the message MRC times.
///
/// Per RFC 8415, section 18.2.2:
///
/// If the message exchange fails, the client takes an action based on
/// the client's local policy. Examples of actions the client might take
/// include the following:
/// - Select another server from a list of servers known to the client
/// -- for example, servers that responded with an Advertise message.
/// - Initiate the server discovery process described in Section 18.
/// - Terminate the configuration process and report failure.
///
/// The client's policy on message exchange failure is to select another
/// server; if there are no more servers available, restart server
/// discovery.
/// TODO(https://fxbug.dev/42169314): make the client policy configurable.
fn retransmission_timer_expired<R: Rng>(
self,
request_transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
collected_advertise: _,
first_request_time: _,
retrans_timeout: _,
transmission_count,
solicit_max_rt: _,
} = &self;
if *transmission_count > REQUEST_MAX_RC {
self.request_from_alternate_server_or_restart_server_discovery(
options_to_request,
rng,
now,
)
} else {
self.send_and_schedule_retransmission(
request_transaction_id,
options_to_request,
rng,
now,
std::iter::empty(),
)
}
}
fn reply_message_received<R: Rng, B: ByteSlice>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
msg: v6::Message<'_, B>,
now: I,
) -> Transition<I> {
let Self {
client_id,
non_temporary_addresses: mut current_non_temporary_addresses,
delegated_prefixes: mut current_delegated_prefixes,
server_id,
collected_advertise,
first_request_time,
retrans_timeout,
transmission_count,
mut solicit_max_rt,
} = self;
let ProcessedReplyWithLeases {
server_id: got_server_id,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
actions,
next_state,
} = match process_reply_with_leases(
&client_id,
&server_id,
&current_non_temporary_addresses,
&current_delegated_prefixes,
&mut solicit_max_rt,
&msg,
RequestLeasesMessageType::Request,
now,
) {
Ok(processed) => processed,
Err(e) => {
match e {
ReplyWithLeasesError::ErrorStatusCode(ErrorStatusCode(error_code, message)) => {
match error_code {
v6::ErrorStatusCode::NotOnLink => {
// Per RFC 8415, section 18.2.10.1:
//
// If the client receives a NotOnLink status from the server
// in response to a Solicit (with a Rapid Commit option;
// see Section 21.14) or a Request, the client can either
// reissue the message without specifying any addresses or
// restart the DHCP server discovery process (see Section 18).
//
// The client reissues the message without specifying addresses,
// leaving it up to the server to assign addresses appropriate
// for the client's link.
fn get_updates_and_reset_to_empty_request<V: IaValue, I>(
current: &mut HashMap<v6::IAID, IaEntry<V, I>>,
) -> HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>>
{
let mut updates = HashMap::new();
current.iter_mut().for_each(|(iaid, entry)| {
// Discard all currently-assigned values.
match entry {
IaEntry::Assigned(values) => {
assert_matches!(
updates.insert(
*iaid,
values
.keys()
.cloned()
.map(|value| (
value,
IaValueUpdateKind::Removed
),)
.collect()
),
None
);
}
IaEntry::ToRequest(_) => {}
};
*entry = IaEntry::ToRequest(Default::default());
});
updates
}
let ia_na_updates = get_updates_and_reset_to_empty_request(
&mut current_non_temporary_addresses,
);
let ia_pd_updates = get_updates_and_reset_to_empty_request(
&mut current_delegated_prefixes,
);
let initial_actions = (!ia_na_updates.is_empty())
.then(|| Action::IaNaUpdates(ia_na_updates))
.into_iter()
.chain(
(!ia_pd_updates.is_empty())
.then(|| Action::IaPdUpdates(ia_pd_updates)),
);
warn!(
"Reply to Request: retrying Request without hints due to \
NotOnLink error status code with message '{}'",
message,
);
return Requesting {
client_id,
non_temporary_addresses: current_non_temporary_addresses,
delegated_prefixes: current_delegated_prefixes,
server_id,
collected_advertise,
first_request_time,
retrans_timeout,
transmission_count,
solicit_max_rt,
}
.send_and_reschedule_retransmission(
*msg.transaction_id(),
options_to_request,
rng,
now,
initial_actions,
);
}
// Per RFC 8415, section 18.2.10:
//
// If the client receives a Reply message with a status code
// of UnspecFail, the server is indicating that it was unable
// to process the client's message due to an unspecified
// failure condition. If the client retransmits the original
// message to the same server to retry the desired operation,
// the client MUST limit the rate at which it retransmits
// the message and limit the duration of the time during
// which it retransmits the message (see Section 14.1).
//
// Ignore this Reply and rely on timeout for retransmission.
// TODO(https://fxbug.dev/42161502): implement rate limiting.
v6::ErrorStatusCode::UnspecFail => {
warn!(
"ignoring Reply to Request: ignoring due to UnspecFail error
status code with message '{}'",
message,
);
}
// TODO(https://fxbug.dev/42156704): implement unicast.
// The client already uses multicast.
v6::ErrorStatusCode::UseMulticast => {
warn!(
"ignoring Reply to Request: ignoring due to UseMulticast \
with message '{}', but Request was already using multicast",
message,
);
}
// Not expected as top level status.
v6::ErrorStatusCode::NoAddrsAvail
| v6::ErrorStatusCode::NoPrefixAvail
| v6::ErrorStatusCode::NoBinding => {
warn!(
"ignoring Reply to Request due to unexpected top level error
{:?} with message '{}'",
error_code, message,
);
}
}
return Transition {
state: ClientState::Requesting(Self {
client_id,
non_temporary_addresses: current_non_temporary_addresses,
delegated_prefixes: current_delegated_prefixes,
server_id,
collected_advertise,
first_request_time,
retrans_timeout,
transmission_count,
solicit_max_rt,
}),
actions: Vec::new(),
transaction_id: None,
};
}
_ => {}
}
warn!("ignoring Reply to Request: {:?}", e);
return Transition {
state: ClientState::Requesting(Self {
client_id,
non_temporary_addresses: current_non_temporary_addresses,
delegated_prefixes: current_delegated_prefixes,
server_id,
collected_advertise,
first_request_time,
retrans_timeout,
transmission_count,
solicit_max_rt,
}),
actions: Vec::new(),
transaction_id: None,
};
}
};
assert_eq!(
server_id, got_server_id,
"should be invalid to accept a reply to Request with mismatched server ID"
);
match next_state {
StateAfterReplyWithLeases::StayRenewingRebinding => {
unreachable!("cannot stay in Renewing/Rebinding state while in Requesting state");
}
StateAfterReplyWithLeases::Requesting => {
unreachable!(
"cannot go back to Requesting from Requesting \
(only possible from Renewing/Rebinding)"
);
}
StateAfterReplyWithLeases::RequestNextServer => {
warn!("Reply to Request: trying next server");
Self {
client_id,
non_temporary_addresses: current_non_temporary_addresses,
delegated_prefixes: current_delegated_prefixes,
server_id,
collected_advertise,
first_request_time,
retrans_timeout,
transmission_count,
solicit_max_rt,
}
.request_from_alternate_server_or_restart_server_discovery(
options_to_request,
rng,
now,
)
}
StateAfterReplyWithLeases::Assigned => {
// Note that we drop the list of collected advertisements when
// we transition to Assigned to avoid picking servers using
// stale advertisements if we ever need to pick a new server in
// the future.
//
// Once we transition into the Assigned state, we will not
// attempt to communicate with a different server for some time
// before an error occurs that requires the client to pick an
// alternate server. In this time, the set of advertisements may
// have gone stale as the server may have assigned advertised
// IAs to some other client.
//
// TODO(https://fxbug.dev/42152192) Send AddressWatcher update with
// assigned addresses.
Transition {
state: ClientState::Assigned(Assigned {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers: dns_servers.unwrap_or(Vec::new()),
solicit_max_rt,
_marker: Default::default(),
}),
actions,
transaction_id: None,
}
}
}
}
fn restart_server_discovery<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id: _,
collected_advertise: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
} = self;
restart_server_discovery(
client_id,
non_temporary_addresses,
delegated_prefixes,
Vec::new(),
options_to_request,
rng,
now,
)
}
/// Helper function to send a request to an alternate server, or if there are no
/// other collected servers, restart server discovery.
///
/// The client removes currently assigned addresses, per RFC 8415, section
/// 18.2.10.1:
///
/// Whenever a client restarts the DHCP server discovery process or
/// selects an alternate server as described in Section 18.2.9, the client
/// SHOULD stop using all the addresses and delegated prefixes for which
/// it has bindings and try to obtain all required leases from the new
/// server.
fn request_from_alternate_server_or_restart_server_discovery<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self {
client_id,
server_id: _,
non_temporary_addresses,
delegated_prefixes,
mut collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt,
} = self;
if let Some(advertise) = collected_advertise.pop() {
fn to_configured_values<V: IaValue, I: Instant>(
entries: HashMap<v6::IAID, IaEntry<V, I>>,
) -> HashMap<v6::IAID, HashSet<V>> {
entries
.into_iter()
.map(|(iaid, entry)| {
(
iaid,
match entry {
IaEntry::Assigned(values) => unreachable!(
"should not have advertisements after an IA was assigned; \
iaid={:?}, values={:?}",
iaid, values,
),
IaEntry::ToRequest(values) => values,
},
)
})
.collect()
}
let configured_non_temporary_addresses = to_configured_values(non_temporary_addresses);
let configured_delegated_prefixes = to_configured_values(delegated_prefixes);
// TODO(https://fxbug.dev/42178817): Before selecting a different server,
// add actions to remove the existing assigned addresses, if any.
let AdvertiseMessage {
server_id,
non_temporary_addresses: advertised_non_temporary_addresses,
delegated_prefixes: advertised_delegated_prefixes,
dns_servers: _,
preference: _,
receive_time: _,
preferred_non_temporary_addresses_count: _,
preferred_delegated_prefixes_count: _,
} = advertise;
Requesting::start(
client_id,
server_id,
advertise_to_ia_entries(
advertised_non_temporary_addresses,
configured_non_temporary_addresses,
),
advertise_to_ia_entries(
advertised_delegated_prefixes,
configured_delegated_prefixes,
),
options_to_request,
collected_advertise,
solicit_max_rt,
rng,
now,
)
} else {
restart_server_discovery(
client_id,
non_temporary_addresses,
delegated_prefixes,
Vec::new(), /* dns_servers */
options_to_request,
rng,
now,
)
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
struct LifetimesInfo<I> {
lifetimes: Lifetimes,
updated_at: I,
}
#[derive(Debug, PartialEq, Clone)]
enum IaEntry<V: IaValue, I> {
/// The IA is assigned.
Assigned(HashMap<V, LifetimesInfo<I>>),
/// The IA is not assigned, and is to be requested in subsequent
/// messages.
ToRequest(HashSet<V>),
}
impl<V: IaValue, I> IaEntry<V, I> {
fn value(&self) -> impl Iterator<Item = V> + '_ {
match self {
Self::Assigned(ias) => either::Either::Left(ias.keys().copied()),
Self::ToRequest(values) => either::Either::Right(values.iter().copied()),
}
}
fn to_request(&self, without_hints: bool) -> Self {
Self::ToRequest(if without_hints { Default::default() } else { self.value().collect() })
}
}
type AddressEntry<I> = IaEntry<Ipv6Addr, I>;
type PrefixEntry<I> = IaEntry<Subnet<Ipv6Addr>, I>;
/// Provides methods for handling state transitions from Assigned state.
#[derive(Debug)]
struct Assigned<I> {
/// [Client Identifier] used for uniquely identifying the client in
/// communication with servers.
///
/// [Client Identifier]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.2
client_id: ClientDuid,
/// The non-temporary addresses negotiated by the client.
non_temporary_addresses: HashMap<v6::IAID, AddressEntry<I>>,
/// The delegated prefixes negotiated by the client.
delegated_prefixes: HashMap<v6::IAID, PrefixEntry<I>>,
/// The [server identifier] of the server to which the client sends
/// requests.
///
/// [Server Identifier]: https://datatracker.ietf.org/doc/html/rfc8415#section-21.3
server_id: Vec<u8>,
/// Stores the DNS servers received from the reply.
dns_servers: Vec<Ipv6Addr>,
/// The [SOL_MAX_RT](https://datatracker.ietf.org/doc/html/rfc8415#section-21.24)
/// used by the client.
solicit_max_rt: Duration,
_marker: PhantomData<I>,
}
fn restart_server_discovery<R: Rng, I: Instant>(
client_id: ClientDuid,
non_temporary_addresses: HashMap<v6::IAID, AddressEntry<I>>,
delegated_prefixes: HashMap<v6::IAID, PrefixEntry<I>>,
dns_servers: Vec<Ipv6Addr>,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
#[derive(Derivative)]
#[derivative(Default(bound = ""))]
struct ClearValuesResult<V: IaValue> {
updates: HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>>,
entries: HashMap<v6::IAID, HashSet<V>>,
}
fn clear_values<V: IaValue, I: Instant>(
values: HashMap<v6::IAID, IaEntry<V, I>>,
) -> ClearValuesResult<V> {
values.into_iter().fold(
ClearValuesResult::default(),
|ClearValuesResult { mut updates, mut entries }, (iaid, entry)| {
match entry {
IaEntry::Assigned(values) => {
assert_matches!(
updates.insert(
iaid,
values
.keys()
.copied()
.map(|value| (value, IaValueUpdateKind::Removed))
.collect()
),
None
);
assert_matches!(entries.insert(iaid, values.into_keys().collect()), None);
}
IaEntry::ToRequest(values) => {
assert_matches!(entries.insert(iaid, values), None);
}
}
ClearValuesResult { updates, entries }
},
)
}
let ClearValuesResult {
updates: non_temporary_address_updates,
entries: non_temporary_address_entries,
} = clear_values(non_temporary_addresses);
let ClearValuesResult { updates: delegated_prefix_updates, entries: delegated_prefix_entries } =
clear_values(delegated_prefixes);
ServerDiscovery::start(
transaction_id(),
client_id,
non_temporary_address_entries,
delegated_prefix_entries,
&options_to_request,
MAX_SOLICIT_TIMEOUT,
rng,
now,
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::CancelTimer(ClientTimerType::Refresh),
Action::CancelTimer(ClientTimerType::Renew),
Action::CancelTimer(ClientTimerType::Rebind),
Action::CancelTimer(ClientTimerType::RestartServerDiscovery),
]
.into_iter()
.chain((!dns_servers.is_empty()).then(|| Action::UpdateDnsServers(Vec::new())))
.chain(
(!non_temporary_address_updates.is_empty())
.then(|| Action::IaNaUpdates(non_temporary_address_updates)),
)
.chain(
(!delegated_prefix_updates.is_empty())
.then(|| Action::IaPdUpdates(delegated_prefix_updates)),
),
)
}
impl<I: Instant> Assigned<I> {
/// Handles renew timer, following [RFC 8415, Section 18.2.4].
///
/// [RFC 8415, Section 18.2.4]: https://tools.ietf.org/html/rfc8415#section-18.2.4
fn renew_timer_expired<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
solicit_max_rt,
_marker,
} = self;
// Start renewing bindings, per RFC 8415, section 18.2.4:
//
// At time T1, the client initiates a Renew/Reply message
// exchange to extend the lifetimes on any leases in the IA.
Renewing::start(
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
options_to_request,
dns_servers,
solicit_max_rt,
rng,
now,
)
}
/// Handles rebind timer, following [RFC 8415, Section 18.2.5].
///
/// [RFC 8415, Section 18.2.5]: https://tools.ietf.org/html/rfc8415#section-18.2.5
fn rebind_timer_expired<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
solicit_max_rt,
_marker,
} = self;
// Start rebinding bindings, per RFC 8415, section 18.2.5:
//
// At time T2 (which will only be reached if the server to which the
// Renew message was sent starting at time T1 has not responded), the
// client initiates a Rebind/Reply message exchange with any available
// server.
Rebinding::start(
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
options_to_request,
dns_servers,
solicit_max_rt,
rng,
now,
)
}
fn restart_server_discovery<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id: _,
dns_servers,
solicit_max_rt: _,
_marker,
} = self;
restart_server_discovery(
client_id,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
options_to_request,
rng,
now,
)
}
}
type Renewing<I> = RenewingOrRebinding<I, false /* IS_REBINDING */>;
type Rebinding<I> = RenewingOrRebinding<I, true /* IS_REBINDING */>;
impl<I: Instant> Renewing<I> {
/// Handles rebind timer, following [RFC 8415, Section 18.2.5].
///
/// [RFC 8415, Section 18.2.5]: https://tools.ietf.org/html/rfc8415#section-18.2.4
fn rebind_timer_expired<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self(RenewingOrRebindingInner {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
start_time: _,
retrans_timeout: _,
solicit_max_rt,
}) = self;
// Start rebinding, per RFC 8415, section 18.2.5:
//
// At time T2 (which will only be reached if the server to which the
// Renew message was sent starting at time T1 has not responded), the
// client initiates a Rebind/Reply message exchange with any available
// server.
Rebinding::start(
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
options_to_request,
dns_servers,
solicit_max_rt,
rng,
now,
)
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Clone))]
struct RenewingOrRebindingInner<I> {
/// [Client Identifier](https://datatracker.ietf.org/doc/html/rfc8415#section-21.2)
/// used for uniquely identifying the client in communication with servers.
client_id: ClientDuid,
/// The non-temporary addresses negotiated by the client.
non_temporary_addresses: HashMap<v6::IAID, AddressEntry<I>>,
/// The delegated prefixes negotiated by the client.
delegated_prefixes: HashMap<v6::IAID, PrefixEntry<I>>,
/// [Server Identifier](https://datatracker.ietf.org/doc/html/rfc8415#section-21.2)
/// of the server selected during server discovery.
server_id: Vec<u8>,
/// Stores the DNS servers received from the reply.
dns_servers: Vec<Ipv6Addr>,
/// The time of the first renew/rebind. Used in calculating the
/// [elapsed time].
///
/// [elapsed time](https://datatracker.ietf.org/doc/html/rfc8415#section-21.9).
start_time: I,
/// The renew/rebind message retransmission timeout.
retrans_timeout: Duration,
/// The [SOL_MAX_RT](https://datatracker.ietf.org/doc/html/rfc8415#section-21.24)
/// used by the client.
solicit_max_rt: Duration,
}
impl<I, const IS_REBINDING: bool> From<RenewingOrRebindingInner<I>>
for RenewingOrRebinding<I, IS_REBINDING>
{
fn from(inner: RenewingOrRebindingInner<I>) -> Self {
Self(inner)
}
}
// TODO(https://github.com/rust-lang/rust/issues/76560): Use an enum for the
// constant generic instead of a boolean for readability.
#[derive(Debug)]
struct RenewingOrRebinding<I, const IS_REBINDING: bool>(RenewingOrRebindingInner<I>);
impl<I: Instant, const IS_REBINDING: bool> RenewingOrRebinding<I, IS_REBINDING> {
/// Starts renewing or rebinding, following [RFC 8415, Section 18.2.4] or
/// [RFC 8415, Section 18.2.5], respectively.
///
/// [RFC 8415, Section 18.2.4]: https://tools.ietf.org/html/rfc8415#section-18.2.4
/// [RFC 8415, Section 18.2.5]: https://tools.ietf.org/html/rfc8415#section-18.2.5
fn start<R: Rng>(
client_id: ClientDuid,
non_temporary_addresses: HashMap<v6::IAID, AddressEntry<I>>,
delegated_prefixes: HashMap<v6::IAID, PrefixEntry<I>>,
server_id: Vec<u8>,
options_to_request: &[v6::OptionCode],
dns_servers: Vec<Ipv6Addr>,
solicit_max_rt: Duration,
rng: &mut R,
now: I,
) -> Transition<I> {
Self(RenewingOrRebindingInner {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
start_time: now,
retrans_timeout: Duration::default(),
solicit_max_rt,
})
.send_and_schedule_retransmission(transaction_id(), options_to_request, rng, now)
}
/// Calculates timeout for retransmitting Renew/Rebind using parameters
/// specified in [RFC 8415, Section 18.2.4]/[RFC 8415, Section 18.2.5].
///
/// [RFC 8415, Section 18.2.4]: https://tools.ietf.org/html/rfc8415#section-18.2.4
/// [RFC 8415, Section 18.2.5]: https://tools.ietf.org/html/rfc8415#section-18.2.5
fn retransmission_timeout<R: Rng>(prev_retrans_timeout: Duration, rng: &mut R) -> Duration {
let (initial, max) = if IS_REBINDING {
(INITIAL_REBIND_TIMEOUT, MAX_REBIND_TIMEOUT)
} else {
(INITIAL_RENEW_TIMEOUT, MAX_RENEW_TIMEOUT)
};
retransmission_timeout(prev_retrans_timeout, initial, max, rng)
}
/// Returns a transition to stay in the current state, with actions to send
/// a message and schedule retransmission.
fn send_and_schedule_retransmission<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self(RenewingOrRebindingInner {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
start_time,
retrans_timeout: prev_retrans_timeout,
solicit_max_rt,
}) = self;
let elapsed_time = elapsed_time_in_centisecs(start_time, now);
// As per RFC 8415 section 18.2.4,
//
// The client sets the "msg-type" field to RENEW. The client generates
// a transaction ID and inserts this value in the "transaction-id"
// field.
//
// The client MUST include a Server Identifier option (see Section
// 21.3) in the Renew message, identifying the server with which the
// client most recently communicated.
//
// The client MUST include a Client Identifier option (see Section
// 21.2) to identify itself to the server. The client adds any
// appropriate options, including one or more IA options.
//
// The client MUST include an Elapsed Time option (see Section 21.9)
// to indicate how long the client has been trying to complete the
// current DHCP message exchange.
//
// For IAs to which leases have been assigned, the client includes a
// corresponding IA option containing an IA Address option for each
// address assigned to the IA and an IA Prefix option for each prefix
// assigned to the IA. The client MUST NOT include addresses and
// prefixes in any IA option that the client did not obtain from the
// server or that are no longer valid (that have a valid lifetime of
// 0).
//
// The client MAY include an IA option for each binding it desires but
// has been unable to obtain. In this case, if the client includes the
// IA_PD option to request prefix delegation, the client MAY include
// the IA Prefix option encapsulated within the IA_PD option, with the
// "IPv6-prefix" field set to 0 and the "prefix-length" field set to
// the desired length of the prefix to be delegated. The server MAY
// use this value as a hint for the prefix length. The client SHOULD
// NOT include an IA Prefix option with the "IPv6-prefix" field set to
// 0 unless it is supplying a hint for the prefix length.
//
// The client includes an Option Request option (see Section 21.7) to
// request the SOL_MAX_RT option (see Section 21.24) and any other
// options the client is interested in receiving. The client MAY
// include options with data values as hints to the server about
// parameter values the client would like to have returned.
//
// As per RFC 8415 section 18.2.5,
//
// The client constructs the Rebind message as described in Section
// 18.2.4, with the following differences:
//
// - The client sets the "msg-type" field to REBIND.
//
// - The client does not include the Server Identifier option (see
// Section 21.3) in the Rebind message.
let (message_type, maybe_server_id) = if IS_REBINDING {
(v6::MessageType::Rebind, None)
} else {
(v6::MessageType::Renew, Some(server_id.as_slice()))
};
let buf = StatefulMessageBuilder {
transaction_id,
message_type,
client_id: &client_id,
server_id: maybe_server_id,
elapsed_time_in_centisecs: elapsed_time,
options_to_request,
ia_nas: non_temporary_addresses.iter().map(|(iaid, ia)| (*iaid, ia.value())),
ia_pds: delegated_prefixes.iter().map(|(iaid, ia)| (*iaid, ia.value())),
_marker: Default::default(),
}
.build();
let retrans_timeout = Self::retransmission_timeout(prev_retrans_timeout, rng);
Transition {
state: {
let inner = RenewingOrRebindingInner {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
start_time,
retrans_timeout,
solicit_max_rt,
};
if IS_REBINDING {
ClientState::Rebinding(inner.into())
} else {
ClientState::Renewing(inner.into())
}
},
actions: vec![
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, now.add(retrans_timeout)),
],
transaction_id: Some(transaction_id),
}
}
/// Retransmits the renew or rebind message.
fn retransmission_timer_expired<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
self.send_and_schedule_retransmission(transaction_id, options_to_request, rng, now)
}
fn reply_message_received<R: Rng, B: ByteSlice>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
msg: v6::Message<'_, B>,
now: I,
) -> Transition<I> {
let Self(RenewingOrRebindingInner {
client_id,
non_temporary_addresses: current_non_temporary_addresses,
delegated_prefixes: current_delegated_prefixes,
server_id,
dns_servers: current_dns_servers,
start_time,
retrans_timeout,
mut solicit_max_rt,
}) = self;
let ProcessedReplyWithLeases {
server_id: got_server_id,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
actions,
next_state,
} = match process_reply_with_leases(
&client_id,
&server_id,
&current_non_temporary_addresses,
&current_delegated_prefixes,
&mut solicit_max_rt,
&msg,
if IS_REBINDING {
RequestLeasesMessageType::Rebind
} else {
RequestLeasesMessageType::Renew
},
now,
) {
Ok(processed) => processed,
Err(e) => {
match e {
// Per RFC 8415, section 18.2.10:
//
// If the client receives a Reply message with a status code of
// UnspecFail, the server is indicating that it was unable to process
// the client's message due to an unspecified failure condition. If
// the client retransmits the original message to the same server to
// retry the desired operation, the client MUST limit the rate at
// which it retransmits the message and limit the duration of the
// time during which it retransmits the message (see Section 14.1).
//
// TODO(https://fxbug.dev/42161502): implement rate limiting. Without
// rate limiting support, the client relies on the regular
// retransmission mechanism to rate limit retransmission.
// Similarly, for other status codes indicating failure that are not
// expected in Reply to Renew, the client behaves as if the Reply
// message had not been received. Note the RFC does not specify what
// to do in this case; the client ignores the Reply in order to
// preserve existing bindings.
ReplyWithLeasesError::ErrorStatusCode(ErrorStatusCode(
v6::ErrorStatusCode::UnspecFail,
message,
)) => {
warn!(
"ignoring Reply to Renew with status code UnspecFail \
and message '{}'",
message
);
}
ReplyWithLeasesError::ErrorStatusCode(ErrorStatusCode(
v6::ErrorStatusCode::UseMulticast,
message,
)) => {
// TODO(https://fxbug.dev/42156704): Implement unicast.
warn!(
"ignoring Reply to Renew with status code UseMulticast \
and message '{}' as Reply was already sent as multicast",
message
);
}
ReplyWithLeasesError::ErrorStatusCode(ErrorStatusCode(
error_code @ (v6::ErrorStatusCode::NoAddrsAvail
| v6::ErrorStatusCode::NoBinding
| v6::ErrorStatusCode::NotOnLink
| v6::ErrorStatusCode::NoPrefixAvail),
message,
)) => {
warn!(
"ignoring Reply to Renew with unexpected status code {:?} \
and message '{}'",
error_code, message
);
}
e @ (ReplyWithLeasesError::OptionsError(_)
| ReplyWithLeasesError::MismatchedServerId { got: _, want: _ }) => {
warn!("ignoring Reply to Renew: {}", e);
}
}
return Transition {
state: {
let inner = RenewingOrRebindingInner {
client_id,
non_temporary_addresses: current_non_temporary_addresses,
delegated_prefixes: current_delegated_prefixes,
server_id,
dns_servers: current_dns_servers,
start_time,
retrans_timeout,
solicit_max_rt,
};
if IS_REBINDING {
ClientState::Rebinding(inner.into())
} else {
ClientState::Renewing(inner.into())
}
},
actions: Vec::new(),
transaction_id: None,
};
}
};
if !IS_REBINDING {
assert_eq!(
server_id, got_server_id,
"should be invalid to accept a reply to Renew with mismatched server ID"
);
} else if server_id != got_server_id {
warn!(
"using Reply to Rebind message from different server; current={:?}, new={:?}",
server_id, got_server_id
);
}
let server_id = got_server_id;
match next_state {
// We need to restart server discovery to pick the next server when
// we are in the Renewing/Rebinding state. Unlike Requesting (which
// holds collected advertisements obtained during Server Discovery),
// we do not know about any other servers. Note that all collected
// advertisements are dropped when we transition from Requesting to
// Assigned.
StateAfterReplyWithLeases::RequestNextServer => restart_server_discovery(
client_id,
current_non_temporary_addresses,
current_delegated_prefixes,
current_dns_servers,
&options_to_request,
rng,
now,
),
StateAfterReplyWithLeases::StayRenewingRebinding => Transition {
state: {
let inner = RenewingOrRebindingInner {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers: dns_servers.unwrap_or_else(|| Vec::new()),
start_time,
retrans_timeout,
solicit_max_rt,
};
if IS_REBINDING {
ClientState::Rebinding(inner.into())
} else {
ClientState::Renewing(inner.into())
}
},
actions: Vec::new(),
transaction_id: None,
},
StateAfterReplyWithLeases::Assigned => {
// TODO(https://fxbug.dev/42152192) Send AddressWatcher update with
// assigned addresses.
Transition {
state: ClientState::Assigned(Assigned {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers: dns_servers.unwrap_or_else(|| Vec::new()),
solicit_max_rt,
_marker: Default::default(),
}),
actions,
transaction_id: None,
}
}
StateAfterReplyWithLeases::Requesting => Requesting::start(
client_id,
server_id,
non_temporary_addresses,
delegated_prefixes,
&options_to_request,
Default::default(), /* collected_advertise */
solicit_max_rt,
rng,
now,
),
}
}
fn restart_server_discovery<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
let Self(RenewingOrRebindingInner {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id: _,
dns_servers,
start_time: _,
retrans_timeout: _,
solicit_max_rt: _,
}) = self;
restart_server_discovery(
client_id,
non_temporary_addresses,
delegated_prefixes,
dns_servers,
options_to_request,
rng,
now,
)
}
}
/// All possible states of a DHCPv6 client.
///
/// States not found in this enum are not supported yet.
#[derive(Debug)]
enum ClientState<I> {
/// Creating and (re)transmitting an information request, and waiting for
/// a reply.
InformationRequesting(InformationRequesting<I>),
/// Client is waiting to refresh, after receiving a valid reply to a
/// previous information request.
InformationReceived(InformationReceived<I>),
/// Sending solicit messages, collecting advertise messages, and selecting
/// a server from which to obtain addresses and other optional
/// configuration information.
ServerDiscovery(ServerDiscovery<I>),
/// Creating and (re)transmitting a request message, and waiting for a
/// reply.
Requesting(Requesting<I>),
/// Client is waiting to renew, after receiving a valid reply to a previous request.
Assigned(Assigned<I>),
/// Creating and (re)transmitting a renew message, and awaiting reply.
Renewing(Renewing<I>),
/// Creating and (re)transmitting a rebind message, and awaiting reply.
Rebinding(Rebinding<I>),
}
/// State transition, containing the next state, and the actions the client
/// should take to transition to that state, and the new transaction ID if it
/// has been updated.
struct Transition<I> {
state: ClientState<I>,
actions: Actions<I>,
transaction_id: Option<[u8; 3]>,
}
impl<I: Instant> ClientState<I> {
/// Handles a received advertise message.
fn advertise_message_received<R: Rng, B: ByteSlice>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
msg: v6::Message<'_, B>,
now: I,
) -> Transition<I> {
match self {
ClientState::ServerDiscovery(s) => {
s.advertise_message_received(options_to_request, rng, msg, now)
}
ClientState::InformationRequesting(_)
| ClientState::InformationReceived(_)
| ClientState::Requesting(_)
| ClientState::Assigned(_)
| ClientState::Renewing(_)
| ClientState::Rebinding(_) => {
Transition { state: self, actions: vec![], transaction_id: None }
}
}
}
/// Handles a received reply message.
fn reply_message_received<R: Rng, B: ByteSlice>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
msg: v6::Message<'_, B>,
now: I,
) -> Transition<I> {
match self {
ClientState::InformationRequesting(s) => s.reply_message_received(msg, now),
ClientState::Requesting(s) => {
s.reply_message_received(options_to_request, rng, msg, now)
}
ClientState::Renewing(s) => s.reply_message_received(options_to_request, rng, msg, now),
ClientState::Rebinding(s) => {
s.reply_message_received(options_to_request, rng, msg, now)
}
ClientState::InformationReceived(_)
| ClientState::ServerDiscovery(_)
| ClientState::Assigned(_) => {
Transition { state: self, actions: vec![], transaction_id: None }
}
}
}
/// Handles retransmission timeout.
fn retransmission_timer_expired<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
match self {
ClientState::InformationRequesting(s) => {
s.retransmission_timer_expired(transaction_id, options_to_request, rng, now)
}
ClientState::ServerDiscovery(s) => {
s.retransmission_timer_expired(transaction_id, options_to_request, rng, now)
}
ClientState::Requesting(s) => {
s.retransmission_timer_expired(transaction_id, options_to_request, rng, now)
}
ClientState::Renewing(s) => {
s.retransmission_timer_expired(transaction_id, options_to_request, rng, now)
}
ClientState::Rebinding(s) => {
s.retransmission_timer_expired(transaction_id, options_to_request, rng, now)
}
ClientState::InformationReceived(_) | ClientState::Assigned(_) => {
unreachable!("received unexpected retransmission timeout in state {:?}.", self);
}
}
}
/// Handles refresh timeout.
fn refresh_timer_expired<R: Rng>(
self,
transaction_id: [u8; 3],
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
match self {
ClientState::InformationReceived(s) => {
s.refresh_timer_expired(transaction_id, options_to_request, rng, now)
}
ClientState::InformationRequesting(_)
| ClientState::ServerDiscovery(_)
| ClientState::Requesting(_)
| ClientState::Assigned(_)
| ClientState::Renewing(_)
| ClientState::Rebinding(_) => {
unreachable!("received unexpected refresh timeout in state {:?}.", self);
}
}
}
/// Handles renew timeout.
fn renew_timer_expired<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
match self {
ClientState::Assigned(s) => s.renew_timer_expired(options_to_request, rng, now),
ClientState::InformationRequesting(_)
| ClientState::InformationReceived(_)
| ClientState::ServerDiscovery(_)
| ClientState::Requesting(_)
| ClientState::Renewing(_)
| ClientState::Rebinding(_) => {
unreachable!("received unexpected renew timeout in state {:?}.", self);
}
}
}
/// Handles rebind timeout.
fn rebind_timer_expired<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
match self {
ClientState::Assigned(s) => s.rebind_timer_expired(options_to_request, rng, now),
ClientState::Renewing(s) => s.rebind_timer_expired(options_to_request, rng, now),
ClientState::InformationRequesting(_)
| ClientState::InformationReceived(_)
| ClientState::ServerDiscovery(_)
| ClientState::Requesting(_)
| ClientState::Rebinding(_) => {
unreachable!("received unexpected rebind timeout in state {:?}.", self);
}
}
}
fn restart_server_discovery<R: Rng>(
self,
options_to_request: &[v6::OptionCode],
rng: &mut R,
now: I,
) -> Transition<I> {
match self {
ClientState::Requesting(s) => s.restart_server_discovery(options_to_request, rng, now),
ClientState::Assigned(s) => s.restart_server_discovery(options_to_request, rng, now),
ClientState::Renewing(s) => s.restart_server_discovery(options_to_request, rng, now),
ClientState::Rebinding(s) => s.restart_server_discovery(options_to_request, rng, now),
ClientState::InformationRequesting(_)
| ClientState::InformationReceived(_)
| ClientState::ServerDiscovery(_) => {
unreachable!("received unexpected rebind timeout in state {:?}.", self);
}
}
}
/// Returns the DNS servers advertised by the server.
fn get_dns_servers(&self) -> Vec<Ipv6Addr> {
match self {
ClientState::InformationReceived(InformationReceived { dns_servers, _marker }) => {
dns_servers.clone()
}
ClientState::Assigned(Assigned {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers,
solicit_max_rt: _,
_marker: _,
})
| ClientState::Renewing(RenewingOrRebinding(RenewingOrRebindingInner {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers,
start_time: _,
retrans_timeout: _,
solicit_max_rt: _,
}))
| ClientState::Rebinding(RenewingOrRebinding(RenewingOrRebindingInner {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers,
start_time: _,
retrans_timeout: _,
solicit_max_rt: _,
})) => dns_servers.clone(),
ClientState::InformationRequesting(InformationRequesting {
retrans_timeout: _,
_marker: _,
})
| ClientState::ServerDiscovery(ServerDiscovery {
client_id: _,
configured_non_temporary_addresses: _,
configured_delegated_prefixes: _,
first_solicit_time: _,
retrans_timeout: _,
solicit_max_rt: _,
collected_advertise: _,
collected_sol_max_rt: _,
})
| ClientState::Requesting(Requesting {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
collected_advertise: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
}) => Vec::new(),
}
}
}
/// The DHCPv6 core state machine.
///
/// This struct maintains the state machine for a DHCPv6 client, and expects an imperative shell to
/// drive it by taking necessary actions (e.g. send packets, schedule timers, etc.) and dispatch
/// events (e.g. packets received, timer expired, etc.). All the functions provided by this struct
/// are pure-functional. All state transition functions return a list of actions that the
/// imperative shell should take to complete the transition.
#[derive(Debug)]
pub struct ClientStateMachine<I, R: Rng> {
/// [Transaction ID] the client is using to communicate with servers.
///
/// [Transaction ID]: https://tools.ietf.org/html/rfc8415#section-16.1
transaction_id: [u8; 3],
/// Options to include in [Option Request Option].
/// [Option Request Option]: https://tools.ietf.org/html/rfc8415#section-21.7
options_to_request: Vec<v6::OptionCode>,
/// Current state of the client, must not be `None`.
///
/// Using an `Option` here allows the client to consume and replace the state during
/// transitions.
state: Option<ClientState<I>>,
/// Used by the client to generate random numbers.
rng: R,
}
impl<I: Instant, R: Rng> ClientStateMachine<I, R> {
/// Starts the client in Stateless mode, as defined in [RFC 8415, Section 6.1].
/// The client exchanges messages with servers to obtain the configuration
/// information specified in `options_to_request`.
///
/// [RFC 8415, Section 6.1]: https://tools.ietf.org/html/rfc8415#section-6.1
pub fn start_stateless(
transaction_id: [u8; 3],
options_to_request: Vec<v6::OptionCode>,
mut rng: R,
now: I,
) -> (Self, Actions<I>) {
let Transition { state, actions, transaction_id: new_transaction_id } =
InformationRequesting::start(transaction_id, &options_to_request, &mut rng, now);
(
Self {
state: Some(state),
transaction_id: new_transaction_id.unwrap_or(transaction_id),
options_to_request,
rng,
},
actions,
)
}
/// Starts the client in Stateful mode, as defined in [RFC 8415, Section 6.2]
/// and [RFC 8415, Section 6.3].
///
/// The client exchanges messages with server(s) to obtain non-temporary
/// addresses in `configured_non_temporary_addresses`, delegated prefixes in
/// `configured_delegated_prefixes` and the configuration information in
/// `options_to_request`.
///
/// [RFC 8415, Section 6.2]: https://tools.ietf.org/html/rfc8415#section-6.2
/// [RFC 8415, Section 6.3]: https://tools.ietf.org/html/rfc8415#section-6.3
pub fn start_stateful(
transaction_id: [u8; 3],
client_id: ClientDuid,
configured_non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>>,
configured_delegated_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
options_to_request: Vec<v6::OptionCode>,
mut rng: R,
now: I,
) -> (Self, Actions<I>) {
let Transition { state, actions, transaction_id: new_transaction_id } =
ServerDiscovery::start(
transaction_id,
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
&options_to_request,
MAX_SOLICIT_TIMEOUT,
&mut rng,
now,
std::iter::empty(),
);
(
Self {
state: Some(state),
transaction_id: new_transaction_id.unwrap_or(transaction_id),
options_to_request,
rng,
},
actions,
)
}
pub fn get_dns_servers(&self) -> Vec<Ipv6Addr> {
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } = self;
state.as_ref().expect("state should not be empty").get_dns_servers()
}
/// Handles a timeout event, dispatches based on timeout type.
///
/// # Panics
///
/// `handle_timeout` panics if current state is None.
pub fn handle_timeout(&mut self, timeout_type: ClientTimerType, now: I) -> Actions<I> {
let ClientStateMachine { transaction_id, options_to_request, state, rng } = self;
let old_state = state.take().expect("state should not be empty");
debug!("handling timeout {:?}", timeout_type);
let Transition { state: new_state, actions, transaction_id: new_transaction_id } =
match timeout_type {
ClientTimerType::Retransmission => old_state.retransmission_timer_expired(
*transaction_id,
&options_to_request,
rng,
now,
),
ClientTimerType::Refresh => {
old_state.refresh_timer_expired(*transaction_id, &options_to_request, rng, now)
}
ClientTimerType::Renew => {
old_state.renew_timer_expired(&options_to_request, rng, now)
}
ClientTimerType::Rebind => {
old_state.rebind_timer_expired(&options_to_request, rng, now)
}
ClientTimerType::RestartServerDiscovery => {
old_state.restart_server_discovery(&options_to_request, rng, now)
}
};
*state = Some(new_state);
*transaction_id = new_transaction_id.unwrap_or(*transaction_id);
actions
}
/// Handles a received DHCPv6 message.
///
/// # Panics
///
/// `handle_reply` panics if current state is None.
pub fn handle_message_receive<B: ByteSlice>(
&mut self,
msg: v6::Message<'_, B>,
now: I,
) -> Actions<I> {
let ClientStateMachine { transaction_id, options_to_request, state, rng } = self;
if msg.transaction_id() != transaction_id {
Vec::new() // Ignore messages for other clients.
} else {
debug!("handling received message of type: {:?}", msg.msg_type());
match msg.msg_type() {
v6::MessageType::Reply => {
let Transition {
state: new_state,
actions,
transaction_id: new_transaction_id,
} = state.take().expect("state should not be empty").reply_message_received(
&options_to_request,
rng,
msg,
now,
);
*state = Some(new_state);
*transaction_id = new_transaction_id.unwrap_or(*transaction_id);
actions
}
v6::MessageType::Advertise => {
let Transition {
state: new_state,
actions,
transaction_id: new_transaction_id,
} = state
.take()
.expect("state should not be empty")
.advertise_message_received(&options_to_request, rng, msg, now);
*state = Some(new_state);
*transaction_id = new_transaction_id.unwrap_or(*transaction_id);
actions
}
v6::MessageType::Reconfigure => {
// TODO(jayzhuang): support Reconfigure messages when needed.
// https://tools.ietf.org/html/rfc8415#section-18.2.11
Vec::new()
}
v6::MessageType::Solicit
| v6::MessageType::Request
| v6::MessageType::Confirm
| v6::MessageType::Renew
| v6::MessageType::Rebind
| v6::MessageType::Release
| v6::MessageType::Decline
| v6::MessageType::InformationRequest
| v6::MessageType::RelayForw
| v6::MessageType::RelayRepl => {
// Ignore unexpected message types.
Vec::new()
}
}
}
}
}
#[cfg(test)]
pub(crate) mod testconsts {
use super::*;
use const_unwrap::const_unwrap_option;
use net_declare::{net_ip_v6, net_subnet_v6};
pub(super) trait IaValueTestExt: IaValue {
const CONFIGURED: [Self; 3];
}
impl IaValueTestExt for Ipv6Addr {
const CONFIGURED: [Self; 3] = CONFIGURED_NON_TEMPORARY_ADDRESSES;
}
impl IaValueTestExt for Subnet<Ipv6Addr> {
const CONFIGURED: [Self; 3] = CONFIGURED_DELEGATED_PREFIXES;
}
pub(crate) const INFINITY: u32 = u32::MAX;
pub(crate) const DNS_SERVERS: [Ipv6Addr; 2] =
[net_ip_v6!("ff01::0102"), net_ip_v6!("ff01::0304")];
pub(crate) const CLIENT_ID: [u8; 18] =
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
pub(crate) const MISMATCHED_CLIENT_ID: [u8; 18] =
[20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37];
pub(crate) const TEST_SERVER_ID_LEN: usize = 3;
pub(crate) const SERVER_ID: [[u8; TEST_SERVER_ID_LEN]; 3] =
[[100, 101, 102], [110, 111, 112], [120, 121, 122]];
pub(crate) const RENEW_NON_TEMPORARY_ADDRESSES: [Ipv6Addr; 3] = [
net_ip_v6!("::ffff:4e45:123"),
net_ip_v6!("::ffff:4e45:456"),
net_ip_v6!("::ffff:4e45:789"),
];
pub(crate) const REPLY_NON_TEMPORARY_ADDRESSES: [Ipv6Addr; 3] = [
net_ip_v6!("::ffff:5447:123"),
net_ip_v6!("::ffff:5447:456"),
net_ip_v6!("::ffff:5447:789"),
];
pub(crate) const CONFIGURED_NON_TEMPORARY_ADDRESSES: [Ipv6Addr; 3] = [
net_ip_v6!("::ffff:c00a:123"),
net_ip_v6!("::ffff:c00a:456"),
net_ip_v6!("::ffff:c00a:789"),
];
pub(crate) const RENEW_DELEGATED_PREFIXES: [Subnet<Ipv6Addr>; 3] =
[net_subnet_v6!("1::/64"), net_subnet_v6!("2::/60"), net_subnet_v6!("3::/56")];
pub(crate) const REPLY_DELEGATED_PREFIXES: [Subnet<Ipv6Addr>; 3] =
[net_subnet_v6!("d::/64"), net_subnet_v6!("e::/60"), net_subnet_v6!("f::/56")];
pub(crate) const CONFIGURED_DELEGATED_PREFIXES: [Subnet<Ipv6Addr>; 3] =
[net_subnet_v6!("a::/64"), net_subnet_v6!("b::/60"), net_subnet_v6!("c::/56")];
pub(crate) const T1: v6::NonZeroOrMaxU32 = const_unwrap_option(v6::NonZeroOrMaxU32::new(30));
pub(crate) const T2: v6::NonZeroOrMaxU32 = const_unwrap_option(v6::NonZeroOrMaxU32::new(70));
pub(crate) const PREFERRED_LIFETIME: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(40));
pub(crate) const VALID_LIFETIME: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(80));
pub(crate) const RENEWED_T1: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(130));
pub(crate) const RENEWED_T2: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(170));
pub(crate) const RENEWED_PREFERRED_LIFETIME: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(140));
pub(crate) const RENEWED_VALID_LIFETIME: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(180));
}
#[cfg(test)]
pub(crate) mod testutil {
use std::time::Instant;
use super::*;
use packet::ParsablePacket;
use testconsts::*;
pub(crate) fn to_configured_addresses(
address_count: usize,
preferred_addresses: impl IntoIterator<Item = HashSet<Ipv6Addr>>,
) -> HashMap<v6::IAID, HashSet<Ipv6Addr>> {
let addresses = preferred_addresses
.into_iter()
.chain(std::iter::repeat_with(HashSet::new))
.take(address_count);
(0..).map(v6::IAID::new).zip(addresses).collect()
}
pub(crate) fn to_configured_prefixes(
prefix_count: usize,
preferred_prefixes: impl IntoIterator<Item = HashSet<Subnet<Ipv6Addr>>>,
) -> HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>> {
let prefixes = preferred_prefixes
.into_iter()
.chain(std::iter::repeat_with(HashSet::new))
.take(prefix_count);
(0..).map(v6::IAID::new).zip(prefixes).collect()
}
pub(super) fn to_default_ias_map<A: IaValue>(addresses: &[A]) -> HashMap<v6::IAID, HashSet<A>> {
(0..)
.map(v6::IAID::new)
.zip(addresses.iter().map(|value| HashSet::from([*value])))
.collect()
}
pub(super) fn assert_server_discovery(
state: &Option<ClientState<Instant>>,
client_id: &[u8],
configured_non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>>,
configured_delegated_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
first_solicit_time: Instant,
buf: &[u8],
options_to_request: &[v6::OptionCode],
) {
assert_matches!(
state,
Some(ClientState::ServerDiscovery(ServerDiscovery {
client_id: got_client_id,
configured_non_temporary_addresses: got_configured_non_temporary_addresses,
configured_delegated_prefixes: got_configured_delegated_prefixes,
first_solicit_time: got_first_solicit_time,
retrans_timeout: INITIAL_SOLICIT_TIMEOUT,
solicit_max_rt: MAX_SOLICIT_TIMEOUT,
collected_advertise,
collected_sol_max_rt,
})) => {
assert_eq!(got_client_id, client_id);
assert_eq!(
got_configured_non_temporary_addresses,
&configured_non_temporary_addresses,
);
assert_eq!(
got_configured_delegated_prefixes,
&configured_delegated_prefixes,
);
assert!(
collected_advertise.is_empty(),
"collected_advertise={:?}",
collected_advertise,
);
assert_eq!(collected_sol_max_rt, &[]);
assert_eq!(*got_first_solicit_time, first_solicit_time);
}
);
assert_outgoing_stateful_message(
buf,
v6::MessageType::Solicit,
client_id,
None,
&options_to_request,
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
);
}
/// Creates a stateful client and asserts that:
/// - the client is started in ServerDiscovery state
/// - the state contain the expected value
/// - the actions are correct
/// - the Solicit message is correct
///
/// Returns the client in ServerDiscovery state.
pub(crate) fn start_and_assert_server_discovery<R: Rng + std::fmt::Debug>(
transaction_id: [u8; 3],
client_id: &ClientDuid,
configured_non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>>,
configured_delegated_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
options_to_request: Vec<v6::OptionCode>,
rng: R,
now: Instant,
) -> ClientStateMachine<Instant, R> {
let (client, actions) = ClientStateMachine::start_stateful(
transaction_id.clone(),
client_id.clone(),
configured_non_temporary_addresses.clone(),
configured_delegated_prefixes.clone(),
options_to_request.clone(),
rng,
now,
);
let ClientStateMachine {
transaction_id: got_transaction_id,
options_to_request: got_options_to_request,
state,
rng: _,
} = &client;
assert_eq!(got_transaction_id, &transaction_id);
assert_eq!(got_options_to_request, &options_to_request);
// Start of server discovery should send a solicit and schedule a
// retransmission timer.
let buf = assert_matches!( &actions[..],
[
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(*instant, now.add(INITIAL_SOLICIT_TIMEOUT));
buf
}
);
assert_server_discovery(
state,
client_id,
configured_non_temporary_addresses,
configured_delegated_prefixes,
now,
buf,
&options_to_request,
);
client
}
impl Lifetimes {
pub(crate) const fn new_default() -> Self {
Lifetimes::new_finite(PREFERRED_LIFETIME, VALID_LIFETIME)
}
pub(crate) fn new(preferred_lifetime: u32, non_zero_valid_lifetime: u32) -> Self {
Lifetimes {
preferred_lifetime: v6::TimeValue::new(preferred_lifetime),
valid_lifetime: assert_matches!(
v6::TimeValue::new(non_zero_valid_lifetime),
v6::TimeValue::NonZero(v) => v
),
}
}
pub(crate) const fn new_finite(
preferred_lifetime: v6::NonZeroOrMaxU32,
valid_lifetime: v6::NonZeroOrMaxU32,
) -> Lifetimes {
Lifetimes {
preferred_lifetime: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
preferred_lifetime,
)),
valid_lifetime: v6::NonZeroTimeValue::Finite(valid_lifetime),
}
}
pub(crate) const fn new_renewed() -> Lifetimes {
Lifetimes {
preferred_lifetime: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
RENEWED_PREFERRED_LIFETIME,
)),
valid_lifetime: v6::NonZeroTimeValue::Finite(RENEWED_VALID_LIFETIME),
}
}
}
impl<V: IaValue> IaEntry<V, Instant> {
pub(crate) fn new_assigned(
value: V,
preferred_lifetime: v6::NonZeroOrMaxU32,
valid_lifetime: v6::NonZeroOrMaxU32,
updated_at: Instant,
) -> Self {
Self::Assigned(HashMap::from([(
value,
LifetimesInfo {
lifetimes: Lifetimes {
preferred_lifetime: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
preferred_lifetime,
)),
valid_lifetime: v6::NonZeroTimeValue::Finite(valid_lifetime),
},
updated_at,
},
)]))
}
}
impl AdvertiseMessage<Instant> {
pub(crate) fn new_default(
server_id: [u8; TEST_SERVER_ID_LEN],
non_temporary_addresses: &[Ipv6Addr],
delegated_prefixes: &[Subnet<Ipv6Addr>],
dns_servers: &[Ipv6Addr],
configured_non_temporary_addresses: &HashMap<v6::IAID, HashSet<Ipv6Addr>>,
configured_delegated_prefixes: &HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
) -> AdvertiseMessage<Instant> {
let non_temporary_addresses = (0..)
.map(v6::IAID::new)
.zip(non_temporary_addresses.iter().map(|address| HashSet::from([*address])))
.collect();
let delegated_prefixes = (0..)
.map(v6::IAID::new)
.zip(delegated_prefixes.iter().map(|prefix| HashSet::from([*prefix])))
.collect();
let preferred_non_temporary_addresses_count = compute_preferred_ia_count(
&non_temporary_addresses,
&configured_non_temporary_addresses,
);
let preferred_delegated_prefixes_count =
compute_preferred_ia_count(&delegated_prefixes, &configured_delegated_prefixes);
AdvertiseMessage {
server_id: server_id.to_vec(),
non_temporary_addresses,
delegated_prefixes,
dns_servers: dns_servers.to_vec(),
preference: 0,
receive_time: Instant::now(),
preferred_non_temporary_addresses_count,
preferred_delegated_prefixes_count,
}
}
}
/// Parses `buf` and returns the DHCPv6 message type.
///
/// # Panics
///
/// `msg_type` panics if parsing fails.
pub(crate) fn msg_type(mut buf: &[u8]) -> v6::MessageType {
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
msg.msg_type()
}
/// A helper identity association test type specifying T1/T2, for testing
/// T1/T2 variations across IAs.
#[derive(Clone)]
pub(super) struct TestIa<V: IaValue> {
pub(crate) values: HashMap<V, Lifetimes>,
pub(crate) t1: v6::TimeValue,
pub(crate) t2: v6::TimeValue,
}
impl<V: IaValue> TestIa<V> {
/// Creates a `TestIa` with default valid values for
/// lifetimes.
pub(crate) fn new_default(value: V) -> TestIa<V> {
TestIa::new_default_with_values(HashMap::from([(value, Lifetimes::new_default())]))
}
pub(crate) fn new_default_with_values(values: HashMap<V, Lifetimes>) -> TestIa<V> {
TestIa {
values,
t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
}
}
/// Creates a `TestIa` with default valid values for
/// renewed lifetimes.
pub(crate) fn new_renewed_default(value: V) -> TestIa<V> {
TestIa::new_renewed_default_with_values([value].into_iter())
}
pub(crate) fn new_renewed_default_with_values(
values: impl Iterator<Item = V>,
) -> TestIa<V> {
TestIa {
values: values.map(|v| (v, Lifetimes::new_renewed())).collect(),
t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(RENEWED_T1)),
t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(RENEWED_T2)),
}
}
}
pub(super) struct TestMessageBuilder<'a, IaNaIter, IaPdIter> {
pub(super) transaction_id: [u8; 3],
pub(super) message_type: v6::MessageType,
pub(super) client_id: &'a [u8],
pub(super) server_id: &'a [u8],
pub(super) preference: Option<u8>,
pub(super) dns_servers: Option<&'a [Ipv6Addr]>,
pub(super) ia_nas: IaNaIter,
pub(super) ia_pds: IaPdIter,
}
impl<
'a,
IaNaIter: Iterator<Item = (v6::IAID, TestIa<Ipv6Addr>)>,
IaPdIter: Iterator<Item = (v6::IAID, TestIa<Subnet<Ipv6Addr>>)>,
> TestMessageBuilder<'a, IaNaIter, IaPdIter>
{
pub(super) fn build(self) -> Vec<u8> {
let TestMessageBuilder {
transaction_id,
message_type,
client_id,
server_id,
preference,
dns_servers,
ia_nas,
ia_pds,
} = self;
struct Inner<'a> {
opt: Vec<v6::DhcpOption<'a>>,
t1: v6::TimeValue,
t2: v6::TimeValue,
}
let iaaddr_options = ia_nas
.map(|(iaid, TestIa { values, t1, t2 })| {
(
iaid,
Inner {
opt: values
.into_iter()
.map(|(value, Lifetimes { preferred_lifetime, valid_lifetime })| {
v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
value,
get_value(preferred_lifetime),
get_value(valid_lifetime.into()),
&[],
))
})
.collect(),
t1,
t2,
},
)
})
.collect::<HashMap<_, _>>();
let iaprefix_options = ia_pds
.map(|(iaid, TestIa { values, t1, t2 })| {
(
iaid,
Inner {
opt: values
.into_iter()
.map(|(value, Lifetimes { preferred_lifetime, valid_lifetime })| {
v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
get_value(preferred_lifetime),
get_value(valid_lifetime.into()),
value,
&[],
))
})
.collect(),
t1,
t2,
},
)
})
.collect::<HashMap<_, _>>();
let options =
[v6::DhcpOption::ServerId(&server_id), v6::DhcpOption::ClientId(client_id)]
.into_iter()
.chain(preference.into_iter().map(v6::DhcpOption::Preference))
.chain(dns_servers.into_iter().map(v6::DhcpOption::DnsServers))
.chain(iaaddr_options.iter().map(|(iaid, Inner { opt, t1, t2 })| {
v6::DhcpOption::Iana(v6::IanaSerializer::new(
*iaid,
get_value(*t1),
get_value(*t2),
opt.as_ref(),
))
}))
.chain(iaprefix_options.iter().map(|(iaid, Inner { opt, t1, t2 })| {
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
*iaid,
get_value(*t1),
get_value(*t2),
opt.as_ref(),
))
}))
.collect::<Vec<_>>();
let builder = v6::MessageBuilder::new(message_type, transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
buf
}
}
pub(super) type TestIaNa = TestIa<Ipv6Addr>;
pub(super) type TestIaPd = TestIa<Subnet<Ipv6Addr>>;
/// Creates a stateful client, exchanges messages to bring it in Requesting
/// state, and sends a Request message. Returns the client in Requesting
/// state and the transaction ID for the Request-Reply exchange. Asserts the
/// content of the sent Request message and of the Requesting state.
///
/// # Panics
///
/// `request_and_assert` panics if the Request message cannot be
/// parsed or does not contain the expected options, or the Requesting state
/// is incorrect.
pub(super) fn request_and_assert<R: Rng + std::fmt::Debug>(
client_id: &ClientDuid,
server_id: [u8; TEST_SERVER_ID_LEN],
non_temporary_addresses_to_assign: Vec<TestIaNa>,
delegated_prefixes_to_assign: Vec<TestIaPd>,
expected_dns_servers: &[Ipv6Addr],
rng: R,
now: Instant,
) -> (ClientStateMachine<Instant, R>, [u8; 3]) {
// Generate a transaction_id for the Solicit - Advertise message
// exchange.
let transaction_id = [1, 2, 3];
let configured_non_temporary_addresses = to_configured_addresses(
non_temporary_addresses_to_assign.len(),
non_temporary_addresses_to_assign
.iter()
.map(|TestIaNa { values, t1: _, t2: _ }| values.keys().cloned().collect()),
);
let configured_delegated_prefixes = to_configured_prefixes(
delegated_prefixes_to_assign.len(),
delegated_prefixes_to_assign
.iter()
.map(|TestIaPd { values, t1: _, t2: _ }| values.keys().cloned().collect()),
);
let options_to_request = if expected_dns_servers.is_empty() {
Vec::new()
} else {
vec![v6::OptionCode::DnsServers]
};
let mut client = testutil::start_and_assert_server_discovery(
transaction_id.clone(),
client_id,
configured_non_temporary_addresses.clone(),
configured_delegated_prefixes.clone(),
options_to_request.clone(),
rng,
now,
);
let buf = TestMessageBuilder {
transaction_id,
message_type: v6::MessageType::Advertise,
client_id: &CLIENT_ID,
server_id: &server_id,
preference: Some(ADVERTISE_MAX_PREFERENCE),
dns_servers: (!expected_dns_servers.is_empty()).then(|| expected_dns_servers),
ia_nas: (0..).map(v6::IAID::new).zip(non_temporary_addresses_to_assign),
ia_pds: (0..).map(v6::IAID::new).zip(delegated_prefixes_to_assign),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
// The client should select the server that sent the best advertise and
// transition to Requesting.
let actions = client.handle_message_receive(msg, now);
let buf = assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(*instant, now.add(INITIAL_REQUEST_TIMEOUT));
buf
}
);
testutil::assert_outgoing_stateful_message(
&buf,
v6::MessageType::Request,
&client_id,
Some(&server_id),
&options_to_request,
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
);
let ClientStateMachine { transaction_id, options_to_request: _, state, rng: _ } = &client;
let request_transaction_id = *transaction_id;
{
let Requesting {
client_id: got_client_id,
server_id: got_server_id,
collected_advertise,
retrans_timeout,
transmission_count,
solicit_max_rt,
non_temporary_addresses: _,
delegated_prefixes: _,
first_request_time: _,
} = assert_matches!(&state, Some(ClientState::Requesting(requesting)) => requesting);
assert_eq!(got_client_id, client_id);
assert_eq!(*got_server_id, server_id);
assert!(
collected_advertise.is_empty(),
"collected_advertise = {:?}",
collected_advertise
);
assert_eq!(*retrans_timeout, INITIAL_REQUEST_TIMEOUT);
assert_eq!(*transmission_count, 1);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
(client, request_transaction_id)
}
/// Creates a stateful client and exchanges messages to assign the
/// configured addresses/prefixes. Returns the client in Assigned state and
/// the actions returned on transitioning to the Assigned state.
/// Asserts the content of the client state.
///
/// # Panics
///
/// `assign_and_assert` panics if assignment fails.
pub(super) fn assign_and_assert<R: Rng + std::fmt::Debug>(
client_id: &ClientDuid,
server_id: [u8; TEST_SERVER_ID_LEN],
non_temporary_addresses_to_assign: Vec<TestIaNa>,
delegated_prefixes_to_assign: Vec<TestIaPd>,
expected_dns_servers: &[Ipv6Addr],
rng: R,
now: Instant,
) -> (ClientStateMachine<Instant, R>, Actions<Instant>) {
let (mut client, transaction_id) = testutil::request_and_assert(
client_id,
server_id.clone(),
non_temporary_addresses_to_assign.clone(),
delegated_prefixes_to_assign.clone(),
expected_dns_servers,
rng,
now,
);
let non_temporary_addresses_to_assign = (0..)
.map(v6::IAID::new)
.zip(non_temporary_addresses_to_assign)
.collect::<HashMap<_, _>>();
let delegated_prefixes_to_assign =
(0..).map(v6::IAID::new).zip(delegated_prefixes_to_assign).collect::<HashMap<_, _>>();
let buf = TestMessageBuilder {
transaction_id,
message_type: v6::MessageType::Reply,
client_id: &CLIENT_ID,
server_id: &SERVER_ID[0],
preference: None,
dns_servers: (!expected_dns_servers.is_empty()).then(|| expected_dns_servers),
ia_nas: non_temporary_addresses_to_assign.iter().map(|(k, v)| (*k, v.clone())),
ia_pds: delegated_prefixes_to_assign.iter().map(|(k, v)| (*k, v.clone())),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let actions = client.handle_message_receive(msg, now);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let expected_non_temporary_addresses = non_temporary_addresses_to_assign
.iter()
.map(|(iaid, TestIaNa { values, t1: _, t2: _ })| {
(
*iaid,
AddressEntry::Assigned(
values
.iter()
.map(|(v, lifetimes)| {
(*v, LifetimesInfo { lifetimes: *lifetimes, updated_at: now })
})
.collect(),
),
)
})
.collect::<HashMap<_, _>>();
let expected_delegated_prefixes = delegated_prefixes_to_assign
.iter()
.map(|(iaid, TestIaPd { values, t1: _, t2: _ })| {
(
*iaid,
PrefixEntry::Assigned(
values
.iter()
.map(|(v, lifetimes)| {
(*v, LifetimesInfo { lifetimes: *lifetimes, updated_at: now })
})
.collect(),
),
)
})
.collect::<HashMap<_, _>>();
let Assigned {
client_id: got_client_id,
non_temporary_addresses,
delegated_prefixes,
server_id: got_server_id,
dns_servers,
solicit_max_rt,
_marker,
} = assert_matches!(
&state,
Some(ClientState::Assigned(assigned)) => assigned
);
assert_eq!(got_client_id, client_id);
assert_eq!(non_temporary_addresses, &expected_non_temporary_addresses);
assert_eq!(delegated_prefixes, &expected_delegated_prefixes);
assert_eq!(*got_server_id, server_id);
assert_eq!(dns_servers, expected_dns_servers);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
(client, actions)
}
/// Gets the `u32` value inside a `v6::TimeValue`.
pub(crate) fn get_value(t: v6::TimeValue) -> u32 {
const INFINITY: u32 = u32::MAX;
match t {
v6::TimeValue::Zero => 0,
v6::TimeValue::NonZero(non_zero_tv) => match non_zero_tv {
v6::NonZeroTimeValue::Finite(t) => t.get(),
v6::NonZeroTimeValue::Infinity => INFINITY,
},
}
}
/// Checks that the buffer contains the expected type and options for an
/// outgoing message in stateful mode.
///
/// # Panics
///
/// `assert_outgoing_stateful_message` panics if the message cannot be
/// parsed, or does not contain the expected options.
pub(crate) fn assert_outgoing_stateful_message(
mut buf: &[u8],
expected_msg_type: v6::MessageType,
expected_client_id: &[u8],
expected_server_id: Option<&[u8; TEST_SERVER_ID_LEN]>,
expected_oro: &[v6::OptionCode],
expected_non_temporary_addresses: &HashMap<v6::IAID, HashSet<Ipv6Addr>>,
expected_delegated_prefixes: &HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>>,
) {
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_eq!(msg.msg_type(), expected_msg_type);
let (mut non_ia_opts, iana_opts, iapd_opts, other) = msg.options().fold(
(Vec::new(), Vec::new(), Vec::new(), Vec::new()),
|(mut non_ia_opts, mut iana_opts, mut iapd_opts, mut other), opt| {
match opt {
v6::ParsedDhcpOption::ClientId(_)
| v6::ParsedDhcpOption::ElapsedTime(_)
| v6::ParsedDhcpOption::Oro(_) => non_ia_opts.push(opt),
v6::ParsedDhcpOption::ServerId(_) if expected_server_id.is_some() => {
non_ia_opts.push(opt)
}
v6::ParsedDhcpOption::Iana(iana_data) => iana_opts.push(iana_data),
v6::ParsedDhcpOption::IaPd(iapd_data) => iapd_opts.push(iapd_data),
opt => other.push(opt),
}
(non_ia_opts, iana_opts, iapd_opts, other)
},
);
let option_sorter: fn(
&v6::ParsedDhcpOption<'_>,
&v6::ParsedDhcpOption<'_>,
) -> std::cmp::Ordering =
|opt1, opt2| (u16::from(opt1.code())).cmp(&(u16::from(opt2.code())));
// Check that the non-IA options are correct.
non_ia_opts.sort_by(option_sorter);
let expected_non_ia_opts = {
let oro = std::iter::once(v6::OptionCode::SolMaxRt)
.chain(expected_oro.iter().copied())
.collect();
let mut expected_non_ia_opts = vec![
v6::ParsedDhcpOption::ClientId(expected_client_id),
v6::ParsedDhcpOption::ElapsedTime(0),
v6::ParsedDhcpOption::Oro(oro),
];
if let Some(server_id) = expected_server_id {
expected_non_ia_opts.push(v6::ParsedDhcpOption::ServerId(server_id));
}
expected_non_ia_opts.sort_by(option_sorter);
expected_non_ia_opts
};
assert_eq!(non_ia_opts, expected_non_ia_opts);
// Check that the IA options are correct.
let sent_non_temporary_addresses = {
let mut sent_non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>> =
HashMap::new();
for iana_data in iana_opts.iter() {
let mut opts = HashSet::new();
for iana_option in iana_data.iter_options() {
match iana_option {
v6::ParsedDhcpOption::IaAddr(iaaddr_data) => {
assert!(opts.insert(iaaddr_data.addr()));
}
option => panic!("unexpected option {:?}", option),
}
}
assert_eq!(
sent_non_temporary_addresses.insert(v6::IAID::new(iana_data.iaid()), opts),
None
);
}
sent_non_temporary_addresses
};
assert_eq!(&sent_non_temporary_addresses, expected_non_temporary_addresses);
let sent_prefixes = {
let mut sent_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>> = HashMap::new();
for iapd_data in iapd_opts.iter() {
let mut opts = HashSet::new();
for iapd_option in iapd_data.iter_options() {
match iapd_option {
v6::ParsedDhcpOption::IaPrefix(iaprefix_data) => {
assert!(opts.insert(iaprefix_data.prefix().unwrap()));
}
option => panic!("unexpected option {:?}", option),
}
}
assert_eq!(sent_prefixes.insert(v6::IAID::new(iapd_data.iaid()), opts), None);
}
sent_prefixes
};
assert_eq!(&sent_prefixes, expected_delegated_prefixes);
// Check that there are no other options besides the expected non-IA and
// IA options.
assert_eq!(&other, &[]);
}
/// Creates a stateful client, exchanges messages to assign the configured
/// leases, and sends a Renew message. Asserts the content of the client
/// state and of the renew message, and returns the client in Renewing
/// state.
///
/// # Panics
///
/// `send_renew_and_assert` panics if assignment fails, or if sending a
/// renew fails.
pub(super) fn send_renew_and_assert<R: Rng + std::fmt::Debug>(
client_id: &ClientDuid,
server_id: [u8; TEST_SERVER_ID_LEN],
non_temporary_addresses_to_assign: Vec<TestIaNa>,
delegated_prefixes_to_assign: Vec<TestIaPd>,
expected_dns_servers: Option<&[Ipv6Addr]>,
expected_t1_secs: v6::NonZeroOrMaxU32,
expected_t2_secs: v6::NonZeroOrMaxU32,
max_valid_lifetime: v6::NonZeroTimeValue,
rng: R,
now: Instant,
) -> ClientStateMachine<Instant, R> {
let expected_dns_servers_as_slice = expected_dns_servers.unwrap_or(&[]);
let (client, actions) = testutil::assign_and_assert(
client_id,
server_id.clone(),
non_temporary_addresses_to_assign.clone(),
delegated_prefixes_to_assign.clone(),
expected_dns_servers_as_slice,
rng,
now,
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
{
let Assigned {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers: _,
solicit_max_rt: _,
_marker,
} = assert_matches!(
state,
Some(ClientState::Assigned(assigned)) => assigned
);
}
let (expected_oro, maybe_dns_server_action) =
if let Some(expected_dns_servers) = expected_dns_servers {
(
Some([v6::OptionCode::DnsServers]),
Some(Action::UpdateDnsServers(expected_dns_servers.to_vec())),
)
} else {
(None, None)
};
let iana_updates = (0..)
.map(v6::IAID::new)
.zip(non_temporary_addresses_to_assign.iter())
.map(|(iaid, TestIa { values, t1: _, t2: _ })| {
(
iaid,
values
.iter()
.map(|(value, lifetimes)| (*value, IaValueUpdateKind::Added(*lifetimes)))
.collect(),
)
})
.collect::<HashMap<_, _>>();
let iapd_updates = (0..)
.map(v6::IAID::new)
.zip(delegated_prefixes_to_assign.iter())
.map(|(iaid, TestIa { values, t1: _, t2: _ })| {
(
iaid,
values
.iter()
.map(|(value, lifetimes)| (*value, IaValueUpdateKind::Added(*lifetimes)))
.collect(),
)
})
.collect::<HashMap<_, _>>();
let expected_actions = [
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(
ClientTimerType::Renew,
now.add(Duration::from_secs(expected_t1_secs.get().into())),
),
Action::ScheduleTimer(
ClientTimerType::Rebind,
now.add(Duration::from_secs(expected_t2_secs.get().into())),
),
]
.into_iter()
.chain(maybe_dns_server_action)
.chain((!iana_updates.is_empty()).then(|| Action::IaNaUpdates(iana_updates)))
.chain((!iapd_updates.is_empty()).then(|| Action::IaPdUpdates(iapd_updates)))
.chain([match max_valid_lifetime {
v6::NonZeroTimeValue::Finite(max_valid_lifetime) => Action::ScheduleTimer(
ClientTimerType::RestartServerDiscovery,
now.add(Duration::from_secs(max_valid_lifetime.get().into())),
),
v6::NonZeroTimeValue::Infinity => {
Action::CancelTimer(ClientTimerType::RestartServerDiscovery)
}
}])
.collect::<Vec<_>>();
assert_eq!(actions, expected_actions);
handle_renew_or_rebind_timer(
client,
&client_id,
server_id,
non_temporary_addresses_to_assign,
delegated_prefixes_to_assign,
expected_dns_servers_as_slice,
expected_oro.as_ref().map_or(&[], |oro| &oro[..]),
now,
RENEW_TEST_STATE,
)
}
pub(super) struct RenewRebindTestState {
initial_timeout: Duration,
timer_type: ClientTimerType,
message_type: v6::MessageType,
expect_server_id: bool,
with_state: fn(&Option<ClientState<Instant>>) -> &RenewingOrRebindingInner<Instant>,
}
pub(super) const RENEW_TEST_STATE: RenewRebindTestState = RenewRebindTestState {
initial_timeout: INITIAL_RENEW_TIMEOUT,
timer_type: ClientTimerType::Renew,
message_type: v6::MessageType::Renew,
expect_server_id: true,
with_state: |state| {
assert_matches!(
state,
Some(ClientState::Renewing(RenewingOrRebinding(inner))) => inner
)
},
};
pub(super) const REBIND_TEST_STATE: RenewRebindTestState = RenewRebindTestState {
initial_timeout: INITIAL_REBIND_TIMEOUT,
timer_type: ClientTimerType::Rebind,
message_type: v6::MessageType::Rebind,
expect_server_id: false,
with_state: |state| {
assert_matches!(
state,
Some(ClientState::Rebinding(RenewingOrRebinding(inner))) => inner
)
},
};
pub(super) fn handle_renew_or_rebind_timer<R: Rng>(
mut client: ClientStateMachine<Instant, R>,
client_id: &[u8],
server_id: [u8; TEST_SERVER_ID_LEN],
non_temporary_addresses_to_assign: Vec<TestIaNa>,
delegated_prefixes_to_assign: Vec<TestIaPd>,
expected_dns_servers_as_slice: &[Ipv6Addr],
expected_oro: &[v6::OptionCode],
now: Instant,
RenewRebindTestState {
initial_timeout,
timer_type,
message_type,
expect_server_id,
with_state,
}: RenewRebindTestState,
) -> ClientStateMachine<Instant, R> {
let actions = client.handle_timeout(timer_type, now);
let buf = assert_matches!(
&actions[..],
[
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, got_time)
] => {
assert_eq!(*got_time, now.add(initial_timeout));
buf
}
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let RenewingOrRebindingInner {
client_id: got_client_id,
server_id: got_server_id,
dns_servers,
solicit_max_rt,
non_temporary_addresses: _,
delegated_prefixes: _,
start_time: _,
retrans_timeout: _,
} = with_state(state);
assert_eq!(got_client_id, client_id);
assert_eq!(*got_server_id, server_id);
assert_eq!(dns_servers, expected_dns_servers_as_slice);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
let expected_addresses_to_renew: HashMap<v6::IAID, HashSet<Ipv6Addr>> = (0..)
.map(v6::IAID::new)
.zip(
non_temporary_addresses_to_assign
.iter()
.map(|TestIaNa { values, t1: _, t2: _ }| values.keys().cloned().collect()),
)
.collect();
let expected_prefixes_to_renew: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>> = (0..)
.map(v6::IAID::new)
.zip(
delegated_prefixes_to_assign
.iter()
.map(|TestIaPd { values, t1: _, t2: _ }| values.keys().cloned().collect()),
)
.collect();
testutil::assert_outgoing_stateful_message(
&buf,
message_type,
client_id,
expect_server_id.then(|| &server_id),
expected_oro,
&expected_addresses_to_renew,
&expected_prefixes_to_renew,
);
client
}
/// Creates a stateful client, exchanges messages to assign the configured
/// leases, and sends a Renew then Rebind message. Asserts the content of
/// the client state and of the rebind message, and returns the client in
/// Rebinding state.
///
/// # Panics
///
/// `send_rebind_and_assert` panics if assignmentment fails, or if sending a
/// rebind fails.
pub(super) fn send_rebind_and_assert<R: Rng + std::fmt::Debug>(
client_id: &ClientDuid,
server_id: [u8; TEST_SERVER_ID_LEN],
non_temporary_addresses_to_assign: Vec<TestIaNa>,
delegated_prefixes_to_assign: Vec<TestIaPd>,
expected_dns_servers: Option<&[Ipv6Addr]>,
expected_t1_secs: v6::NonZeroOrMaxU32,
expected_t2_secs: v6::NonZeroOrMaxU32,
max_valid_lifetime: v6::NonZeroTimeValue,
rng: R,
now: Instant,
) -> ClientStateMachine<Instant, R> {
let client = testutil::send_renew_and_assert(
client_id,
server_id,
non_temporary_addresses_to_assign.clone(),
delegated_prefixes_to_assign.clone(),
expected_dns_servers,
expected_t1_secs,
expected_t2_secs,
max_valid_lifetime,
rng,
now,
);
let (expected_oro, expected_dns_servers) =
if let Some(expected_dns_servers) = expected_dns_servers {
(Some([v6::OptionCode::DnsServers]), expected_dns_servers)
} else {
(None, &[][..])
};
handle_renew_or_rebind_timer(
client,
&client_id,
server_id,
non_temporary_addresses_to_assign,
delegated_prefixes_to_assign,
expected_dns_servers,
expected_oro.as_ref().map_or(&[], |oro| &oro[..]),
now,
REBIND_TEST_STATE,
)
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use std::time::Instant;
use super::*;
use const_unwrap::const_unwrap_option;
use packet::ParsablePacket;
use rand::rngs::mock::StepRng;
use test_case::test_case;
use testconsts::*;
use testutil::{
handle_renew_or_rebind_timer, RenewRebindTestState, TestIa, TestIaNa, TestIaPd,
TestMessageBuilder, REBIND_TEST_STATE, RENEW_TEST_STATE,
};
#[test]
fn send_information_request_and_receive_reply() {
// Try to start information request with different list of requested options.
for options in [
Vec::new(),
vec![v6::OptionCode::DnsServers],
vec![v6::OptionCode::DnsServers, v6::OptionCode::DomainList],
] {
let now = Instant::now();
let (mut client, actions) = ClientStateMachine::start_stateless(
[0, 1, 2],
options.clone(),
StepRng::new(u64::MAX / 2, 0),
now,
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
assert_matches!(
*state,
Some(ClientState::InformationRequesting(InformationRequesting {
retrans_timeout: INITIAL_INFO_REQ_TIMEOUT,
_marker,
}))
);
// Start of information requesting should send an information request and schedule a
// retransmission timer.
let want_options_array = [v6::DhcpOption::Oro(&options)];
let want_options = if options.is_empty() { &[][..] } else { &want_options_array[..] };
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let builder = v6::MessageBuilder::new(
v6::MessageType::InformationRequest,
*transaction_id,
want_options,
);
let mut want_buf = vec![0; builder.bytes_len()];
builder.serialize(&mut want_buf);
assert_eq!(
actions[..],
[
Action::SendMessage(want_buf),
Action::ScheduleTimer(
ClientTimerType::Retransmission,
now.add(INITIAL_INFO_REQ_TIMEOUT),
)
]
);
let test_dhcp_refresh_time = 42u32;
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::InformationRefreshTime(test_dhcp_refresh_time),
v6::DhcpOption::DnsServers(&DNS_SERVERS),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let now = Instant::now();
let actions = client.handle_message_receive(msg, now);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
client;
{
assert_matches!(
state,
Some(ClientState::InformationReceived(InformationReceived { dns_servers, _marker }))
if dns_servers == DNS_SERVERS.to_vec()
);
}
// Upon receiving a valid reply, client should set up for refresh based on the reply.
assert_eq!(
actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(
ClientTimerType::Refresh,
now.add(Duration::from_secs(u64::from(test_dhcp_refresh_time))),
),
Action::UpdateDnsServers(DNS_SERVERS.to_vec()),
]
);
}
}
#[test]
fn send_information_request_on_retransmission_timeout() {
let now = Instant::now();
let (mut client, actions) = ClientStateMachine::start_stateless(
[0, 1, 2],
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
now,
);
assert_matches!(
actions[..],
[_, Action::ScheduleTimer(ClientTimerType::Retransmission, instant)] => {
assert_eq!(instant, now.add(INITIAL_INFO_REQ_TIMEOUT));
}
);
let actions = client.handle_timeout(ClientTimerType::Retransmission, now);
// Following exponential backoff defined in https://tools.ietf.org/html/rfc8415#section-15.
assert_matches!(
actions[..],
[
_,
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => assert_eq!(instant, now.add(2 * INITIAL_INFO_REQ_TIMEOUT))
);
}
#[test]
fn send_information_request_on_refresh_timeout() {
let (mut client, _) = ClientStateMachine::start_stateless(
[0, 1, 2],
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let options = [v6::DhcpOption::ServerId(&SERVER_ID[0])];
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
// Transition to InformationReceived state.
let time = Instant::now();
assert_eq!(
client.handle_message_receive(msg, time)[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Refresh, time.add(IRT_DEFAULT))
]
);
// Refresh should start another round of information request.
let actions = client.handle_timeout(ClientTimerType::Refresh, time);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let builder =
v6::MessageBuilder::new(v6::MessageType::InformationRequest, *transaction_id, &[]);
let mut want_buf = vec![0; builder.bytes_len()];
builder.serialize(&mut want_buf);
assert_eq!(
actions[..],
[
Action::SendMessage(want_buf),
Action::ScheduleTimer(
ClientTimerType::Retransmission,
time.add(INITIAL_INFO_REQ_TIMEOUT)
)
]
);
}
// Test starting the client in stateful mode with different address
// and prefix configurations.
#[test_case(
0, std::iter::empty(),
2, (&CONFIGURED_DELEGATED_PREFIXES[0..2]).iter().copied(),
Vec::new()
)]
#[test_case(
2, (&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]).iter().copied(),
0, std::iter::empty(),
vec![v6::OptionCode::DnsServers]
)]
#[test_case(
1, std::iter::empty(),
2, (&CONFIGURED_DELEGATED_PREFIXES[0..2]).iter().copied(),
Vec::new()
)]
#[test_case(
2, std::iter::once(CONFIGURED_NON_TEMPORARY_ADDRESSES[0]),
1, std::iter::empty(),
vec![v6::OptionCode::DnsServers]
)]
#[test_case(
2, (&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]).iter().copied(),
2, std::iter::once(CONFIGURED_DELEGATED_PREFIXES[0]),
vec![v6::OptionCode::DnsServers]
)]
fn send_solicit(
address_count: usize,
preferred_non_temporary_addresses: impl IntoIterator<Item = Ipv6Addr>,
prefix_count: usize,
preferred_delegated_prefixes: impl IntoIterator<Item = Subnet<Ipv6Addr>>,
options_to_request: Vec<v6::OptionCode>,
) {
// The client is checked inside `start_and_assert_server_discovery`.
let _client = testutil::start_and_assert_server_discovery(
[0, 1, 2],
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
address_count,
preferred_non_temporary_addresses.into_iter().map(|a| HashSet::from([a])),
),
testutil::to_configured_prefixes(
prefix_count,
preferred_delegated_prefixes.into_iter().map(|a| HashSet::from([a])),
),
options_to_request,
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
}
#[test_case(
1, std::iter::empty(), std::iter::once(CONFIGURED_NON_TEMPORARY_ADDRESSES[0]), 0;
"zero"
)]
#[test_case(
2, CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2].iter().copied(), CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2].iter().copied(), 2;
"two"
)]
#[test_case(
4,
CONFIGURED_NON_TEMPORARY_ADDRESSES.iter().copied(),
std::iter::once(CONFIGURED_NON_TEMPORARY_ADDRESSES[0]).chain(REPLY_NON_TEMPORARY_ADDRESSES.iter().copied()),
1;
"one"
)]
fn compute_preferred_address_count(
configure_count: usize,
hints: impl IntoIterator<Item = Ipv6Addr>,
got_addresses: impl IntoIterator<Item = Ipv6Addr>,
want: usize,
) {
// No preferred addresses configured.
let got_addresses: HashMap<_, _> = (0..)
.map(v6::IAID::new)
.zip(got_addresses.into_iter().map(|a| HashSet::from([a])))
.collect();
let configured_non_temporary_addresses = testutil::to_configured_addresses(
configure_count,
hints.into_iter().map(|a| HashSet::from([a])),
);
assert_eq!(
super::compute_preferred_ia_count(&got_addresses, &configured_non_temporary_addresses),
want,
);
}
#[test_case(&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2], &CONFIGURED_DELEGATED_PREFIXES[0..2], true)]
#[test_case(&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..1], &CONFIGURED_DELEGATED_PREFIXES[0..1], true)]
#[test_case(&REPLY_NON_TEMPORARY_ADDRESSES[0..2], &REPLY_DELEGATED_PREFIXES[0..2], true)]
#[test_case(&[], &[], false)]
fn advertise_message_has_ias(
non_temporary_addresses: &[Ipv6Addr],
delegated_prefixes: &[Subnet<Ipv6Addr>],
expected: bool,
) {
let configured_non_temporary_addresses = testutil::to_configured_addresses(
2,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
);
let configured_delegated_prefixes = testutil::to_configured_prefixes(
2,
std::iter::once(HashSet::from([CONFIGURED_DELEGATED_PREFIXES[0]])),
);
// Advertise is acceptable even though it does not contain the solicited
// preferred address.
let advertise = AdvertiseMessage::new_default(
SERVER_ID[0],
non_temporary_addresses,
delegated_prefixes,
&[],
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
);
assert_eq!(advertise.has_ias(), expected);
}
struct AdvertiseMessageOrdTestCase<'a> {
adv1_non_temporary_addresses: &'a [Ipv6Addr],
adv1_delegated_prefixes: &'a [Subnet<Ipv6Addr>],
adv2_non_temporary_addresses: &'a [Ipv6Addr],
adv2_delegated_prefixes: &'a [Subnet<Ipv6Addr>],
expected: Ordering,
}
#[test_case(AdvertiseMessageOrdTestCase{
adv1_non_temporary_addresses: &CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2],
adv1_delegated_prefixes: &CONFIGURED_DELEGATED_PREFIXES[0..2],
adv2_non_temporary_addresses: &CONFIGURED_NON_TEMPORARY_ADDRESSES[0..3],
adv2_delegated_prefixes: &CONFIGURED_DELEGATED_PREFIXES[0..3],
expected: Ordering::Less,
}; "adv1 has less IAs")]
#[test_case(AdvertiseMessageOrdTestCase{
adv1_non_temporary_addresses: &CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2],
adv1_delegated_prefixes: &CONFIGURED_DELEGATED_PREFIXES[0..2],
adv2_non_temporary_addresses: &CONFIGURED_NON_TEMPORARY_ADDRESSES[1..3],
adv2_delegated_prefixes: &CONFIGURED_DELEGATED_PREFIXES[1..3],
expected: Ordering::Greater,
}; "adv1 has IAs matching hint")]
#[test_case(AdvertiseMessageOrdTestCase{
adv1_non_temporary_addresses: &[],
adv1_delegated_prefixes: &CONFIGURED_DELEGATED_PREFIXES[0..3],
adv2_non_temporary_addresses: &CONFIGURED_NON_TEMPORARY_ADDRESSES[0..1],
adv2_delegated_prefixes: &CONFIGURED_DELEGATED_PREFIXES[0..1],
expected: Ordering::Less,
}; "adv1 missing IA_NA")]
#[test_case(AdvertiseMessageOrdTestCase{
adv1_non_temporary_addresses: &CONFIGURED_NON_TEMPORARY_ADDRESSES[0..3],
adv1_delegated_prefixes: &CONFIGURED_DELEGATED_PREFIXES[0..1],
adv2_non_temporary_addresses: &CONFIGURED_NON_TEMPORARY_ADDRESSES[0..3],
adv2_delegated_prefixes: &[],
expected: Ordering::Greater,
}; "adv2 missing IA_PD")]
fn advertise_message_ord(
AdvertiseMessageOrdTestCase {
adv1_non_temporary_addresses,
adv1_delegated_prefixes,
adv2_non_temporary_addresses,
adv2_delegated_prefixes,
expected,
}: AdvertiseMessageOrdTestCase<'_>,
) {
let configured_non_temporary_addresses = testutil::to_configured_addresses(
3,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
);
let configured_delegated_prefixes = testutil::to_configured_prefixes(
3,
std::iter::once(HashSet::from([CONFIGURED_DELEGATED_PREFIXES[0]])),
);
let advertise1 = AdvertiseMessage::new_default(
SERVER_ID[0],
adv1_non_temporary_addresses,
adv1_delegated_prefixes,
&[],
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
);
let advertise2 = AdvertiseMessage::new_default(
SERVER_ID[1],
adv2_non_temporary_addresses,
adv2_delegated_prefixes,
&[],
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
);
assert_eq!(advertise1.cmp(&advertise2), expected);
}
#[test_case(v6::DhcpOption::StatusCode(v6::StatusCode::Success.into(), ""); "status_code")]
#[test_case(v6::DhcpOption::ClientId(&CLIENT_ID); "client_id")]
#[test_case(v6::DhcpOption::ServerId(&SERVER_ID[0]); "server_id")]
#[test_case(v6::DhcpOption::Preference(ADVERTISE_MAX_PREFERENCE); "preference")]
#[test_case(v6::DhcpOption::SolMaxRt(*VALID_MAX_SOLICIT_TIMEOUT_RANGE.end()); "sol_max_rt")]
#[test_case(v6::DhcpOption::DnsServers(&DNS_SERVERS); "dns_servers")]
fn process_options_duplicates<'a>(opt: v6::DhcpOption<'a>) {
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
60,
60,
&[],
))];
let iaid = v6::IAID::new(0);
let options = [
v6::DhcpOption::StatusCode(v6::StatusCode::Success.into(), ""),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Preference(ADVERTISE_MAX_PREFERENCE),
v6::DhcpOption::SolMaxRt(*VALID_MAX_SOLICIT_TIMEOUT_RANGE.end()),
v6::DhcpOption::Iana(v6::IanaSerializer::new(iaid, T1.get(), T2.get(), &iana_options)),
v6::DhcpOption::DnsServers(&DNS_SERVERS),
opt,
];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let requested_ia_nas = HashMap::from([(iaid, None::<Ipv6Addr>)]);
assert_matches!(
process_options(
&msg,
ExchangeType::AdvertiseToSolicit,
Some(&CLIENT_ID),
&requested_ia_nas,
&NoIaRequested
),
Err(OptionsError::DuplicateOption(_, _, _))
);
}
#[derive(Copy, Clone)]
enum DupIaValue {
Address,
Prefix,
}
impl DupIaValue {
fn second_address(self) -> Ipv6Addr {
match self {
DupIaValue::Address => CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
DupIaValue::Prefix => CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
}
}
fn second_prefix(self) -> Subnet<Ipv6Addr> {
match self {
DupIaValue::Address => CONFIGURED_DELEGATED_PREFIXES[1],
DupIaValue::Prefix => CONFIGURED_DELEGATED_PREFIXES[0],
}
}
}
#[test_case(
DupIaValue::Address,
|res| {
assert_matches!(
res,
Err(OptionsError::IaNaError(IaOptionError::DuplicateIaValue {
value,
first_lifetimes,
second_lifetimes,
})) => {
assert_eq!(value, CONFIGURED_NON_TEMPORARY_ADDRESSES[0]);
(first_lifetimes, second_lifetimes)
}
)
}; "duplicate address")]
#[test_case(
DupIaValue::Prefix,
|res| {
assert_matches!(
res,
Err(OptionsError::IaPdError(IaPdOptionError::IaOptionError(
IaOptionError::DuplicateIaValue {
value,
first_lifetimes,
second_lifetimes,
}
))) => {
assert_eq!(value, CONFIGURED_DELEGATED_PREFIXES[0]);
(first_lifetimes, second_lifetimes)
}
)
}; "duplicate prefix")]
fn process_options_duplicate_ia_value(
dup_ia_value: DupIaValue,
check: fn(
Result<ProcessedOptions, OptionsError>,
)
-> (Result<Lifetimes, LifetimesError>, Result<Lifetimes, LifetimesError>),
) {
const IA_VALUE1_LIFETIME: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(60));
const IA_VALUE2_LIFETIME: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(100));
let iana_options = [
v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
IA_VALUE1_LIFETIME.get(),
IA_VALUE1_LIFETIME.get(),
&[],
)),
v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
dup_ia_value.second_address(),
IA_VALUE2_LIFETIME.get(),
IA_VALUE2_LIFETIME.get(),
&[],
)),
];
let iapd_options = [
v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
IA_VALUE1_LIFETIME.get(),
IA_VALUE1_LIFETIME.get(),
CONFIGURED_DELEGATED_PREFIXES[0],
&[],
)),
v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
IA_VALUE2_LIFETIME.get(),
IA_VALUE2_LIFETIME.get(),
dup_ia_value.second_prefix(),
&[],
)),
];
let iaid = v6::IAID::new(0);
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Iana(v6::IanaSerializer::new(iaid, T1.get(), T2.get(), &iana_options)),
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(iaid, T1.get(), T2.get(), &iapd_options)),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let requested_ia_nas = HashMap::from([(iaid, None::<Ipv6Addr>)]);
let (first_lifetimes, second_lifetimes) = check(process_options(
&msg,
ExchangeType::AdvertiseToSolicit,
Some(&CLIENT_ID),
&requested_ia_nas,
&NoIaRequested,
));
assert_eq!(
first_lifetimes,
Ok(Lifetimes::new_finite(IA_VALUE1_LIFETIME, IA_VALUE1_LIFETIME))
);
assert_eq!(
second_lifetimes,
Ok(Lifetimes::new_finite(IA_VALUE2_LIFETIME, IA_VALUE2_LIFETIME))
)
}
#[test]
fn process_options_t1_greather_than_t2() {
let iana_options1 = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
MEDIUM_NON_ZERO_OR_MAX_U32.get(),
MEDIUM_NON_ZERO_OR_MAX_U32.get(),
&[],
))];
let iana_options2 = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
MEDIUM_NON_ZERO_OR_MAX_U32.get(),
MEDIUM_NON_ZERO_OR_MAX_U32.get(),
&[],
))];
let iapd_options1 = [v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
LARGE_NON_ZERO_OR_MAX_U32.get(),
LARGE_NON_ZERO_OR_MAX_U32.get(),
CONFIGURED_DELEGATED_PREFIXES[0],
&[],
))];
let iapd_options2 = [v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
LARGE_NON_ZERO_OR_MAX_U32.get(),
LARGE_NON_ZERO_OR_MAX_U32.get(),
CONFIGURED_DELEGATED_PREFIXES[1],
&[],
))];
let iaid1 = v6::IAID::new(1);
let iaid2 = v6::IAID::new(2);
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
iaid1,
MEDIUM_NON_ZERO_OR_MAX_U32.get(),
SMALL_NON_ZERO_OR_MAX_U32.get(),
&iana_options1,
)),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
iaid2,
SMALL_NON_ZERO_OR_MAX_U32.get(),
MEDIUM_NON_ZERO_OR_MAX_U32.get(),
&iana_options2,
)),
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
iaid1,
LARGE_NON_ZERO_OR_MAX_U32.get(),
TINY_NON_ZERO_OR_MAX_U32.get(),
&iapd_options1,
)),
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
iaid2,
TINY_NON_ZERO_OR_MAX_U32.get(),
LARGE_NON_ZERO_OR_MAX_U32.get(),
&iapd_options2,
)),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let requested_ia_nas = HashMap::from([(iaid1, None::<Ipv6Addr>), (iaid2, None)]);
let requested_ia_pds = HashMap::from([(iaid1, None::<Subnet<Ipv6Addr>>), (iaid2, None)]);
assert_matches!(
process_options(&msg, ExchangeType::AdvertiseToSolicit, Some(&CLIENT_ID), &requested_ia_nas, &requested_ia_pds),
Ok(ProcessedOptions {
server_id: _,
solicit_max_rt_opt: _,
result: Ok(Options {
success_status_message: _,
next_contact_time: _,
non_temporary_addresses,
delegated_prefixes,
dns_servers: _,
preference: _,
}),
}) => {
assert_eq!(non_temporary_addresses, HashMap::from([(iaid2, IaOption::Success {
status_message: None,
t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(SMALL_NON_ZERO_OR_MAX_U32)),
t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(MEDIUM_NON_ZERO_OR_MAX_U32)),
ia_values: HashMap::from([(CONFIGURED_NON_TEMPORARY_ADDRESSES[1], Ok(Lifetimes{
preferred_lifetime: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(MEDIUM_NON_ZERO_OR_MAX_U32)),
valid_lifetime: v6::NonZeroTimeValue::Finite(MEDIUM_NON_ZERO_OR_MAX_U32),
}))]),
})]));
assert_eq!(delegated_prefixes, HashMap::from([(iaid2, IaOption::Success {
status_message: None,
t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(TINY_NON_ZERO_OR_MAX_U32)),
t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(LARGE_NON_ZERO_OR_MAX_U32)),
ia_values: HashMap::from([(CONFIGURED_DELEGATED_PREFIXES[1], Ok(Lifetimes{
preferred_lifetime: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(LARGE_NON_ZERO_OR_MAX_U32)),
valid_lifetime: v6::NonZeroTimeValue::Finite(LARGE_NON_ZERO_OR_MAX_U32),
}))]),
})]));
}
);
}
#[test]
fn process_options_duplicate_ia_na_id() {
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
60,
60,
&[],
))];
let iaid = v6::IAID::new(0);
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Iana(v6::IanaSerializer::new(iaid, T1.get(), T2.get(), &iana_options)),
v6::DhcpOption::Iana(v6::IanaSerializer::new(iaid, T1.get(), T2.get(), &iana_options)),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let requested_ia_nas = HashMap::from([(iaid, None::<Ipv6Addr>)]);
assert_matches!(
process_options(&msg, ExchangeType::AdvertiseToSolicit, Some(&CLIENT_ID), &requested_ia_nas, &NoIaRequested),
Err(OptionsError::DuplicateIaNaId(got_iaid, _, _)) if got_iaid == iaid
);
}
#[test]
fn process_options_missing_server_id() {
let options = [v6::DhcpOption::ClientId(&CLIENT_ID)];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(
process_options(
&msg,
ExchangeType::AdvertiseToSolicit,
Some(&CLIENT_ID),
&NoIaRequested,
&NoIaRequested
),
Err(OptionsError::MissingServerId)
);
}
#[test]
fn process_options_missing_client_id() {
let options = [v6::DhcpOption::ServerId(&SERVER_ID[0])];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(
process_options(
&msg,
ExchangeType::AdvertiseToSolicit,
Some(&CLIENT_ID),
&NoIaRequested,
&NoIaRequested
),
Err(OptionsError::MissingClientId)
);
}
#[test]
fn process_options_mismatched_client_id() {
let options = [
v6::DhcpOption::ClientId(&MISMATCHED_CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(
process_options(&msg, ExchangeType::AdvertiseToSolicit, Some(&CLIENT_ID), &NoIaRequested, &NoIaRequested),
Err(OptionsError::MismatchedClientId { got, want })
if got[..] == MISMATCHED_CLIENT_ID && want == CLIENT_ID
);
}
#[test]
fn process_options_unexpected_client_id() {
let options =
[v6::DhcpOption::ClientId(&CLIENT_ID), v6::DhcpOption::ServerId(&SERVER_ID[0])];
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(
process_options(&msg, ExchangeType::ReplyToInformationRequest, None, &NoIaRequested, &NoIaRequested),
Err(OptionsError::UnexpectedClientId(got))
if got[..] == CLIENT_ID
);
}
#[test_case(
v6::MessageType::Reply,
ExchangeType::ReplyToInformationRequest,
v6::DhcpOption::Preference(ADVERTISE_MAX_PREFERENCE);
"reply_to_information_request_preference"
)]
#[test_case(
v6::MessageType::Reply,
ExchangeType::ReplyToInformationRequest,
v6::DhcpOption::Iana(v6::IanaSerializer::new(v6::IAID::new(0), T1.get(),T2.get(), &[]));
"reply_to_information_request_ia_na"
)]
#[test_case(
v6::MessageType::Advertise,
ExchangeType::AdvertiseToSolicit,
v6::DhcpOption::InformationRefreshTime(42u32);
"advertise_to_solicit_information_refresh_time"
)]
#[test_case(
v6::MessageType::Reply,
ExchangeType::ReplyWithLeases(RequestLeasesMessageType::Request),
v6::DhcpOption::Preference(ADVERTISE_MAX_PREFERENCE);
"reply_to_request_preference"
)]
fn process_options_invalid<'a>(
message_type: v6::MessageType,
exchange_type: ExchangeType,
opt: v6::DhcpOption<'a>,
) {
let options =
[v6::DhcpOption::ClientId(&CLIENT_ID), v6::DhcpOption::ServerId(&SERVER_ID[0]), opt];
let builder = v6::MessageBuilder::new(message_type, [0, 1, 2], &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(
process_options(&msg, exchange_type, Some(&CLIENT_ID), &NoIaRequested, &NoIaRequested),
Err(OptionsError::InvalidOption(_))
);
}
mod process_reply_with_leases_unexpected_iaid {
use super::*;
use test_case::test_case;
const EXPECTED_IAID: v6::IAID = v6::IAID::new(1);
const UNEXPECTED_IAID: v6::IAID = v6::IAID::new(2);
struct TestCase {
assigned_addresses: fn(Instant) -> HashMap<v6::IAID, AddressEntry<Instant>>,
assigned_prefixes: fn(Instant) -> HashMap<v6::IAID, PrefixEntry<Instant>>,
check_res: fn(Result<ProcessedReplyWithLeases<Instant>, ReplyWithLeasesError>),
}
fn expected_iaids<V: IaValueTestExt>(
time: Instant,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
HashMap::from([(
EXPECTED_IAID,
IaEntry::new_assigned(V::CONFIGURED[0], PREFERRED_LIFETIME, VALID_LIFETIME, time),
)])
}
fn unexpected_iaids<V: IaValueTestExt>(
time: Instant,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
[EXPECTED_IAID, UNEXPECTED_IAID]
.into_iter()
.enumerate()
.map(|(i, iaid)| {
(
iaid,
IaEntry::new_assigned(
V::CONFIGURED[i],
PREFERRED_LIFETIME,
VALID_LIFETIME,
time,
),
)
})
.collect()
}
#[test_case(
TestCase {
assigned_addresses: expected_iaids::<Ipv6Addr>,
assigned_prefixes: unexpected_iaids::<Subnet<Ipv6Addr>>,
check_res: |res| {
assert_matches!(
res,
Err(ReplyWithLeasesError::OptionsError(
OptionsError::UnexpectedIaNa(iaid, _),
)) => {
assert_eq!(iaid, UNEXPECTED_IAID);
}
);
},
}
; "unknown IA_NA IAID")]
#[test_case(
TestCase {
assigned_addresses: unexpected_iaids::<Ipv6Addr>,
assigned_prefixes: expected_iaids::<Subnet<Ipv6Addr>>,
check_res: |res| {
assert_matches!(
res,
Err(ReplyWithLeasesError::OptionsError(
OptionsError::UnexpectedIaPd(iaid, _),
)) => {
assert_eq!(iaid, UNEXPECTED_IAID);
}
);
},
}
; "unknown IA_PD IAID")]
fn test(TestCase { assigned_addresses, assigned_prefixes, check_res }: TestCase) {
let options =
[v6::DhcpOption::ClientId(&CLIENT_ID), v6::DhcpOption::ServerId(&SERVER_ID[0])]
.into_iter()
.chain([EXPECTED_IAID, UNEXPECTED_IAID].into_iter().map(|iaid| {
v6::DhcpOption::Iana(v6::IanaSerializer::new(iaid, T1.get(), T2.get(), &[]))
}))
.chain([EXPECTED_IAID, UNEXPECTED_IAID].into_iter().map(|iaid| {
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(iaid, T1.get(), T2.get(), &[]))
}))
.collect::<Vec<_>>();
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, [0, 1, 2], options.as_slice());
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let mut solicit_max_rt = MAX_SOLICIT_TIMEOUT;
let time = Instant::now();
check_res(process_reply_with_leases(
&CLIENT_ID,
&SERVER_ID[0],
&assigned_addresses(time),
&assigned_prefixes(time),
&mut solicit_max_rt,
&msg,
RequestLeasesMessageType::Request,
time,
))
}
}
#[test]
fn ignore_advertise_with_unknown_ia() {
let time = Instant::now();
let mut client = testutil::start_and_assert_server_discovery(
[0, 1, 2],
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
1,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
),
Default::default(),
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
time,
);
let iana_options_0 = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
60,
60,
&[],
))];
let iana_options_99 = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
60,
60,
&[],
))];
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Preference(42),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options_0,
)),
// An IA_NA with an IAID that was not included in the sent solicit
// message.
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(99),
T1.get(),
T2.get(),
&iana_options_99,
)),
];
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let builder =
v6::MessageBuilder::new(v6::MessageType::Advertise, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
// The client should have dropped the Advertise with the unrecognized
// IA_NA IAID.
assert_eq!(client.handle_message_receive(msg, time), []);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
assert_matches!(
state,
Some(ClientState::ServerDiscovery(ServerDiscovery {
client_id: _,
configured_non_temporary_addresses: _,
configured_delegated_prefixes: _,
first_solicit_time: _,
retrans_timeout: _,
solicit_max_rt: _,
collected_advertise,
collected_sol_max_rt: _,
})) => {
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
}
);
}
#[test]
fn receive_advertise_with_max_preference() {
let time = Instant::now();
let mut client = testutil::start_and_assert_server_discovery(
[0, 1, 2],
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
2,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
),
Default::default(),
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
time,
);
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
60,
60,
&[],
))];
// The client should stay in ServerDiscovery when it gets an Advertise
// with:
// - Preference < 255 & and at least one IA, or...
// - Preference == 255 but no IAs
for (preference, iana) in [
(
42,
Some(v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
))),
),
(255, None),
]
.into_iter()
{
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Preference(preference),
]
.into_iter()
.chain(iana)
.collect::<Vec<_>>();
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let builder =
v6::MessageBuilder::new(v6::MessageType::Advertise, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_eq!(client.handle_message_receive(msg, time), []);
}
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
60,
60,
&[],
))];
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Preference(255),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
)),
];
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let builder =
v6::MessageBuilder::new(v6::MessageType::Advertise, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
// The client should transition to Requesting when receiving a complete
// advertise with preference 255.
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } = client;
let Requesting {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
collected_advertise: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
} = assert_matches!(
state,
Some(ClientState::Requesting(requesting)) => requesting
);
let buf = assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(*instant, time.add(INITIAL_REQUEST_TIMEOUT));
buf
}
);
assert_eq!(testutil::msg_type(buf), v6::MessageType::Request);
}
// T1 and T2 are non-zero and T1 > T2, the client should ignore this IA_NA option.
#[test_case(T2.get() + 1, T2.get(), true)]
#[test_case(INFINITY, T2.get(), true)]
// T1 > T2, but T2 is zero, the client should process this IA_NA option.
#[test_case(T1.get(), 0, false)]
// T1 is zero, the client should process this IA_NA option.
#[test_case(0, T2.get(), false)]
// T1 <= T2, the client should process this IA_NA option.
#[test_case(T1.get(), T2.get(), false)]
#[test_case(T1.get(), INFINITY, false)]
#[test_case(INFINITY, INFINITY, false)]
fn receive_advertise_with_invalid_iana(t1: u32, t2: u32, ignore_iana: bool) {
let transaction_id = [0, 1, 2];
let time = Instant::now();
let mut client = testutil::start_and_assert_server_discovery(
transaction_id,
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
1,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
),
Default::default(),
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
time,
);
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
PREFERRED_LIFETIME.get(),
VALID_LIFETIME.get(),
&[],
))];
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Iana(v6::IanaSerializer::new(v6::IAID::new(0), t1, t2, &iana_options)),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Advertise, transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(client.handle_message_receive(msg, time)[..], []);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let collected_advertise = assert_matches!(
state,
Some(ClientState::ServerDiscovery(ServerDiscovery {
client_id: _,
configured_non_temporary_addresses: _,
configured_delegated_prefixes: _,
first_solicit_time: _,
retrans_timeout: _,
solicit_max_rt: _,
collected_advertise,
collected_sol_max_rt: _,
})) => collected_advertise
);
match ignore_iana {
true => assert!(collected_advertise.is_empty(), "{:?}", collected_advertise),
false => {
assert_matches!(
collected_advertise.peek(),
Some(AdvertiseMessage {
server_id: _,
non_temporary_addresses,
delegated_prefixes: _,
dns_servers: _,
preference: _,
receive_time: _,
preferred_non_temporary_addresses_count: _,
preferred_delegated_prefixes_count: _,
}) => {
assert_eq!(
non_temporary_addresses,
&HashMap::from([(
v6::IAID::new(0),
HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])
)])
);
}
)
}
}
}
#[test]
fn select_first_server_while_retransmitting() {
let time = Instant::now();
let mut client = testutil::start_and_assert_server_discovery(
[0, 1, 2],
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
1,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
),
Default::default(),
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
time,
);
// On transmission timeout, if no advertise were received the client
// should stay in server discovery and resend solicit.
let actions = client.handle_timeout(ClientTimerType::Retransmission, time);
assert_matches!(
&actions[..],
[
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(testutil::msg_type(buf), v6::MessageType::Solicit);
assert_eq!(*instant, time.add(2 * INITIAL_SOLICIT_TIMEOUT));
buf
}
);
let ClientStateMachine { transaction_id, options_to_request: _, state, rng: _ } = &client;
{
let ServerDiscovery {
client_id: _,
configured_non_temporary_addresses: _,
configured_delegated_prefixes: _,
first_solicit_time: _,
retrans_timeout: _,
solicit_max_rt: _,
collected_advertise,
collected_sol_max_rt: _,
} = assert_matches!(
state,
Some(ClientState::ServerDiscovery(server_discovery)) => server_discovery
);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
}
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
60,
60,
&[],
))];
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
)),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Advertise, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
// The client should transition to Requesting when receiving any
// advertise while retransmitting.
let actions = client.handle_message_receive(msg, time);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(*instant, time.add(INITIAL_REQUEST_TIMEOUT));
assert_eq!(testutil::msg_type(buf), v6::MessageType::Request);
}
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } = client;
let Requesting {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
} = assert_matches!(
state,
Some(ClientState::Requesting(requesting )) => requesting
);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
}
#[test]
fn send_request() {
let (mut _client, _transaction_id) = testutil::request_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
CONFIGURED_NON_TEMPORARY_ADDRESSES.into_iter().map(TestIaNa::new_default).collect(),
CONFIGURED_DELEGATED_PREFIXES.into_iter().map(TestIaPd::new_default).collect(),
&[],
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
}
// TODO(https://fxbug.dev/42060598): Refactor this test into independent test cases.
#[test]
fn requesting_receive_reply_with_failure_status_code() {
let options_to_request = vec![];
let configured_non_temporary_addresses = testutil::to_configured_addresses(1, vec![]);
let advertised_non_temporary_addresses = [CONFIGURED_NON_TEMPORARY_ADDRESSES[0]];
let configured_delegated_prefixes = HashMap::new();
let mut want_collected_advertise = [
AdvertiseMessage::new_default(
SERVER_ID[1],
&CONFIGURED_NON_TEMPORARY_ADDRESSES[1..=1],
&[],
&[],
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
),
AdvertiseMessage::new_default(
SERVER_ID[2],
&CONFIGURED_NON_TEMPORARY_ADDRESSES[2..=2],
&[],
&[],
&configured_non_temporary_addresses,
&configured_delegated_prefixes,
),
]
.into_iter()
.collect::<BinaryHeap<_>>();
let mut rng = StepRng::new(u64::MAX / 2, 0);
let time = Instant::now();
let Transition { state, actions: _, transaction_id } = Requesting::start(
CLIENT_ID.into(),
SERVER_ID[0].to_vec(),
advertise_to_ia_entries(
testutil::to_default_ias_map(&advertised_non_temporary_addresses),
configured_non_temporary_addresses.clone(),
),
Default::default(), /* delegated_prefixes */
&options_to_request[..],
want_collected_advertise.clone(),
MAX_SOLICIT_TIMEOUT,
&mut rng,
time,
);
let expected_non_temporary_addresses = (0..)
.map(v6::IAID::new)
.zip(
advertised_non_temporary_addresses
.iter()
.map(|addr| AddressEntry::ToRequest(HashSet::from([*addr]))),
)
.collect::<HashMap<v6::IAID, AddressEntry<_>>>();
{
let Requesting {
non_temporary_addresses: got_non_temporary_addresses,
delegated_prefixes: _,
server_id,
collected_advertise,
client_id: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
} = assert_matches!(&state, ClientState::Requesting(requesting) => requesting);
assert_eq!(server_id[..], SERVER_ID[0]);
assert_eq!(*got_non_temporary_addresses, expected_non_temporary_addresses);
assert_eq!(
collected_advertise.clone().into_sorted_vec(),
want_collected_advertise.clone().into_sorted_vec()
);
}
// If the reply contains a top level UnspecFail status code, the reply
// should be ignored.
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&[],
)),
v6::DhcpOption::StatusCode(v6::ErrorStatusCode::UnspecFail.into(), ""),
];
let request_transaction_id = transaction_id.unwrap();
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, request_transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions, transaction_id: got_transaction_id } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
{
let Requesting {
client_id: _,
non_temporary_addresses: got_non_temporary_addresses,
delegated_prefixes: _,
server_id,
collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
} = assert_matches!(&state, ClientState::Requesting(requesting) => requesting);
assert_eq!(server_id[..], SERVER_ID[0]);
assert_eq!(
collected_advertise.clone().into_sorted_vec(),
want_collected_advertise.clone().into_sorted_vec()
);
assert_eq!(*got_non_temporary_addresses, expected_non_temporary_addresses);
}
assert_eq!(got_transaction_id, None);
assert_eq!(actions[..], []);
// If the reply contains a top level NotOnLink status code, the
// request should be resent without specifying any addresses.
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&[],
)),
v6::DhcpOption::StatusCode(v6::ErrorStatusCode::NotOnLink.into(), ""),
];
let request_transaction_id = transaction_id.unwrap();
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, request_transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions: _, transaction_id } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
let expected_non_temporary_addresses: HashMap<v6::IAID, AddressEntry<_>> =
HashMap::from([(v6::IAID::new(0), AddressEntry::ToRequest(Default::default()))]);
{
let Requesting {
client_id: _,
non_temporary_addresses: got_non_temporary_addresses,
delegated_prefixes: _,
server_id,
collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
} = assert_matches!(
&state,
ClientState::Requesting(requesting) => requesting
);
assert_eq!(server_id[..], SERVER_ID[0]);
assert_eq!(
collected_advertise.clone().into_sorted_vec(),
want_collected_advertise.clone().into_sorted_vec()
);
assert_eq!(*got_non_temporary_addresses, expected_non_temporary_addresses);
}
assert!(transaction_id.is_some());
// If the reply contains no usable addresses, the client selects
// another server and sends a request to it.
let iana_options =
[v6::DhcpOption::StatusCode(v6::ErrorStatusCode::NoAddrsAvail.into(), "")];
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
)),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, transaction_id.unwrap(), &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions, transaction_id } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
{
let Requesting {
server_id,
collected_advertise,
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
} = assert_matches!(
state,
ClientState::Requesting(requesting) => requesting
);
assert_eq!(server_id[..], SERVER_ID[1]);
let _: Option<AdvertiseMessage<_>> = want_collected_advertise.pop();
assert_eq!(
collected_advertise.clone().into_sorted_vec(),
want_collected_advertise.clone().into_sorted_vec(),
);
}
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(_buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(*instant, time.add(INITIAL_REQUEST_TIMEOUT));
}
);
assert!(transaction_id.is_some());
}
#[test]
fn requesting_receive_reply_with_ia_not_on_link() {
let options_to_request = vec![];
let configured_non_temporary_addresses = testutil::to_configured_addresses(
2,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
);
let mut rng = StepRng::new(u64::MAX / 2, 0);
let time = Instant::now();
let Transition { state, actions: _, transaction_id } = Requesting::start(
CLIENT_ID.into(),
SERVER_ID[0].to_vec(),
advertise_to_ia_entries(
testutil::to_default_ias_map(&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]),
configured_non_temporary_addresses.clone(),
),
Default::default(), /* delegated_prefixes */
&options_to_request[..],
BinaryHeap::new(),
MAX_SOLICIT_TIMEOUT,
&mut rng,
time,
);
// If the reply contains an address with status code NotOnLink, the
// client should request the IAs without specifying any addresses in
// subsequent messages.
let iana_options1 = [v6::DhcpOption::StatusCode(v6::ErrorStatusCode::NotOnLink.into(), "")];
let iana_options2 = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
PREFERRED_LIFETIME.get(),
VALID_LIFETIME.get(),
&[],
))];
let iaid1 = v6::IAID::new(0);
let iaid2 = v6::IAID::new(1);
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
iaid1,
T1.get(),
T2.get(),
&iana_options1,
)),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
iaid2,
T1.get(),
T2.get(),
&iana_options2,
)),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, transaction_id.unwrap(), &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions, transaction_id } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
let expected_non_temporary_addresses = HashMap::from([
(iaid1, AddressEntry::ToRequest(Default::default())),
(
iaid2,
AddressEntry::Assigned(HashMap::from([(
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
LifetimesInfo {
lifetimes: Lifetimes {
preferred_lifetime: v6::TimeValue::NonZero(
v6::NonZeroTimeValue::Finite(PREFERRED_LIFETIME),
),
valid_lifetime: v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
},
updated_at: time,
},
)])),
),
]);
{
let Assigned {
client_id: _,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers: _,
solicit_max_rt: _,
_marker,
} = assert_matches!(
state,
ClientState::Assigned(assigned) => assigned
);
assert_eq!(server_id[..], SERVER_ID[0]);
assert_eq!(non_temporary_addresses, expected_non_temporary_addresses);
assert_eq!(delegated_prefixes, HashMap::new());
}
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Renew, t1),
Action::ScheduleTimer(ClientTimerType::Rebind, t2),
Action::IaNaUpdates(iana_updates),
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, restart_time),
] => {
assert_eq!(*t1, time.add(Duration::from_secs(T1.get().into())));
assert_eq!(*t2, time.add(Duration::from_secs(T2.get().into())));
assert_eq!(
*restart_time,
time.add(Duration::from_secs(VALID_LIFETIME.get().into())),
);
assert_eq!(
iana_updates,
&HashMap::from([(
iaid2,
HashMap::from([(
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
IaValueUpdateKind::Added(Lifetimes::new_default()),
)]),
)]),
);
}
);
assert!(transaction_id.is_none());
}
#[test_case(0, VALID_LIFETIME.get(), true)]
#[test_case(PREFERRED_LIFETIME.get(), 0, false)]
#[test_case(VALID_LIFETIME.get() + 1, VALID_LIFETIME.get(), false)]
#[test_case(0, 0, false)]
#[test_case(PREFERRED_LIFETIME.get(), VALID_LIFETIME.get(), true)]
fn requesting_receive_reply_with_invalid_ia_lifetimes(
preferred_lifetime: u32,
valid_lifetime: u32,
valid_ia: bool,
) {
let options_to_request = vec![];
let configured_non_temporary_addresses = testutil::to_configured_addresses(1, vec![]);
let mut rng = StepRng::new(u64::MAX / 2, 0);
let time = Instant::now();
let Transition { state, actions: _, transaction_id } = Requesting::start(
CLIENT_ID.into(),
SERVER_ID[0].to_vec(),
advertise_to_ia_entries(
testutil::to_default_ias_map(&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..1]),
configured_non_temporary_addresses.clone(),
),
Default::default(), /* delegated_prefixes */
&options_to_request[..],
BinaryHeap::new(),
MAX_SOLICIT_TIMEOUT,
&mut rng,
time,
);
// The client should discard the IAs with invalid lifetimes.
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
preferred_lifetime,
valid_lifetime,
&[],
))];
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
)),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, transaction_id.unwrap(), &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions: _, transaction_id: _ } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
match valid_ia {
true =>
// The client should transition to Assigned if the reply contains
// a valid IA.
{
let Assigned {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers: _,
solicit_max_rt: _,
_marker,
} = assert_matches!(
state,
ClientState::Assigned(assigned) => assigned
);
}
false =>
// The client should transition to ServerDiscovery if the reply contains
// no valid IAs.
{
let ServerDiscovery {
client_id: _,
configured_non_temporary_addresses: _,
configured_delegated_prefixes: _,
first_solicit_time: _,
retrans_timeout: _,
solicit_max_rt: _,
collected_advertise,
collected_sol_max_rt: _,
} = assert_matches!(
state,
ClientState::ServerDiscovery(server_discovery) => server_discovery
);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
}
}
}
// Test that T1/T2 are calculated correctly on receiving a Reply to Request.
#[test]
fn compute_t1_t2_on_reply_to_request() {
let mut rng = StepRng::new(u64::MAX / 2, 0);
for (
(ia1_preferred_lifetime, ia1_valid_lifetime, ia1_t1, ia1_t2),
(ia2_preferred_lifetime, ia2_valid_lifetime, ia2_t1, ia2_t2),
expected_t1,
expected_t2,
) in vec![
// If T1/T2 are 0, they should be computed as as 0.5 * minimum
// preferred lifetime, and 0.8 * minimum preferred lifetime
// respectively.
(
(100, 160, 0, 0),
(120, 180, 0, 0),
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(50).expect("should succeed")),
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(80).expect("should succeed")),
),
(
(INFINITY, INFINITY, 0, 0),
(120, 180, 0, 0),
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(60).expect("should succeed")),
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(96).expect("should succeed")),
),
// If T1/T2 are 0, and the minimum preferred lifetime, is infinity,
// T1/T2 should also be infinity.
(
(INFINITY, INFINITY, 0, 0),
(INFINITY, INFINITY, 0, 0),
v6::NonZeroTimeValue::Infinity,
v6::NonZeroTimeValue::Infinity,
),
// T2 may be infinite if T1 is finite.
(
(INFINITY, INFINITY, 50, INFINITY),
(INFINITY, INFINITY, 50, INFINITY),
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(50).expect("should succeed")),
v6::NonZeroTimeValue::Infinity,
),
// If T1/T2 are set, and have different values across IAs, T1/T2
// should be computed as the minimum T1/T2. NOTE: the server should
// send the same T1/T2 across all IA, but the client should be
// prepared for the server sending different T1/T2 values.
(
(100, 160, 40, 70),
(120, 180, 50, 80),
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(40).expect("should succeed")),
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(70).expect("should succeed")),
),
] {
let time = Instant::now();
let Transition { state, actions: _, transaction_id } = Requesting::start(
CLIENT_ID.into(),
SERVER_ID[0].to_vec(),
advertise_to_ia_entries(
testutil::to_default_ias_map(&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]),
testutil::to_configured_addresses(
2,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
),
),
Default::default(), /* delegated_prefixes */
&[],
BinaryHeap::new(),
MAX_SOLICIT_TIMEOUT,
&mut rng,
time,
);
let iana_options1 = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
ia1_preferred_lifetime,
ia1_valid_lifetime,
&[],
))];
let iana_options2 = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
ia2_preferred_lifetime,
ia2_valid_lifetime,
&[],
))];
let iaid1 = v6::IAID::new(0);
let iaid2 = v6::IAID::new(1);
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
iaid1,
ia1_t1,
ia1_t2,
&iana_options1,
)),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
iaid2,
ia2_t1,
ia2_t2,
&iana_options2,
)),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, transaction_id.unwrap(), &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions, transaction_id: _ } =
state.reply_message_received(&[], &mut rng, msg, time);
let Assigned {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers: _,
solicit_max_rt: _,
_marker,
} = assert_matches!(
state,
ClientState::Assigned(assigned) => assigned
);
let update_actions = [Action::IaNaUpdates(HashMap::from([
(
iaid1,
HashMap::from([(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
IaValueUpdateKind::Added(Lifetimes::new(
ia1_preferred_lifetime,
ia1_valid_lifetime,
)),
)]),
),
(
iaid2,
HashMap::from([(
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
IaValueUpdateKind::Added(Lifetimes::new(
ia2_preferred_lifetime,
ia2_valid_lifetime,
)),
)]),
),
]))];
let timer_action = |timer, tv| match tv {
v6::NonZeroTimeValue::Finite(tv) => {
Action::ScheduleTimer(timer, time.add(Duration::from_secs(tv.get().into())))
}
v6::NonZeroTimeValue::Infinity => Action::CancelTimer(timer),
};
let non_zero_time_value = |v| {
assert_matches!(
v6::TimeValue::new(v),
v6::TimeValue::NonZero(v) => v
)
};
assert!(expected_t1 <= expected_t2);
assert_eq!(
actions,
[
Action::CancelTimer(ClientTimerType::Retransmission),
timer_action(ClientTimerType::Renew, expected_t1),
timer_action(ClientTimerType::Rebind, expected_t2),
]
.into_iter()
.chain(update_actions)
.chain([timer_action(
ClientTimerType::RestartServerDiscovery,
std::cmp::max(
non_zero_time_value(ia1_valid_lifetime),
non_zero_time_value(ia2_valid_lifetime),
),
)])
.collect::<Vec<_>>(),
);
}
}
#[test]
fn use_advertise_from_best_server() {
let transaction_id = [0, 1, 2];
let time = Instant::now();
let mut client = testutil::start_and_assert_server_discovery(
transaction_id,
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
CONFIGURED_NON_TEMPORARY_ADDRESSES.len(),
CONFIGURED_NON_TEMPORARY_ADDRESSES.map(|a| HashSet::from([a])),
),
testutil::to_configured_prefixes(
CONFIGURED_DELEGATED_PREFIXES.len(),
CONFIGURED_DELEGATED_PREFIXES.map(|a| HashSet::from([a])),
),
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
time,
);
// Server0 advertises only IA_NA but all matching our hints.
let buf = TestMessageBuilder {
transaction_id,
message_type: v6::MessageType::Advertise,
client_id: &CLIENT_ID,
server_id: &SERVER_ID[0],
preference: None,
dns_servers: None,
ia_nas: (0..)
.map(v6::IAID::new)
.zip(CONFIGURED_NON_TEMPORARY_ADDRESSES)
.map(|(iaid, value)| (iaid, TestIa::new_default(value))),
ia_pds: std::iter::empty(),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(client.handle_message_receive(msg, time)[..], []);
// Server1 advertises only IA_PD but all matching our hints.
let buf = TestMessageBuilder {
transaction_id,
message_type: v6::MessageType::Advertise,
client_id: &CLIENT_ID,
server_id: &SERVER_ID[1],
preference: None,
dns_servers: None,
ia_nas: std::iter::empty(),
ia_pds: (0..)
.map(v6::IAID::new)
.zip(CONFIGURED_DELEGATED_PREFIXES)
.map(|(iaid, value)| (iaid, TestIa::new_default(value))),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(client.handle_message_receive(msg, time)[..], []);
// Server2 advertises only a single IA_NA and IA_PD but not matching our
// hint.
//
// This should be the best advertisement the client receives since it
// allows the client to get the most diverse set of IAs which the client
// prefers over a large quantity of a single IA type.
let buf = TestMessageBuilder {
transaction_id,
message_type: v6::MessageType::Advertise,
client_id: &CLIENT_ID,
server_id: &SERVER_ID[2],
preference: None,
dns_servers: None,
ia_nas: std::iter::once((
v6::IAID::new(0),
TestIa {
values: HashMap::from([(
REPLY_NON_TEMPORARY_ADDRESSES[0],
Lifetimes {
preferred_lifetime: v6::TimeValue::NonZero(
v6::NonZeroTimeValue::Finite(PREFERRED_LIFETIME),
),
valid_lifetime: v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
},
)]),
t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
},
)),
ia_pds: std::iter::once((
v6::IAID::new(0),
TestIa {
values: HashMap::from([(
REPLY_DELEGATED_PREFIXES[0],
Lifetimes {
preferred_lifetime: v6::TimeValue::NonZero(
v6::NonZeroTimeValue::Finite(PREFERRED_LIFETIME),
),
valid_lifetime: v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
},
)]),
t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
},
)),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(client.handle_message_receive(msg, time)[..], []);
// Handle the retransmission timeout for the first time which should
// pick a server and transition to requesting with the best server.
//
// The best server should be `SERVER_ID[2]` and we should have replaced
// our hint for IA_NA/IA_PD with IAID == 0 to what was in the server's
// advertise message. We keep the hints for the other IAIDs since the
// server did not include those IAID in its advertise so the client will
// continue to request the hints with the selected server.
let actions = client.handle_timeout(ClientTimerType::Retransmission, time);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant),
] => {
assert_eq!(testutil::msg_type(buf), v6::MessageType::Request);
assert_eq!(*instant, time.add(INITIAL_REQUEST_TIMEOUT));
}
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } = client;
assert_matches!(
state,
Some(ClientState::Requesting(Requesting {
client_id: _,
non_temporary_addresses,
delegated_prefixes,
server_id,
collected_advertise: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt: _,
})) => {
assert_eq!(&server_id, &SERVER_ID[2]);
assert_eq!(
non_temporary_addresses,
[REPLY_NON_TEMPORARY_ADDRESSES[0]]
.iter()
.chain(CONFIGURED_NON_TEMPORARY_ADDRESSES[1..3].iter())
.enumerate().map(|(iaid, addr)| {
(v6::IAID::new(iaid.try_into().unwrap()), AddressEntry::ToRequest(HashSet::from([*addr])))
}).collect::<HashMap<_, _>>()
);
assert_eq!(
delegated_prefixes,
[REPLY_DELEGATED_PREFIXES[0]]
.iter()
.chain(CONFIGURED_DELEGATED_PREFIXES[1..3].iter())
.enumerate().map(|(iaid, addr)| {
(v6::IAID::new(iaid.try_into().unwrap()), PrefixEntry::ToRequest(HashSet::from([*addr])))
}).collect::<HashMap<_, _>>()
);
}
);
}
// Test that Request retransmission respects max retransmission count.
#[test]
fn requesting_retransmit_max_retrans_count() {
let transaction_id = [0, 1, 2];
let time = Instant::now();
let mut client = testutil::start_and_assert_server_discovery(
transaction_id,
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
1,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
),
Default::default(),
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
time,
);
for i in 0..2 {
let buf = TestMessageBuilder {
transaction_id,
message_type: v6::MessageType::Advertise,
client_id: &CLIENT_ID,
server_id: &SERVER_ID[i],
preference: None,
dns_servers: None,
ia_nas: std::iter::once((
v6::IAID::new(0),
TestIa::new_default(CONFIGURED_NON_TEMPORARY_ADDRESSES[i]),
)),
ia_pds: std::iter::empty(),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert_matches!(client.handle_message_receive(msg, time)[..], []);
}
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let ServerDiscovery {
client_id: _,
configured_non_temporary_addresses: _,
configured_delegated_prefixes: _,
first_solicit_time: _,
retrans_timeout: _,
solicit_max_rt: _,
collected_advertise: want_collected_advertise,
collected_sol_max_rt: _,
} = assert_matches!(
state,
Some(ClientState::ServerDiscovery(server_discovery)) => server_discovery
);
let mut want_collected_advertise = want_collected_advertise.clone();
let _: Option<AdvertiseMessage<_>> = want_collected_advertise.pop();
// The client should transition to Requesting and select the server that
// sent the best advertise.
assert_matches!(
&client.handle_timeout(ClientTimerType::Retransmission, time)[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(testutil::msg_type(buf), v6::MessageType::Request);
assert_eq!(*instant, time.add(INITIAL_REQUEST_TIMEOUT));
}
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
{
let Requesting {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id,
collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count,
solicit_max_rt: _,
} = assert_matches!(state, Some(ClientState::Requesting(requesting)) => requesting);
assert_eq!(
collected_advertise.clone().into_sorted_vec(),
want_collected_advertise.clone().into_sorted_vec()
);
assert_eq!(server_id[..], SERVER_ID[0]);
assert_eq!(*transmission_count, 1);
}
for count in 2..=(REQUEST_MAX_RC + 1) {
assert_matches!(
&client.handle_timeout(ClientTimerType::Retransmission, time)[..],
[
Action::SendMessage(buf),
// `_timeout` is not checked because retransmission timeout
// calculation is covered in its respective test.
Action::ScheduleTimer(ClientTimerType::Retransmission, _timeout)
] if testutil::msg_type(buf) == v6::MessageType::Request
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let Requesting {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id,
collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count,
solicit_max_rt: _,
} = assert_matches!(state, Some(ClientState::Requesting(requesting)) => requesting);
assert_eq!(
collected_advertise.clone().into_sorted_vec(),
want_collected_advertise.clone().into_sorted_vec()
);
assert_eq!(server_id[..], SERVER_ID[0]);
assert_eq!(*transmission_count, count);
}
// When the retransmission count reaches REQUEST_MAX_RC, the client
// should select another server.
assert_matches!(
&client.handle_timeout(ClientTimerType::Retransmission, time)[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(testutil::msg_type(buf), v6::MessageType::Request);
assert_eq!(*instant, time.add(INITIAL_REQUEST_TIMEOUT));
}
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let Requesting {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id,
collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count,
solicit_max_rt: _,
} = assert_matches!(state, Some(ClientState::Requesting(requesting)) => requesting);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
assert_eq!(server_id[..], SERVER_ID[1]);
assert_eq!(*transmission_count, 1);
for count in 2..=(REQUEST_MAX_RC + 1) {
assert_matches!(
&client.handle_timeout(ClientTimerType::Retransmission, time)[..],
[
Action::SendMessage(buf),
// `_timeout` is not checked because retransmission timeout
// calculation is covered in its respective test.
Action::ScheduleTimer(ClientTimerType::Retransmission, _timeout)
] if testutil::msg_type(buf) == v6::MessageType::Request
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let Requesting {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id,
collected_advertise,
first_request_time: _,
retrans_timeout: _,
transmission_count,
solicit_max_rt: _,
} = assert_matches!(state, Some(ClientState::Requesting(requesting)) => requesting);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
assert_eq!(server_id[..], SERVER_ID[1]);
assert_eq!(*transmission_count, count);
}
// When the retransmission count reaches REQUEST_MAX_RC, and the client
// does not have information about another server, the client should
// restart server discovery.
assert_matches!(
&client.handle_timeout(ClientTimerType::Retransmission, time)[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::CancelTimer(ClientTimerType::Refresh),
Action::CancelTimer(ClientTimerType::Renew),
Action::CancelTimer(ClientTimerType::Rebind),
Action::CancelTimer(ClientTimerType::RestartServerDiscovery),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(testutil::msg_type(buf), v6::MessageType::Solicit);
assert_eq!(*instant, time.add(INITIAL_SOLICIT_TIMEOUT));
}
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } = client;
assert_matches!(state,
Some(ClientState::ServerDiscovery(ServerDiscovery {
client_id: _,
configured_non_temporary_addresses: _,
configured_delegated_prefixes: _,
first_solicit_time: _,
retrans_timeout: _,
solicit_max_rt: _,
collected_advertise,
collected_sol_max_rt: _,
})) if collected_advertise.is_empty()
);
}
// Test 4-msg exchange for assignment.
#[test]
fn assignment() {
let now = Instant::now();
let (client, actions) = testutil::assign_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]
.iter()
.copied()
.map(TestIaNa::new_default)
.collect(),
CONFIGURED_DELEGATED_PREFIXES[0..2]
.iter()
.copied()
.map(TestIaPd::new_default)
.collect(),
&[],
StepRng::new(u64::MAX / 2, 0),
now,
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let Assigned {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers: _,
solicit_max_rt: _,
_marker,
} = assert_matches!(
state,
Some(ClientState::Assigned(assigned)) => assigned
);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Renew, t1),
Action::ScheduleTimer(ClientTimerType::Rebind, t2),
Action::IaNaUpdates(iana_updates),
Action::IaPdUpdates(iapd_updates),
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, restart_time),
] => {
assert_eq!(*t1, now.add(Duration::from_secs(T1.get().into())));
assert_eq!(*t2, now.add(Duration::from_secs(T2.get().into())));
assert_eq!(
*restart_time,
now.add(Duration::from_secs(VALID_LIFETIME.get().into())),
);
assert_eq!(
iana_updates,
&(0..).map(v6::IAID::new)
.zip(CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2].iter().cloned())
.map(|(iaid, value)| (
iaid,
HashMap::from([(value, IaValueUpdateKind::Added(Lifetimes::new_default()))])
))
.collect::<HashMap<_, _>>(),
);
assert_eq!(
iapd_updates,
&(0..).map(v6::IAID::new)
.zip(CONFIGURED_DELEGATED_PREFIXES[0..2].iter().cloned())
.map(|(iaid, value)| (
iaid,
HashMap::from([(value, IaValueUpdateKind::Added(Lifetimes::new_default()))])
))
.collect::<HashMap<_, _>>(),
);
}
);
}
#[test]
fn assigned_get_dns_servers() {
let now = Instant::now();
let (client, actions) = testutil::assign_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
vec![TestIaNa::new_default(CONFIGURED_NON_TEMPORARY_ADDRESSES[0])],
Default::default(), /* delegated_prefixes_to_assign */
&DNS_SERVERS,
StepRng::new(u64::MAX / 2, 0),
now,
);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Renew, t1),
Action::ScheduleTimer(ClientTimerType::Rebind, t2),
Action::UpdateDnsServers(dns_servers),
Action::IaNaUpdates(iana_updates),
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, restart_time),
] => {
assert_eq!(dns_servers[..], DNS_SERVERS);
assert_eq!(*t1, now.add(Duration::from_secs(T1.get().into())));
assert_eq!(*t2, now.add(Duration::from_secs(T2.get().into())));
assert_eq!(
*restart_time,
now.add(Duration::from_secs(VALID_LIFETIME.get().into())),
);
assert_eq!(
iana_updates,
&HashMap::from([
(
v6::IAID::new(0),
HashMap::from([(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
IaValueUpdateKind::Added(Lifetimes::new_default()),
)]),
),
]),
);
}
);
assert_eq!(client.get_dns_servers()[..], DNS_SERVERS);
}
#[test]
fn update_sol_max_rt_on_reply_to_request() {
let options_to_request = vec![];
let configured_non_temporary_addresses = testutil::to_configured_addresses(1, vec![]);
let mut rng = StepRng::new(u64::MAX / 2, 0);
let time = Instant::now();
let Transition { state, actions: _, transaction_id } = Requesting::start(
CLIENT_ID.into(),
SERVER_ID[0].to_vec(),
advertise_to_ia_entries(
testutil::to_default_ias_map(&CONFIGURED_NON_TEMPORARY_ADDRESSES[0..1]),
configured_non_temporary_addresses.clone(),
),
Default::default(), /* delegated_prefixes */
&options_to_request[..],
BinaryHeap::new(),
MAX_SOLICIT_TIMEOUT,
&mut rng,
time,
);
{
let Requesting {
collected_advertise,
solicit_max_rt,
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
} = assert_matches!(&state, ClientState::Requesting(requesting) => requesting);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
let received_sol_max_rt = 4800;
// If the reply does not contain a server ID, the reply should be
// discarded and the `solicit_max_rt` should not be updated.
let iana_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
60,
120,
&[],
))];
let options = [
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
)),
v6::DhcpOption::SolMaxRt(received_sol_max_rt),
];
let request_transaction_id = transaction_id.unwrap();
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, request_transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions: _, transaction_id: _ } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
{
let Requesting {
collected_advertise,
solicit_max_rt,
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
} = assert_matches!(&state, ClientState::Requesting(requesting) => requesting);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
// If the reply has a different client ID than the test client's client ID,
// the `solicit_max_rt` should not be updated.
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&MISMATCHED_CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
)),
v6::DhcpOption::SolMaxRt(received_sol_max_rt),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, request_transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions: _, transaction_id: _ } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
{
let Requesting {
collected_advertise,
solicit_max_rt,
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
} = assert_matches!(&state, ClientState::Requesting(requesting) => requesting);
assert!(collected_advertise.is_empty(), "{:?}", collected_advertise);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
// If the client receives a valid reply containing a SOL_MAX_RT option,
// the `solicit_max_rt` should be updated.
let options = [
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
T1.get(),
T2.get(),
&iana_options,
)),
v6::DhcpOption::SolMaxRt(received_sol_max_rt),
];
let builder =
v6::MessageBuilder::new(v6::MessageType::Reply, request_transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let Transition { state, actions: _, transaction_id: _ } =
state.reply_message_received(&options_to_request, &mut rng, msg, time);
{
let Assigned {
solicit_max_rt,
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers: _,
_marker,
} = assert_matches!(&state, ClientState::Assigned(assigned) => assigned);
assert_eq!(*solicit_max_rt, Duration::from_secs(received_sol_max_rt.into()));
}
}
struct RenewRebindTest {
send_and_assert: fn(
&ClientDuid,
[u8; TEST_SERVER_ID_LEN],
Vec<TestIaNa>,
Vec<TestIaPd>,
Option<&[Ipv6Addr]>,
v6::NonZeroOrMaxU32,
v6::NonZeroOrMaxU32,
v6::NonZeroTimeValue,
StepRng,
Instant,
) -> ClientStateMachine<Instant, StepRng>,
message_type: v6::MessageType,
expect_server_id: bool,
with_state: fn(&Option<ClientState<Instant>>) -> &RenewingOrRebindingInner<Instant>,
allow_response_from_any_server: bool,
}
const RENEW_TEST: RenewRebindTest = RenewRebindTest {
send_and_assert: testutil::send_renew_and_assert,
message_type: v6::MessageType::Renew,
expect_server_id: true,
with_state: |state| {
assert_matches!(
state,
Some(ClientState::Renewing(RenewingOrRebinding(inner))) => inner
)
},
allow_response_from_any_server: false,
};
const REBIND_TEST: RenewRebindTest = RenewRebindTest {
send_and_assert: testutil::send_rebind_and_assert,
message_type: v6::MessageType::Rebind,
expect_server_id: false,
with_state: |state| {
assert_matches!(
state,
Some(ClientState::Rebinding(RenewingOrRebinding(inner))) => inner
)
},
allow_response_from_any_server: true,
};
struct RenewRebindSendTestCase {
ia_nas: Vec<TestIaNa>,
ia_pds: Vec<TestIaPd>,
}
impl RenewRebindSendTestCase {
fn single_value_per_ia() -> RenewRebindSendTestCase {
RenewRebindSendTestCase {
ia_nas: CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]
.into_iter()
.map(|&addr| TestIaNa::new_default(addr))
.collect(),
ia_pds: CONFIGURED_DELEGATED_PREFIXES[0..2]
.into_iter()
.map(|&addr| TestIaPd::new_default(addr))
.collect(),
}
}
fn multiple_values_per_ia() -> RenewRebindSendTestCase {
RenewRebindSendTestCase {
ia_nas: vec![TestIaNa::new_default_with_values(
CONFIGURED_NON_TEMPORARY_ADDRESSES
.into_iter()
.map(|a| (a, Lifetimes::new_default()))
.collect(),
)],
ia_pds: vec![TestIaPd::new_default_with_values(
CONFIGURED_DELEGATED_PREFIXES
.into_iter()
.map(|a| (a, Lifetimes::new_default()))
.collect(),
)],
}
}
}
#[test_case(
RENEW_TEST,
RenewRebindSendTestCase::single_value_per_ia(); "renew single value per IA")]
#[test_case(
RENEW_TEST,
RenewRebindSendTestCase::multiple_values_per_ia(); "renew multiple value per IA")]
#[test_case(
REBIND_TEST,
RenewRebindSendTestCase::single_value_per_ia(); "rebind single value per IA")]
#[test_case(
REBIND_TEST,
RenewRebindSendTestCase::multiple_values_per_ia(); "rebind multiple value per IA")]
fn send(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
RenewRebindSendTestCase { ia_nas, ia_pds }: RenewRebindSendTestCase,
) {
let _client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
ia_nas,
ia_pds,
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
}
#[test_case(RENEW_TEST)]
#[test_case(REBIND_TEST)]
fn get_dns_server(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
) {
let client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]
.into_iter()
.map(|&addr| TestIaNa::new_default(addr))
.collect(),
Default::default(), /* delegated_prefixes_to_assign */
Some(&DNS_SERVERS),
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
assert_eq!(client.get_dns_servers()[..], DNS_SERVERS);
}
struct ScheduleRenewAndRebindTimersAfterAssignmentTestCase {
ia_na_t1: v6::TimeValue,
ia_na_t2: v6::TimeValue,
ia_pd_t1: v6::TimeValue,
ia_pd_t2: v6::TimeValue,
expected_timer_actions: fn(Instant) -> [Action<Instant>; 2],
next_timer: Option<RenewRebindTestState>,
}
// Make sure that both IA_NA and IA_PD is considered when calculating
// renew/rebind timers.
#[test_case(ScheduleRenewAndRebindTimersAfterAssignmentTestCase{
ia_na_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_na_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_pd_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_pd_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
expected_timer_actions: |_| [
Action::CancelTimer(ClientTimerType::Renew),
Action::CancelTimer(ClientTimerType::Rebind),
],
next_timer: None,
}; "all infinite time values")]
#[test_case(ScheduleRenewAndRebindTimersAfterAssignmentTestCase{
ia_na_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
ia_na_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
ia_pd_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
ia_pd_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
expected_timer_actions: |time| [
Action::ScheduleTimer(
ClientTimerType::Renew,
time.add(Duration::from_secs(T1.get().into())),
),
Action::ScheduleTimer(
ClientTimerType::Rebind,
time.add(Duration::from_secs(T2.get().into())),
),
],
next_timer: Some(RENEW_TEST_STATE),
}; "all finite time values")]
#[test_case(ScheduleRenewAndRebindTimersAfterAssignmentTestCase{
ia_na_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
ia_na_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
ia_pd_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
ia_pd_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
expected_timer_actions: |time| [
// Skip Renew and just go to Rebind when T2 == T1.
Action::CancelTimer(ClientTimerType::Renew),
Action::ScheduleTimer(
ClientTimerType::Rebind,
time.add(Duration::from_secs(T2.get().into())),
),
],
next_timer: Some(REBIND_TEST_STATE),
}; "finite T1 equals finite T2")]
#[test_case(ScheduleRenewAndRebindTimersAfterAssignmentTestCase{
ia_na_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
ia_na_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_pd_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_pd_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
expected_timer_actions: |time| [
Action::ScheduleTimer(
ClientTimerType::Renew,
time.add(Duration::from_secs(T1.get().into())),
),
Action::CancelTimer(ClientTimerType::Rebind),
],
next_timer: Some(RENEW_TEST_STATE),
}; "finite IA_NA T1")]
#[test_case(ScheduleRenewAndRebindTimersAfterAssignmentTestCase{
ia_na_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
ia_na_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
ia_pd_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_pd_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
expected_timer_actions: |time| [
Action::ScheduleTimer(
ClientTimerType::Renew,
time.add(Duration::from_secs(T1.get().into())),
),
Action::ScheduleTimer(
ClientTimerType::Rebind,
time.add(Duration::from_secs(T2.get().into())),
),
],
next_timer: Some(RENEW_TEST_STATE),
}; "finite IA_NA T1 and T2")]
#[test_case(ScheduleRenewAndRebindTimersAfterAssignmentTestCase{
ia_na_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_na_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_pd_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
ia_pd_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
expected_timer_actions: |time| [
Action::ScheduleTimer(
ClientTimerType::Renew,
time.add(Duration::from_secs(T1.get().into())),
),
Action::CancelTimer(ClientTimerType::Rebind),
],
next_timer: Some(RENEW_TEST_STATE),
}; "finite IA_PD t1")]
#[test_case(ScheduleRenewAndRebindTimersAfterAssignmentTestCase{
ia_na_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_na_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
ia_pd_t1: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T1)),
ia_pd_t2: v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(T2)),
expected_timer_actions: |time| [
Action::ScheduleTimer(
ClientTimerType::Renew,
time.add(Duration::from_secs(T1.get().into())),
),
Action::ScheduleTimer(
ClientTimerType::Rebind,
time.add(Duration::from_secs(T2.get().into())),
),
],
next_timer: Some(RENEW_TEST_STATE),
}; "finite IA_PD T1 and T2")]
fn schedule_renew_and_rebind_timers_after_assignment(
ScheduleRenewAndRebindTimersAfterAssignmentTestCase {
ia_na_t1,
ia_na_t2,
ia_pd_t1,
ia_pd_t2,
expected_timer_actions,
next_timer,
}: ScheduleRenewAndRebindTimersAfterAssignmentTestCase,
) {
fn get_ia_and_updates<V: IaValue>(
t1: v6::TimeValue,
t2: v6::TimeValue,
value: V,
) -> (TestIa<V>, HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>>) {
(
TestIa { t1, t2, ..TestIa::new_default(value) },
HashMap::from([(
v6::IAID::new(0),
HashMap::from([(value, IaValueUpdateKind::Added(Lifetimes::new_default()))]),
)]),
)
}
let (iana, iana_updates) =
get_ia_and_updates(ia_na_t1, ia_na_t2, CONFIGURED_NON_TEMPORARY_ADDRESSES[0]);
let (iapd, iapd_updates) =
get_ia_and_updates(ia_pd_t1, ia_pd_t2, CONFIGURED_DELEGATED_PREFIXES[0]);
let iana = vec![iana];
let iapd = vec![iapd];
let now = Instant::now();
let (client, actions) = testutil::assign_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
iana.clone(),
iapd.clone(),
&[],
StepRng::new(u64::MAX / 2, 0),
now,
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let Assigned {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers: _,
solicit_max_rt: _,
_marker,
} = assert_matches!(
state,
Some(ClientState::Assigned(assigned)) => assigned
);
assert_eq!(
actions,
[Action::CancelTimer(ClientTimerType::Retransmission)]
.into_iter()
.chain(expected_timer_actions(now))
.chain((!iana_updates.is_empty()).then(|| Action::IaNaUpdates(iana_updates)))
.chain((!iapd_updates.is_empty()).then(|| Action::IaPdUpdates(iapd_updates)))
.chain([Action::ScheduleTimer(
ClientTimerType::RestartServerDiscovery,
now.add(Duration::from_secs(VALID_LIFETIME.get().into())),
),])
.collect::<Vec<_>>()
);
let _client = if let Some(next_timer) = next_timer {
handle_renew_or_rebind_timer(
client,
&CLIENT_ID,
SERVER_ID[0],
iana,
iapd,
&[],
&[],
Instant::now(),
next_timer,
)
} else {
client
};
}
#[test_case(RENEW_TEST)]
#[test_case(REBIND_TEST)]
fn retransmit(
RenewRebindTest {
send_and_assert,
message_type,
expect_server_id,
with_state,
allow_response_from_any_server: _,
}: RenewRebindTest,
) {
let non_temporary_addresses_to_assign = CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]
.into_iter()
.map(|&addr| TestIaNa::new_default(addr))
.collect::<Vec<_>>();
let delegated_prefixes_to_assign = CONFIGURED_DELEGATED_PREFIXES[0..2]
.into_iter()
.map(|&addr| TestIaPd::new_default(addr))
.collect::<Vec<_>>();
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
non_temporary_addresses_to_assign.clone(),
delegated_prefixes_to_assign.clone(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state, rng: _ } = &client;
let expected_transaction_id = *transaction_id;
let RenewingOrRebindingInner {
client_id: _,
non_temporary_addresses: _,
delegated_prefixes: _,
server_id: _,
dns_servers: _,
start_time: _,
retrans_timeout: _,
solicit_max_rt: _,
} = with_state(state);
// Assert renew is retransmitted on retransmission timeout.
let actions = client.handle_timeout(ClientTimerType::Retransmission, time);
let buf = assert_matches!(
&actions[..],
[
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, timeout)
] => {
assert_eq!(*timeout, time.add(2 * INITIAL_RENEW_TIMEOUT));
buf
}
);
let ClientStateMachine { transaction_id, options_to_request: _, state, rng: _ } = &client;
// Check that the retransmitted renew is part of the same transaction.
assert_eq!(*transaction_id, expected_transaction_id);
{
let RenewingOrRebindingInner {
client_id,
server_id,
dns_servers,
solicit_max_rt,
non_temporary_addresses: _,
delegated_prefixes: _,
start_time: _,
retrans_timeout: _,
} = with_state(state);
assert_eq!(client_id.as_slice(), &CLIENT_ID);
assert_eq!(server_id[..], SERVER_ID[0]);
assert_eq!(dns_servers, &[] as &[Ipv6Addr]);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
let expected_non_temporary_addresses: HashMap<v6::IAID, HashSet<Ipv6Addr>> = (0..)
.map(v6::IAID::new)
.zip(
non_temporary_addresses_to_assign
.iter()
.map(|TestIaNa { values, t1: _, t2: _ }| values.keys().cloned().collect()),
)
.collect();
let expected_delegated_prefixes: HashMap<v6::IAID, HashSet<Subnet<Ipv6Addr>>> = (0..)
.map(v6::IAID::new)
.zip(
delegated_prefixes_to_assign
.iter()
.map(|TestIaPd { values, t1: _, t2: _ }| values.keys().cloned().collect()),
)
.collect();
testutil::assert_outgoing_stateful_message(
&buf,
message_type,
&CLIENT_ID,
expect_server_id.then(|| &SERVER_ID[0]),
&[],
&expected_non_temporary_addresses,
&expected_delegated_prefixes,
);
}
#[test_case(
RENEW_TEST,
&SERVER_ID[0],
&SERVER_ID[0],
RenewRebindSendTestCase::single_value_per_ia()
)]
#[test_case(
REBIND_TEST,
&SERVER_ID[0],
&SERVER_ID[0],
RenewRebindSendTestCase::single_value_per_ia()
)]
#[test_case(
RENEW_TEST,
&SERVER_ID[0],
&SERVER_ID[1],
RenewRebindSendTestCase::single_value_per_ia()
)]
#[test_case(
REBIND_TEST,
&SERVER_ID[0],
&SERVER_ID[1],
RenewRebindSendTestCase::single_value_per_ia()
)]
#[test_case(
RENEW_TEST,
&SERVER_ID[0],
&SERVER_ID[0],
RenewRebindSendTestCase::multiple_values_per_ia()
)]
#[test_case(
REBIND_TEST,
&SERVER_ID[0],
&SERVER_ID[0],
RenewRebindSendTestCase::multiple_values_per_ia()
)]
#[test_case(
RENEW_TEST,
&SERVER_ID[0],
&SERVER_ID[1],
RenewRebindSendTestCase::multiple_values_per_ia()
)]
#[test_case(
REBIND_TEST,
&SERVER_ID[0],
&SERVER_ID[1],
RenewRebindSendTestCase::multiple_values_per_ia()
)]
fn receive_reply_extends_lifetime(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state,
allow_response_from_any_server,
}: RenewRebindTest,
original_server_id: &[u8; TEST_SERVER_ID_LEN],
reply_server_id: &[u8],
RenewRebindSendTestCase { ia_nas, ia_pds }: RenewRebindSendTestCase,
) {
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
original_server_id.clone(),
ia_nas.clone(),
ia_pds.clone(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state, rng: _ } = &client;
let buf = TestMessageBuilder {
transaction_id: *transaction_id,
message_type: v6::MessageType::Reply,
client_id: &CLIENT_ID,
server_id: reply_server_id,
preference: None,
dns_servers: None,
ia_nas: (0..).map(v6::IAID::new).zip(ia_nas.iter().map(
|TestIa { values, t1: _, t2: _ }| {
TestIa::new_renewed_default_with_values(values.keys().cloned())
},
)),
ia_pds: (0..).map(v6::IAID::new).zip(ia_pds.iter().map(
|TestIa { values, t1: _, t2: _ }| {
TestIa::new_renewed_default_with_values(values.keys().cloned())
},
)),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
// Make sure we are in renewing/rebinding before we handle the message.
let original_state = with_state(state).clone();
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
if original_server_id.as_slice() != reply_server_id && !allow_response_from_any_server {
// Renewing does not allow us to receive replies from a different
// server but Rebinding does. If we aren't allowed to accept a
// response from a different server, just make sure we are in the
// same state.
let RenewingOrRebindingInner {
client_id: original_client_id,
non_temporary_addresses: original_non_temporary_addresses,
delegated_prefixes: original_delegated_prefixes,
server_id: original_server_id,
dns_servers: original_dns_servers,
start_time: original_start_time,
retrans_timeout: original_retrans_timeout,
solicit_max_rt: original_solicit_max_rt,
} = original_state;
let RenewingOrRebindingInner {
client_id: new_client_id,
non_temporary_addresses: new_non_temporary_addresses,
delegated_prefixes: new_delegated_prefixes,
server_id: new_server_id,
dns_servers: new_dns_servers,
start_time: new_start_time,
retrans_timeout: new_retrans_timeout,
solicit_max_rt: new_solicit_max_rt,
} = with_state(state);
assert_eq!(&original_client_id, new_client_id);
assert_eq!(&original_non_temporary_addresses, new_non_temporary_addresses);
assert_eq!(&original_delegated_prefixes, new_delegated_prefixes);
assert_eq!(&original_server_id, new_server_id);
assert_eq!(&original_dns_servers, new_dns_servers);
assert_eq!(&original_start_time, new_start_time);
assert_eq!(&original_retrans_timeout, new_retrans_timeout);
assert_eq!(&original_solicit_max_rt, new_solicit_max_rt);
assert_eq!(actions, []);
return;
}
let expected_non_temporary_addresses = (0..)
.map(v6::IAID::new)
.zip(ia_nas.iter().map(|TestIa { values, t1: _, t2: _ }| {
AddressEntry::Assigned(
values
.keys()
.cloned()
.map(|value| {
(
value,
LifetimesInfo {
lifetimes: Lifetimes::new_renewed(),
updated_at: time,
},
)
})
.collect(),
)
}))
.collect();
let expected_delegated_prefixes = (0..)
.map(v6::IAID::new)
.zip(ia_pds.iter().map(|TestIa { values, t1: _, t2: _ }| {
PrefixEntry::Assigned(
values
.keys()
.cloned()
.map(|value| {
(
value,
LifetimesInfo {
lifetimes: Lifetimes::new_renewed(),
updated_at: time,
},
)
})
.collect(),
)
}))
.collect();
assert_matches!(
&state,
Some(ClientState::Assigned(Assigned {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
solicit_max_rt,
_marker,
})) => {
assert_eq!(client_id.as_slice(), &CLIENT_ID);
assert_eq!(non_temporary_addresses, &expected_non_temporary_addresses);
assert_eq!(delegated_prefixes, &expected_delegated_prefixes);
assert_eq!(server_id.as_slice(), reply_server_id);
assert_eq!(dns_servers.as_slice(), &[] as &[Ipv6Addr]);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Renew, t1),
Action::ScheduleTimer(ClientTimerType::Rebind, t2),
Action::IaNaUpdates(iana_updates),
Action::IaPdUpdates(iapd_updates),
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, restart_time),
] => {
assert_eq!(*t1, time.add(Duration::from_secs(RENEWED_T1.get().into())));
assert_eq!(*t2, time.add(Duration::from_secs(RENEWED_T2.get().into())));
assert_eq!(
*restart_time,
time.add(Duration::from_secs(RENEWED_VALID_LIFETIME.get().into()))
);
fn get_updates<V: IaValue>(ias: Vec<TestIa<V>>) -> HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>> {
(0..).map(v6::IAID::new).zip(ias.into_iter().map(
|TestIa { values, t1: _, t2: _ }| {
values.into_keys()
.map(|value| (
value,
IaValueUpdateKind::UpdatedLifetimes(Lifetimes::new_renewed())
))
.collect()
},
)).collect()
}
assert_eq!(iana_updates, &get_updates(ia_nas));
assert_eq!(iapd_updates, &get_updates(ia_pds));
}
);
}
// Tests that receiving a Reply with an error status code other than
// UseMulticast results in only SOL_MAX_RT being updated, with the rest
// of the message contents ignored.
#[test_case(RENEW_TEST, v6::ErrorStatusCode::UnspecFail)]
#[test_case(RENEW_TEST, v6::ErrorStatusCode::NoBinding)]
#[test_case(RENEW_TEST, v6::ErrorStatusCode::NotOnLink)]
#[test_case(RENEW_TEST, v6::ErrorStatusCode::NoAddrsAvail)]
#[test_case(RENEW_TEST, v6::ErrorStatusCode::NoPrefixAvail)]
#[test_case(REBIND_TEST, v6::ErrorStatusCode::UnspecFail)]
#[test_case(REBIND_TEST, v6::ErrorStatusCode::NoBinding)]
#[test_case(REBIND_TEST, v6::ErrorStatusCode::NotOnLink)]
#[test_case(REBIND_TEST, v6::ErrorStatusCode::NoAddrsAvail)]
#[test_case(REBIND_TEST, v6::ErrorStatusCode::NoPrefixAvail)]
fn renewing_receive_reply_with_error_status(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state,
allow_response_from_any_server: _,
}: RenewRebindTest,
error_status_code: v6::ErrorStatusCode,
) {
let time = Instant::now();
let addr = CONFIGURED_NON_TEMPORARY_ADDRESSES[0];
let prefix = CONFIGURED_DELEGATED_PREFIXES[0];
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
vec![TestIaNa::new_default(addr)],
vec![TestIaPd::new_default(prefix)],
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let ia_na_options = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
addr,
RENEWED_PREFERRED_LIFETIME.get(),
RENEWED_VALID_LIFETIME.get(),
&[],
))];
let sol_max_rt = *VALID_MAX_SOLICIT_TIMEOUT_RANGE.start();
let options = vec![
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::StatusCode(error_status_code.into(), ""),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(0),
RENEWED_T1.get(),
RENEWED_T2.get(),
&ia_na_options,
)),
v6::DhcpOption::SolMaxRt(sol_max_rt),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let actions = client.handle_message_receive(msg, time);
assert_eq!(actions, &[]);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
let RenewingOrRebindingInner {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
start_time: _,
retrans_timeout: _,
solicit_max_rt: got_sol_max_rt,
} = with_state(state);
assert_eq!(client_id.as_slice(), &CLIENT_ID);
fn expected_values<V: IaValue>(
value: V,
time: Instant,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
std::iter::once((
v6::IAID::new(0),
IaEntry::new_assigned(value, PREFERRED_LIFETIME, VALID_LIFETIME, time),
))
.collect()
}
assert_eq!(*non_temporary_addresses, expected_values(addr, time));
assert_eq!(*delegated_prefixes, expected_values(prefix, time));
assert_eq!(*server_id, SERVER_ID[0]);
assert_eq!(dns_servers, &[] as &[Ipv6Addr]);
assert_eq!(*got_sol_max_rt, Duration::from_secs(sol_max_rt.into()));
assert_matches!(&actions[..], []);
}
struct ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: Vec<v6::IAID>,
present_ia_pd_iaids: Vec<v6::IAID>,
}
#[test_case(
REBIND_TEST,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: Vec::new(),
present_ia_pd_iaids: Vec::new(),
}; "none presenet")]
#[test_case(
RENEW_TEST,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: vec![v6::IAID::new(0)],
present_ia_pd_iaids: Vec::new(),
}; "only one IA_NA present")]
#[test_case(
RENEW_TEST,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: Vec::new(),
present_ia_pd_iaids: vec![v6::IAID::new(1)],
}; "only one IA_PD present")]
#[test_case(
REBIND_TEST,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: vec![v6::IAID::new(0), v6::IAID::new(1)],
present_ia_pd_iaids: Vec::new(),
}; "only both IA_NAs present")]
#[test_case(
REBIND_TEST,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: Vec::new(),
present_ia_pd_iaids: vec![v6::IAID::new(0), v6::IAID::new(1)],
}; "only both IA_PDs present")]
#[test_case(
REBIND_TEST,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: vec![v6::IAID::new(1)],
present_ia_pd_iaids: vec![v6::IAID::new(0), v6::IAID::new(1)],
}; "both IA_PDs and one IA_NA present")]
#[test_case(
REBIND_TEST,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids: vec![v6::IAID::new(0), v6::IAID::new(1)],
present_ia_pd_iaids: vec![v6::IAID::new(0)],
}; "both IA_NAs and one IA_PD present")]
fn receive_reply_with_missing_ias(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state,
allow_response_from_any_server: _,
}: RenewRebindTest,
ReceiveReplyWithMissingIasTestCase {
present_ia_na_iaids,
present_ia_pd_iaids,
}: ReceiveReplyWithMissingIasTestCase,
) {
let non_temporary_addresses = &CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2];
let delegated_prefixes = &CONFIGURED_DELEGATED_PREFIXES[0..2];
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
non_temporary_addresses.iter().copied().map(TestIaNa::new_default).collect(),
delegated_prefixes.iter().copied().map(TestIaPd::new_default).collect(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
// The server includes only the IA with ID equal to `present_iaid` in the
// reply.
let buf = TestMessageBuilder {
transaction_id: *transaction_id,
message_type: v6::MessageType::Reply,
client_id: &CLIENT_ID,
server_id: &SERVER_ID[0],
preference: None,
dns_servers: None,
ia_nas: present_ia_na_iaids.iter().map(|iaid| {
(
*iaid,
TestIa::new_renewed_default(
CONFIGURED_NON_TEMPORARY_ADDRESSES[iaid.get() as usize],
),
)
}),
ia_pds: present_ia_pd_iaids.iter().map(|iaid| {
(
*iaid,
TestIa::new_renewed_default(CONFIGURED_DELEGATED_PREFIXES[iaid.get() as usize]),
)
}),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
// Only the IA that is present will have its lifetimes updated.
{
let RenewingOrRebindingInner {
client_id,
non_temporary_addresses: got_non_temporary_addresses,
delegated_prefixes: got_delegated_prefixes,
server_id,
dns_servers,
start_time: _,
retrans_timeout: _,
solicit_max_rt,
} = with_state(state);
assert_eq!(client_id.as_slice(), &CLIENT_ID);
fn expected_values<V: IaValue>(
values: &[V],
present_iaids: Vec<v6::IAID>,
time: Instant,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
(0..)
.map(v6::IAID::new)
.zip(values)
.map(|(iaid, &value)| {
(
iaid,
if present_iaids.contains(&iaid) {
IaEntry::new_assigned(
value,
RENEWED_PREFERRED_LIFETIME,
RENEWED_VALID_LIFETIME,
time,
)
} else {
IaEntry::new_assigned(
value,
PREFERRED_LIFETIME,
VALID_LIFETIME,
time,
)
},
)
})
.collect()
}
assert_eq!(
*got_non_temporary_addresses,
expected_values(non_temporary_addresses, present_ia_na_iaids, time)
);
assert_eq!(
*got_delegated_prefixes,
expected_values(delegated_prefixes, present_ia_pd_iaids, time)
);
assert_eq!(*server_id, SERVER_ID[0]);
assert_eq!(dns_servers, &[] as &[Ipv6Addr]);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
// The client relies on retransmission to send another Renew, so no actions are needed.
assert_matches!(&actions[..], []);
}
#[test_case(RENEW_TEST)]
#[test_case(REBIND_TEST)]
fn receive_reply_with_missing_ia_suboption_for_assigned_entry_does_not_extend_lifetime(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
) {
const IA_NA_WITHOUT_ADDRESS_IAID: v6::IAID = v6::IAID::new(0);
const IA_PD_WITHOUT_PREFIX_IAID: v6::IAID = v6::IAID::new(1);
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
CONFIGURED_NON_TEMPORARY_ADDRESSES.iter().copied().map(TestIaNa::new_default).collect(),
CONFIGURED_DELEGATED_PREFIXES.iter().copied().map(TestIaPd::new_default).collect(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
// The server includes an IA Address/Prefix option in only one of the IAs.
let iaaddr_opts = (0..)
.map(v6::IAID::new)
.zip(CONFIGURED_NON_TEMPORARY_ADDRESSES)
.map(|(iaid, addr)| {
(
iaid,
(iaid != IA_NA_WITHOUT_ADDRESS_IAID).then(|| {
[v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
addr,
RENEWED_PREFERRED_LIFETIME.get(),
RENEWED_VALID_LIFETIME.get(),
&[],
))]
}),
)
})
.collect::<HashMap<_, _>>();
let iaprefix_opts = (0..)
.map(v6::IAID::new)
.zip(CONFIGURED_DELEGATED_PREFIXES)
.map(|(iaid, prefix)| {
(
iaid,
(iaid != IA_PD_WITHOUT_PREFIX_IAID).then(|| {
[v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
RENEWED_PREFERRED_LIFETIME.get(),
RENEWED_VALID_LIFETIME.get(),
prefix,
&[],
))]
}),
)
})
.collect::<HashMap<_, _>>();
let options =
[v6::DhcpOption::ClientId(&CLIENT_ID), v6::DhcpOption::ServerId(&SERVER_ID[0])]
.into_iter()
.chain(iaaddr_opts.iter().map(|(iaid, iaaddr_opts)| {
v6::DhcpOption::Iana(v6::IanaSerializer::new(
*iaid,
RENEWED_T1.get(),
RENEWED_T2.get(),
iaaddr_opts.as_ref().map_or(&[], AsRef::as_ref),
))
}))
.chain(iaprefix_opts.iter().map(|(iaid, iaprefix_opts)| {
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
*iaid,
RENEWED_T1.get(),
RENEWED_T2.get(),
iaprefix_opts.as_ref().map_or(&[], AsRef::as_ref),
))
}))
.collect::<Vec<_>>();
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
// Expect the client to transition to Assigned and only extend
// the lifetime for one IA.
assert_matches!(
&state,
Some(ClientState::Assigned(Assigned {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
solicit_max_rt,
_marker,
})) => {
assert_eq!(client_id.as_slice(), &CLIENT_ID);
fn expected_values<V: IaValueTestExt>(
without_value: v6::IAID,
time: Instant,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
(0..)
.map(v6::IAID::new)
.zip(V::CONFIGURED)
.map(|(iaid, value)| {
let (preferred_lifetime, valid_lifetime) =
if iaid == without_value {
(PREFERRED_LIFETIME, VALID_LIFETIME)
} else {
(RENEWED_PREFERRED_LIFETIME, RENEWED_VALID_LIFETIME)
};
(
iaid,
IaEntry::new_assigned(
value,
preferred_lifetime,
valid_lifetime,
time,
),
)
})
.collect()
}
assert_eq!(
non_temporary_addresses,
&expected_values::<Ipv6Addr>(IA_NA_WITHOUT_ADDRESS_IAID, time)
);
assert_eq!(
delegated_prefixes,
&expected_values::<Subnet<Ipv6Addr>>(IA_PD_WITHOUT_PREFIX_IAID, time)
);
assert_eq!(server_id.as_slice(), &SERVER_ID[0]);
assert_eq!(dns_servers.as_slice(), &[] as &[Ipv6Addr]);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Renew, t1),
Action::ScheduleTimer(ClientTimerType::Rebind, t2),
Action::IaNaUpdates(iana_updates),
Action::IaPdUpdates(iapd_updates),
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, restart_time),
] => {
assert_eq!(*t1, time.add(Duration::from_secs(RENEWED_T1.get().into())));
assert_eq!(*t2, time.add(Duration::from_secs(RENEWED_T2.get().into())));
assert_eq!(
*restart_time,
time.add(Duration::from_secs(std::cmp::max(
VALID_LIFETIME,
RENEWED_VALID_LIFETIME,
).get().into()))
);
fn get_updates<V: IaValue>(
values: &[V],
omit_iaid: v6::IAID,
) -> HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>> {
(0..).map(v6::IAID::new)
.zip(values.iter().cloned())
.filter_map(|(iaid, value)| {
(iaid != omit_iaid).then(|| (
iaid,
HashMap::from([(
value,
IaValueUpdateKind::UpdatedLifetimes(Lifetimes::new_renewed()),
)])
))
})
.collect()
}
assert_eq!(
iana_updates,
&get_updates(&CONFIGURED_NON_TEMPORARY_ADDRESSES, IA_NA_WITHOUT_ADDRESS_IAID),
);
assert_eq!(
iapd_updates,
&get_updates(&CONFIGURED_DELEGATED_PREFIXES, IA_PD_WITHOUT_PREFIX_IAID),
);
}
);
}
#[test_case(RENEW_TEST)]
#[test_case(REBIND_TEST)]
fn receive_reply_with_zero_lifetime(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
) {
const IA_NA_ZERO_LIFETIMES_ADDRESS_IAID: v6::IAID = v6::IAID::new(0);
const IA_PD_ZERO_LIFETIMES_PREFIX_IAID: v6::IAID = v6::IAID::new(1);
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
CONFIGURED_NON_TEMPORARY_ADDRESSES.iter().copied().map(TestIaNa::new_default).collect(),
CONFIGURED_DELEGATED_PREFIXES.iter().copied().map(TestIaPd::new_default).collect(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
// The server includes an IA Address/Prefix option in only one of the IAs.
let iaaddr_opts = (0..)
.map(v6::IAID::new)
.zip(CONFIGURED_NON_TEMPORARY_ADDRESSES)
.map(|(iaid, addr)| {
let (pl, vl) = if iaid == IA_NA_ZERO_LIFETIMES_ADDRESS_IAID {
(0, 0)
} else {
(RENEWED_PREFERRED_LIFETIME.get(), RENEWED_VALID_LIFETIME.get())
};
(iaid, [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(addr, pl, vl, &[]))])
})
.collect::<HashMap<_, _>>();
let iaprefix_opts = (0..)
.map(v6::IAID::new)
.zip(CONFIGURED_DELEGATED_PREFIXES)
.map(|(iaid, prefix)| {
let (pl, vl) = if iaid == IA_PD_ZERO_LIFETIMES_PREFIX_IAID {
(0, 0)
} else {
(RENEWED_PREFERRED_LIFETIME.get(), RENEWED_VALID_LIFETIME.get())
};
(iaid, [v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(pl, vl, prefix, &[]))])
})
.collect::<HashMap<_, _>>();
let options =
[v6::DhcpOption::ClientId(&CLIENT_ID), v6::DhcpOption::ServerId(&SERVER_ID[0])]
.into_iter()
.chain(iaaddr_opts.iter().map(|(iaid, iaaddr_opts)| {
v6::DhcpOption::Iana(v6::IanaSerializer::new(
*iaid,
RENEWED_T1.get(),
RENEWED_T2.get(),
iaaddr_opts.as_ref(),
))
}))
.chain(iaprefix_opts.iter().map(|(iaid, iaprefix_opts)| {
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
*iaid,
RENEWED_T1.get(),
RENEWED_T2.get(),
iaprefix_opts.as_ref(),
))
}))
.collect::<Vec<_>>();
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
// Expect the client to transition to Assigned and only extend
// the lifetime for one IA.
assert_matches!(
&state,
Some(ClientState::Assigned(Assigned {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
solicit_max_rt,
_marker,
})) => {
assert_eq!(client_id.as_slice(), &CLIENT_ID);
fn expected_values<V: IaValueTestExt>(
zero_lifetime_iaid: v6::IAID,
time: Instant,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
(0..)
.map(v6::IAID::new)
.zip(V::CONFIGURED)
.map(|(iaid, value)| {
(
iaid,
if iaid == zero_lifetime_iaid {
IaEntry::ToRequest(HashSet::from([value]))
} else {
IaEntry::new_assigned(
value,
RENEWED_PREFERRED_LIFETIME,
RENEWED_VALID_LIFETIME,
time,
)
},
)
})
.collect()
}
assert_eq!(
non_temporary_addresses,
&expected_values::<Ipv6Addr>(IA_NA_ZERO_LIFETIMES_ADDRESS_IAID, time)
);
assert_eq!(
delegated_prefixes,
&expected_values::<Subnet<Ipv6Addr>>(IA_PD_ZERO_LIFETIMES_PREFIX_IAID, time)
);
assert_eq!(server_id.as_slice(), &SERVER_ID[0]);
assert_eq!(dns_servers.as_slice(), &[] as &[Ipv6Addr]);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Renew, t1),
Action::ScheduleTimer(ClientTimerType::Rebind, t2),
Action::IaNaUpdates(iana_updates),
Action::IaPdUpdates(iapd_updates),
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, restart_time),
] => {
assert_eq!(*t1, time.add(Duration::from_secs(RENEWED_T1.get().into())));
assert_eq!(*t2, time.add(Duration::from_secs(RENEWED_T2.get().into())));
assert_eq!(
*restart_time,
time.add(Duration::from_secs(RENEWED_VALID_LIFETIME.get().into()))
);
fn get_updates<V: IaValue>(
values: &[V],
omit_iaid: v6::IAID,
) -> HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>> {
(0..).map(v6::IAID::new)
.zip(values.iter().cloned())
.map(|(iaid, value)| (
iaid,
HashMap::from([(
value,
if iaid == omit_iaid {
IaValueUpdateKind::Removed
} else {
IaValueUpdateKind::UpdatedLifetimes(Lifetimes::new_renewed())
}
)]),
))
.collect()
}
assert_eq!(
iana_updates,
&get_updates(
&CONFIGURED_NON_TEMPORARY_ADDRESSES,
IA_NA_ZERO_LIFETIMES_ADDRESS_IAID,
),
);
assert_eq!(
iapd_updates,
&get_updates(
&CONFIGURED_DELEGATED_PREFIXES,
IA_PD_ZERO_LIFETIMES_PREFIX_IAID,
),
);
}
);
}
#[test_case(RENEW_TEST)]
#[test_case(REBIND_TEST)]
fn receive_reply_with_original_ia_value_omitted(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
) {
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
vec![TestIaNa::new_default(CONFIGURED_NON_TEMPORARY_ADDRESSES[0])],
vec![TestIaPd::new_default(CONFIGURED_DELEGATED_PREFIXES[0])],
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
// The server includes IAs with different values from what was
// previously assigned.
let iaid = v6::IAID::new(0);
let buf = TestMessageBuilder {
transaction_id: *transaction_id,
message_type: v6::MessageType::Reply,
client_id: &CLIENT_ID,
server_id: &SERVER_ID[0],
preference: None,
dns_servers: None,
ia_nas: std::iter::once((
iaid,
TestIa::new_renewed_default(RENEW_NON_TEMPORARY_ADDRESSES[0]),
)),
ia_pds: std::iter::once((
iaid,
TestIa::new_renewed_default(RENEW_DELEGATED_PREFIXES[0]),
)),
}
.build();
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
// Expect the client to transition to Assigned with both the new value
// found in the latest Reply and the original value found when we first
// transitioned to Assigned above. We always keep the old value even
// though it was missing from the received Reply since the server did
// not send an IA Address/Prefix option with the zero valid lifetime.
assert_matches!(
&state,
Some(ClientState::Assigned(Assigned {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
dns_servers,
solicit_max_rt,
_marker,
})) => {
assert_eq!(client_id.as_slice(), &CLIENT_ID);
fn calc_expected<V: IaValue>(
iaid: v6::IAID,
time: Instant,
initial: V,
in_renew: V,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
HashMap::from([(
iaid,
IaEntry::Assigned(HashMap::from([
(
initial,
LifetimesInfo {
lifetimes: Lifetimes::new_finite(
PREFERRED_LIFETIME,
VALID_LIFETIME,
),
updated_at: time,
}
),
(
in_renew,
LifetimesInfo {
lifetimes: Lifetimes::new_finite(
RENEWED_PREFERRED_LIFETIME,
RENEWED_VALID_LIFETIME,
),
updated_at: time,
},
),
])),
)])
}
assert_eq!(
non_temporary_addresses,
&calc_expected(
iaid,
time,
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
RENEW_NON_TEMPORARY_ADDRESSES[0],
)
);
assert_eq!(
delegated_prefixes,
&calc_expected(
iaid,
time,
CONFIGURED_DELEGATED_PREFIXES[0],
RENEW_DELEGATED_PREFIXES[0],
)
);
assert_eq!(server_id.as_slice(), &SERVER_ID[0]);
assert_eq!(dns_servers.as_slice(), &[] as &[Ipv6Addr]);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
);
assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Renew, t1),
Action::ScheduleTimer(ClientTimerType::Rebind, t2),
Action::IaNaUpdates(iana_updates),
Action::IaPdUpdates(iapd_updates),
Action::ScheduleTimer(ClientTimerType::RestartServerDiscovery, restart_time),
] => {
assert_eq!(*t1, time.add(Duration::from_secs(RENEWED_T1.get().into())));
assert_eq!(*t2, time.add(Duration::from_secs(RENEWED_T2.get().into())));
assert_eq!(
*restart_time,
time.add(Duration::from_secs(std::cmp::max(
VALID_LIFETIME,
RENEWED_VALID_LIFETIME,
).get().into()))
);
fn get_updates<V: IaValue>(
iaid: v6::IAID,
new_value: V,
) -> HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>> {
HashMap::from([
(
iaid,
HashMap::from([
(
new_value,
IaValueUpdateKind::Added(Lifetimes::new_renewed()),
),
])
),
])
}
assert_eq!(
iana_updates,
&get_updates(
iaid,
RENEW_NON_TEMPORARY_ADDRESSES[0],
),
);
assert_eq!(
iapd_updates,
&get_updates(
iaid,
RENEW_DELEGATED_PREFIXES[0],
),
);
}
);
}
struct NoBindingTestCase {
ia_na_no_binding: bool,
ia_pd_no_binding: bool,
}
#[test_case(
RENEW_TEST,
NoBindingTestCase {
ia_na_no_binding: true,
ia_pd_no_binding: false,
}
)]
#[test_case(
REBIND_TEST,
NoBindingTestCase {
ia_na_no_binding: true,
ia_pd_no_binding: false,
}
)]
#[test_case(
RENEW_TEST,
NoBindingTestCase {
ia_na_no_binding: false,
ia_pd_no_binding: true,
}
)]
#[test_case(
REBIND_TEST,
NoBindingTestCase {
ia_na_no_binding: false,
ia_pd_no_binding: true,
}
)]
#[test_case(
RENEW_TEST,
NoBindingTestCase {
ia_na_no_binding: true,
ia_pd_no_binding: true,
}
)]
#[test_case(
REBIND_TEST,
NoBindingTestCase {
ia_na_no_binding: true,
ia_pd_no_binding: true,
}
)]
fn no_binding(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
NoBindingTestCase { ia_na_no_binding, ia_pd_no_binding }: NoBindingTestCase,
) {
const NUM_IAS: u32 = 2;
const NO_BINDING_IA_IDX: usize = (NUM_IAS - 1) as usize;
fn to_assign<V: IaValueTestExt>() -> Vec<TestIa<V>> {
V::CONFIGURED[0..usize::try_from(NUM_IAS).unwrap()]
.iter()
.copied()
.map(TestIa::new_default)
.collect()
}
let time = Instant::now();
let non_temporary_addresses_to_assign = to_assign::<Ipv6Addr>();
let delegated_prefixes_to_assign = to_assign::<Subnet<Ipv6Addr>>();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
non_temporary_addresses_to_assign.clone(),
delegated_prefixes_to_assign.clone(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
// Build a reply with NoBinding status..
let iaaddr_opts = (0..usize::try_from(NUM_IAS).unwrap())
.map(|i| {
if i == NO_BINDING_IA_IDX && ia_na_no_binding {
[v6::DhcpOption::StatusCode(
v6::ErrorStatusCode::NoBinding.into(),
"Binding not found.",
)]
} else {
[v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[i],
RENEWED_PREFERRED_LIFETIME.get(),
RENEWED_VALID_LIFETIME.get(),
&[],
))]
}
})
.collect::<Vec<_>>();
let iaprefix_opts = (0..usize::try_from(NUM_IAS).unwrap())
.map(|i| {
if i == NO_BINDING_IA_IDX && ia_pd_no_binding {
[v6::DhcpOption::StatusCode(
v6::ErrorStatusCode::NoBinding.into(),
"Binding not found.",
)]
} else {
[v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
RENEWED_PREFERRED_LIFETIME.get(),
RENEWED_VALID_LIFETIME.get(),
CONFIGURED_DELEGATED_PREFIXES[i],
&[],
))]
}
})
.collect::<Vec<_>>();
let options =
[v6::DhcpOption::ClientId(&CLIENT_ID), v6::DhcpOption::ServerId(&SERVER_ID[0])]
.into_iter()
.chain((0..NUM_IAS).map(|id| {
v6::DhcpOption::Iana(v6::IanaSerializer::new(
v6::IAID::new(id),
RENEWED_T1.get(),
RENEWED_T2.get(),
&iaaddr_opts[id as usize],
))
}))
.chain((0..NUM_IAS).map(|id| {
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
v6::IAID::new(id),
RENEWED_T1.get(),
RENEWED_T2.get(),
&iaprefix_opts[id as usize],
))
}))
.collect::<Vec<_>>();
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
// Expect the client to transition to Requesting.
{
let Requesting {
client_id,
non_temporary_addresses,
delegated_prefixes,
server_id,
collected_advertise: _,
first_request_time: _,
retrans_timeout: _,
transmission_count: _,
solicit_max_rt,
} = assert_matches!(
&state,
Some(ClientState::Requesting(requesting)) => requesting
);
assert_eq!(client_id.as_slice(), &CLIENT_ID);
fn expected_values<V: IaValueTestExt>(
no_binding: bool,
time: Instant,
) -> HashMap<v6::IAID, IaEntry<V, Instant>> {
(0..NUM_IAS)
.map(|i| {
(v6::IAID::new(i), {
let i = usize::try_from(i).unwrap();
if i == NO_BINDING_IA_IDX && no_binding {
IaEntry::ToRequest(HashSet::from([V::CONFIGURED[i]]))
} else {
IaEntry::new_assigned(
V::CONFIGURED[i],
RENEWED_PREFERRED_LIFETIME,
RENEWED_VALID_LIFETIME,
time,
)
}
})
})
.collect()
}
assert_eq!(
*non_temporary_addresses,
expected_values::<Ipv6Addr>(ia_na_no_binding, time)
);
assert_eq!(
*delegated_prefixes,
expected_values::<Subnet<Ipv6Addr>>(ia_pd_no_binding, time)
);
assert_eq!(*server_id, SERVER_ID[0]);
assert_eq!(*solicit_max_rt, MAX_SOLICIT_TIMEOUT);
}
let buf = assert_matches!(
&actions[..],
[
// TODO(https://fxbug.dev/42178817): should include action to
// remove the address of IA with NoBinding status.
Action::CancelTimer(ClientTimerType::Retransmission),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
assert_eq!(*instant, time.add(INITIAL_REQUEST_TIMEOUT));
buf
}
);
// Expect that the Request message contains both the assigned address
// and the address to request.
testutil::assert_outgoing_stateful_message(
&buf,
v6::MessageType::Request,
&CLIENT_ID,
Some(&SERVER_ID[0]),
&[],
&(0..NUM_IAS)
.map(v6::IAID::new)
.zip(CONFIGURED_NON_TEMPORARY_ADDRESSES.into_iter().map(|a| HashSet::from([a])))
.collect(),
&(0..NUM_IAS)
.map(v6::IAID::new)
.zip(CONFIGURED_DELEGATED_PREFIXES.into_iter().map(|p| HashSet::from([p])))
.collect(),
);
// While we are in requesting state after being in Assigned, make sure
// all addresses may be invalidated.
handle_all_leases_invalidated(
client,
&CLIENT_ID,
non_temporary_addresses_to_assign,
delegated_prefixes_to_assign,
ia_na_no_binding.then_some(NO_BINDING_IA_IDX),
ia_pd_no_binding.then_some(NO_BINDING_IA_IDX),
&[],
)
}
struct ReceiveReplyCalculateT1T2 {
ia_na_success_t1: v6::NonZeroOrMaxU32,
ia_na_success_t2: v6::NonZeroOrMaxU32,
ia_pd_success_t1: v6::NonZeroOrMaxU32,
ia_pd_success_t2: v6::NonZeroOrMaxU32,
}
const TINY_NON_ZERO_OR_MAX_U32: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(10));
const SMALL_NON_ZERO_OR_MAX_U32: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(100));
const MEDIUM_NON_ZERO_OR_MAX_U32: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(1000));
const LARGE_NON_ZERO_OR_MAX_U32: v6::NonZeroOrMaxU32 =
const_unwrap_option(v6::NonZeroOrMaxU32::new(10000));
#[test_case(
RENEW_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: TINY_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: TINY_NON_ZERO_OR_MAX_U32,
}; "renew lifetimes matching erroneous IAs")]
#[test_case(
RENEW_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
}; "renew same lifetimes")]
#[test_case(
RENEW_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
}; "renew IA_NA smaller lifetimes")]
#[test_case(
RENEW_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
}; "renew IA_PD smaller lifetimes")]
#[test_case(
RENEW_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
}; "renew IA_NA smaller T1 but IA_PD smaller t2")]
#[test_case(
RENEW_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
}; "renew IA_PD smaller T1 but IA_NA smaller t2")]
#[test_case(
REBIND_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: TINY_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: TINY_NON_ZERO_OR_MAX_U32,
}; "rebind lifetimes matching erroneous IAs")]
#[test_case(
REBIND_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
}; "rebind same lifetimes")]
#[test_case(
REBIND_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
}; "rebind IA_NA smaller lifetimes")]
#[test_case(
REBIND_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
}; "rebind IA_PD smaller lifetimes")]
#[test_case(
REBIND_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
}; "rebind IA_NA smaller T1 but IA_PD smaller t2")]
#[test_case(
REBIND_TEST,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1: SMALL_NON_ZERO_OR_MAX_U32,
ia_na_success_t2: MEDIUM_NON_ZERO_OR_MAX_U32,
ia_pd_success_t1: TINY_NON_ZERO_OR_MAX_U32,
ia_pd_success_t2: LARGE_NON_ZERO_OR_MAX_U32,
}; "rebind IA_PD smaller T1 but IA_NA smaller t2")]
// Tests that only valid IAs are considered when calculating T1/T2.
fn receive_reply_calculate_t1_t2(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
ReceiveReplyCalculateT1T2 {
ia_na_success_t1,
ia_na_success_t2,
ia_pd_success_t1,
ia_pd_success_t2,
}: ReceiveReplyCalculateT1T2,
) {
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
CONFIGURED_NON_TEMPORARY_ADDRESSES.into_iter().map(TestIaNa::new_default).collect(),
CONFIGURED_DELEGATED_PREFIXES.into_iter().map(TestIaPd::new_default).collect(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let ia_addr = [v6::DhcpOption::IaAddr(v6::IaAddrSerializer::new(
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
RENEWED_PREFERRED_LIFETIME.get(),
RENEWED_VALID_LIFETIME.get(),
&[],
))];
let ia_no_addrs_avail = [v6::DhcpOption::StatusCode(
v6::ErrorStatusCode::NoAddrsAvail.into(),
"No address available.",
)];
let ia_prefix = [v6::DhcpOption::IaPrefix(v6::IaPrefixSerializer::new(
RENEWED_PREFERRED_LIFETIME.get(),
RENEWED_VALID_LIFETIME.get(),
CONFIGURED_DELEGATED_PREFIXES[0],
&[],
))];
let ia_no_prefixes_avail = [v6::DhcpOption::StatusCode(
v6::ErrorStatusCode::NoPrefixAvail.into(),
"No prefixes available.",
)];
let ok_iaid = v6::IAID::new(0);
let no_value_avail_iaid = v6::IAID::new(1);
let empty_values_iaid = v6::IAID::new(2);
let options = vec![
v6::DhcpOption::ClientId(&CLIENT_ID),
v6::DhcpOption::ServerId(&SERVER_ID[0]),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
ok_iaid,
ia_na_success_t1.get(),
ia_na_success_t2.get(),
&ia_addr,
)),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
no_value_avail_iaid,
// If the server returns an IA with status code indicating
// failure, the T1/T2 values for that IA should not be included
// in the T1/T2 calculation.
TINY_NON_ZERO_OR_MAX_U32.get(),
TINY_NON_ZERO_OR_MAX_U32.get(),
&ia_no_addrs_avail,
)),
v6::DhcpOption::Iana(v6::IanaSerializer::new(
empty_values_iaid,
// If the server returns an IA_NA with no IA Address option, the
// T1/T2 values for that IA should not be included in the T1/T2
// calculation.
TINY_NON_ZERO_OR_MAX_U32.get(),
TINY_NON_ZERO_OR_MAX_U32.get(),
&[],
)),
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
ok_iaid,
ia_pd_success_t1.get(),
ia_pd_success_t2.get(),
&ia_prefix,
)),
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
no_value_avail_iaid,
// If the server returns an IA with status code indicating
// failure, the T1/T2 values for that IA should not be included
// in the T1/T2 calculation.
TINY_NON_ZERO_OR_MAX_U32.get(),
TINY_NON_ZERO_OR_MAX_U32.get(),
&ia_no_prefixes_avail,
)),
v6::DhcpOption::IaPd(v6::IaPdSerializer::new(
empty_values_iaid,
// If the server returns an IA_PD with no IA Prefix option, the
// T1/T2 values for that IA should not be included in the T1/T2
// calculation.
TINY_NON_ZERO_OR_MAX_U32.get(),
TINY_NON_ZERO_OR_MAX_U32.get(),
&[],
)),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
fn get_updates<V: IaValue>(
ok_iaid: v6::IAID,
ok_value: V,
no_value_avail_iaid: v6::IAID,
no_value_avail_value: V,
) -> HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>> {
HashMap::from([
(
ok_iaid,
HashMap::from([(
ok_value,
IaValueUpdateKind::UpdatedLifetimes(Lifetimes::new_renewed()),
)]),
),
(
no_value_avail_iaid,
HashMap::from([(no_value_avail_value, IaValueUpdateKind::Removed)]),
),
])
}
let expected_t1 = std::cmp::min(ia_na_success_t1, ia_pd_success_t1);
let expected_t2 = std::cmp::min(ia_na_success_t2, ia_pd_success_t2);
assert_eq!(
client.handle_message_receive(msg, time),
[
Action::CancelTimer(ClientTimerType::Retransmission),
if expected_t1 == expected_t2 {
// Skip Renew and just go to Rebind when T2 == T1.
Action::CancelTimer(ClientTimerType::Renew)
} else {
Action::ScheduleTimer(
ClientTimerType::Renew,
time.add(Duration::from_secs(expected_t1.get().into())),
)
},
Action::ScheduleTimer(
ClientTimerType::Rebind,
time.add(Duration::from_secs(expected_t2.get().into())),
),
Action::IaNaUpdates(get_updates(
ok_iaid,
CONFIGURED_NON_TEMPORARY_ADDRESSES[0],
no_value_avail_iaid,
CONFIGURED_NON_TEMPORARY_ADDRESSES[1],
)),
Action::IaPdUpdates(get_updates(
ok_iaid,
CONFIGURED_DELEGATED_PREFIXES[0],
no_value_avail_iaid,
CONFIGURED_DELEGATED_PREFIXES[1],
)),
Action::ScheduleTimer(
ClientTimerType::RestartServerDiscovery,
time.add(Duration::from_secs(
std::cmp::max(VALID_LIFETIME, RENEWED_VALID_LIFETIME,).get().into()
)),
),
],
);
}
#[test]
fn unexpected_messages_are_ignored() {
let (mut client, _) = ClientStateMachine::start_stateless(
[0, 1, 2],
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
let builder = v6::MessageBuilder::new(
v6::MessageType::Reply,
// Transaction ID is different from the client's.
[4, 5, 6],
&[],
);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert!(client.handle_message_receive(msg, Instant::now()).is_empty());
// Messages with unsupported/unexpected types are discarded.
for msg_type in [
v6::MessageType::Solicit,
v6::MessageType::Advertise,
v6::MessageType::Request,
v6::MessageType::Confirm,
v6::MessageType::Renew,
v6::MessageType::Rebind,
v6::MessageType::Release,
v6::MessageType::Decline,
v6::MessageType::Reconfigure,
v6::MessageType::InformationRequest,
v6::MessageType::RelayForw,
v6::MessageType::RelayRepl,
] {
let ClientStateMachine { transaction_id, options_to_request: _, state: _, rng: _ } =
&client;
let builder = v6::MessageBuilder::new(msg_type, *transaction_id, &[]);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
assert!(client.handle_message_receive(msg, Instant::now()).is_empty());
}
}
#[test]
#[should_panic(expected = "received unexpected refresh timeout")]
fn information_requesting_refresh_timeout_is_unreachable() {
let (mut client, _) = ClientStateMachine::start_stateless(
[0, 1, 2],
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
// Should panic if Refresh timeout is received while in
// InformationRequesting state.
let _actions = client.handle_timeout(ClientTimerType::Refresh, Instant::now());
}
#[test]
#[should_panic(expected = "received unexpected retransmission timeout")]
fn information_received_retransmission_timeout_is_unreachable() {
let (mut client, _) = ClientStateMachine::start_stateless(
[0, 1, 2],
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
let ClientStateMachine { transaction_id, options_to_request: _, state, rng: _ } = &client;
assert_matches!(
*state,
Some(ClientState::InformationRequesting(InformationRequesting {
retrans_timeout: INITIAL_INFO_REQ_TIMEOUT,
_marker,
}))
);
let options = [v6::DhcpOption::ServerId(&SERVER_ID[0])];
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, *transaction_id, &options);
let mut buf = vec![0; builder.bytes_len()];
builder.serialize(&mut buf);
let mut buf = &buf[..]; // Implements BufferView.
let msg = v6::Message::parse(&mut buf, ()).expect("failed to parse test buffer");
// Transition to InformationReceived state.
let time = Instant::now();
let actions = client.handle_message_receive(msg, time);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
assert_matches!(
state,
Some(ClientState::InformationReceived(InformationReceived { dns_servers, _marker }))
if dns_servers.is_empty()
);
assert_eq!(
actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::ScheduleTimer(ClientTimerType::Refresh, time.add(IRT_DEFAULT)),
]
);
// Should panic if Retransmission timeout is received while in
// InformationReceived state.
let _actions = client.handle_timeout(ClientTimerType::Retransmission, time);
}
#[test]
#[should_panic(expected = "received unexpected refresh timeout")]
fn server_discovery_refresh_timeout_is_unreachable() {
let time = Instant::now();
let mut client = testutil::start_and_assert_server_discovery(
[0, 1, 2],
&(CLIENT_ID.into()),
testutil::to_configured_addresses(
1,
std::iter::once(HashSet::from([CONFIGURED_NON_TEMPORARY_ADDRESSES[0]])),
),
Default::default(),
Vec::new(),
StepRng::new(u64::MAX / 2, 0),
time,
);
// Should panic if Refresh is received while in ServerDiscovery state.
let _actions = client.handle_timeout(ClientTimerType::Refresh, time);
}
#[test]
#[should_panic(expected = "received unexpected refresh timeout")]
fn requesting_refresh_timeout_is_unreachable() {
let time = Instant::now();
let (mut client, _transaction_id) = testutil::request_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
vec![TestIaNa::new_default(CONFIGURED_NON_TEMPORARY_ADDRESSES[0])],
Default::default(),
&[],
StepRng::new(u64::MAX / 2, 0),
time,
);
// Should panic if Refresh is received while in Requesting state.
let _actions = client.handle_timeout(ClientTimerType::Refresh, time);
}
#[test_case(ClientTimerType::Refresh)]
#[test_case(ClientTimerType::Retransmission)]
#[should_panic(expected = "received unexpected")]
fn address_assiged_unexpected_timeout_is_unreachable(timeout: ClientTimerType) {
let time = Instant::now();
let (mut client, _actions) = testutil::assign_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
vec![TestIaNa::new_default(CONFIGURED_NON_TEMPORARY_ADDRESSES[0])],
Default::default(), /* delegated_prefixes_to_assign */
&[],
StepRng::new(u64::MAX / 2, 0),
time,
);
// Should panic if Refresh or Retransmission timeout is received while
// in Assigned state.
let _actions = client.handle_timeout(timeout, time);
}
#[test_case(RENEW_TEST)]
#[test_case(REBIND_TEST)]
#[should_panic(expected = "received unexpected refresh timeout")]
fn refresh_timeout_is_unreachable(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
) {
let time = Instant::now();
let mut client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
vec![TestIaNa::new_default(CONFIGURED_NON_TEMPORARY_ADDRESSES[0])],
Default::default(), /* delegated_prefixes_to_assign */
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
time,
);
// Should panic if Refresh is received while in Renewing state.
let _actions = client.handle_timeout(ClientTimerType::Refresh, time);
}
fn handle_all_leases_invalidated<R: Rng>(
mut client: ClientStateMachine<Instant, R>,
client_id: &[u8],
non_temporary_addresses_to_assign: Vec<TestIaNa>,
delegated_prefixes_to_assign: Vec<TestIaPd>,
skip_removed_event_for_test_iana_idx: Option<usize>,
skip_removed_event_for_test_iapd_idx: Option<usize>,
options_to_request: &[v6::OptionCode],
) {
let time = Instant::now();
let actions = client.handle_timeout(ClientTimerType::RestartServerDiscovery, time);
let buf = assert_matches!(
&actions[..],
[
Action::CancelTimer(ClientTimerType::Retransmission),
Action::CancelTimer(ClientTimerType::Refresh),
Action::CancelTimer(ClientTimerType::Renew),
Action::CancelTimer(ClientTimerType::Rebind),
Action::CancelTimer(ClientTimerType::RestartServerDiscovery),
Action::IaNaUpdates(ia_na_updates),
Action::IaPdUpdates(ia_pd_updates),
Action::SendMessage(buf),
Action::ScheduleTimer(ClientTimerType::Retransmission, instant)
] => {
fn get_updates<V: IaValue>(
to_assign: &Vec<TestIa<V>>,
skip_idx: Option<usize>,
) -> HashMap<v6::IAID, HashMap<V, IaValueUpdateKind>> {
(0..).zip(to_assign.iter())
.filter_map(|(iaid, TestIa { values, t1: _, t2: _})| {
skip_idx
.map_or(true, |skip_idx| skip_idx != iaid)
.then(|| (
v6::IAID::new(iaid.try_into().unwrap()),
values.keys().copied().map(|value| (
value,
IaValueUpdateKind::Removed,
)).collect(),
))
})
.collect()
}
assert_eq!(
ia_na_updates,
&get_updates(
&non_temporary_addresses_to_assign,
skip_removed_event_for_test_iana_idx
),
);
assert_eq!(
ia_pd_updates,
&get_updates(
&delegated_prefixes_to_assign,
skip_removed_event_for_test_iapd_idx,
),
);
assert_eq!(*instant, time.add(INITIAL_SOLICIT_TIMEOUT));
buf
}
);
let ClientStateMachine { transaction_id: _, options_to_request: _, state, rng: _ } =
&client;
testutil::assert_server_discovery(
state,
client_id,
testutil::to_configured_addresses(
non_temporary_addresses_to_assign.len(),
non_temporary_addresses_to_assign
.iter()
.map(|TestIaNa { values, t1: _, t2: _ }| values.keys().cloned().collect()),
),
testutil::to_configured_prefixes(
delegated_prefixes_to_assign.len(),
delegated_prefixes_to_assign
.iter()
.map(|TestIaPd { values, t1: _, t2: _ }| values.keys().cloned().collect()),
),
time,
buf,
options_to_request,
)
}
#[test]
fn assigned_handle_all_leases_invalidated() {
let non_temporary_addresses_to_assign = CONFIGURED_NON_TEMPORARY_ADDRESSES
.iter()
.copied()
.map(TestIaNa::new_default)
.collect::<Vec<_>>();
let delegated_prefixes_to_assign = CONFIGURED_DELEGATED_PREFIXES
.iter()
.copied()
.map(TestIaPd::new_default)
.collect::<Vec<_>>();
let (client, _actions) = testutil::assign_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
non_temporary_addresses_to_assign.clone(),
delegated_prefixes_to_assign.clone(),
&[],
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
handle_all_leases_invalidated(
client,
&CLIENT_ID,
non_temporary_addresses_to_assign,
delegated_prefixes_to_assign,
None,
None,
&[],
)
}
#[test_case(RENEW_TEST)]
#[test_case(REBIND_TEST)]
fn renew_rebind_handle_all_leases_invalidated(
RenewRebindTest {
send_and_assert,
message_type: _,
expect_server_id: _,
with_state: _,
allow_response_from_any_server: _,
}: RenewRebindTest,
) {
let non_temporary_addresses_to_assign = CONFIGURED_NON_TEMPORARY_ADDRESSES[0..2]
.into_iter()
.map(|&addr| TestIaNa::new_default(addr))
.collect::<Vec<_>>();
let delegated_prefixes_to_assign = CONFIGURED_DELEGATED_PREFIXES[0..2]
.into_iter()
.map(|&addr| TestIaPd::new_default(addr))
.collect::<Vec<_>>();
let client = send_and_assert(
&(CLIENT_ID.into()),
SERVER_ID[0],
non_temporary_addresses_to_assign.clone(),
delegated_prefixes_to_assign.clone(),
None,
T1,
T2,
v6::NonZeroTimeValue::Finite(VALID_LIFETIME),
StepRng::new(u64::MAX / 2, 0),
Instant::now(),
);
handle_all_leases_invalidated(
client,
&CLIENT_ID,
non_temporary_addresses_to_assign,
delegated_prefixes_to_assign,
None,
None,
&[],
)
}
// NOTE: All comparisons are done on millisecond, so this test is not affected by precision
// loss from floating point arithmetic.
#[test]
fn retransmission_timeout() {
let mut rng = StepRng::new(u64::MAX / 2, 0);
let initial_rt = Duration::from_secs(1);
let max_rt = Duration::from_secs(100);
// Start with initial timeout if previous timeout is zero.
let t =
super::retransmission_timeout(Duration::from_nanos(0), initial_rt, max_rt, &mut rng);
assert_eq!(t.as_millis(), initial_rt.as_millis());
// Use previous timeout when it's not zero and apply the formula.
let t =
super::retransmission_timeout(Duration::from_secs(10), initial_rt, max_rt, &mut rng);
assert_eq!(t, Duration::from_secs(20));
// Cap at max timeout.
let t = super::retransmission_timeout(100 * max_rt, initial_rt, max_rt, &mut rng);
assert_eq!(t.as_millis(), max_rt.as_millis());
let t = super::retransmission_timeout(MAX_DURATION, initial_rt, max_rt, &mut rng);
assert_eq!(t.as_millis(), max_rt.as_millis());
// Zero max means no cap.
let t = super::retransmission_timeout(
100 * max_rt,
initial_rt,
Duration::from_nanos(0),
&mut rng,
);
assert_eq!(t.as_millis(), (200 * max_rt).as_millis());
// Overflow durations are clipped.
let t = super::retransmission_timeout(
MAX_DURATION,
initial_rt,
Duration::from_nanos(0),
&mut rng,
);
assert_eq!(t.as_millis(), MAX_DURATION.as_millis());
// Steps through the range with deterministic randomness, 20% at a time.
let mut rng = StepRng::new(0, u64::MAX / 5);
[
(Duration::from_millis(10000), 19000),
(Duration::from_millis(10000), 19400),
(Duration::from_millis(10000), 19800),
(Duration::from_millis(10000), 20200),
(Duration::from_millis(10000), 20600),
(Duration::from_millis(10000), 21000),
(Duration::from_millis(10000), 19400),
// Cap at max timeout with randomness.
(100 * max_rt, 98000),
(100 * max_rt, 102000),
(100 * max_rt, 106000),
(100 * max_rt, 110000),
(100 * max_rt, 94000),
(100 * max_rt, 98000),
]
.iter()
.for_each(|(rt, want_ms)| {
let t = super::retransmission_timeout(*rt, initial_rt, max_rt, &mut rng);
assert_eq!(t.as_millis(), *want_ms);
});
}
#[test_case(v6::TimeValue::Zero, v6::TimeValue::Zero, v6::TimeValue::Zero)]
#[test_case(
v6::TimeValue::Zero,
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
))
)]
#[test_case(
v6::TimeValue::Zero,
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity)
)]
#[test_case(
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)),
v6::TimeValue::Zero,
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
))
)]
#[test_case(
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(60)
.expect("should succeed for non-zero or u32::MAX values")
)),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(60)
.expect("should succeed for non-zero or u32::MAX values")
))
)]
#[test_case(
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
))
)]
#[test_case(
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
))
)]
#[test_case(
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity)
)]
fn maybe_get_nonzero_min(
old_value: v6::TimeValue,
new_value: v6::TimeValue,
expected_value: v6::TimeValue,
) {
assert_eq!(super::maybe_get_nonzero_min(old_value, new_value), expected_value);
}
#[test_case(
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
),
v6::TimeValue::Zero,
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)
)]
#[test_case(
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(60)
.expect("should succeed for non-zero or u32::MAX values")
)),
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(60)
.expect("should succeed for non-zero or u32::MAX values")
)
)]
#[test_case(
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
),
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)
)]
#[test_case(
v6::NonZeroTimeValue::Infinity,
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values"))
),
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(120)
.expect("should succeed for non-zero or u32::MAX values")
)
)]
#[test_case(
v6::NonZeroTimeValue::Infinity,
v6::TimeValue::NonZero(v6::NonZeroTimeValue::Infinity),
v6::NonZeroTimeValue::Infinity
)]
#[test_case(
v6::NonZeroTimeValue::Infinity,
v6::TimeValue::Zero,
v6::NonZeroTimeValue::Infinity
)]
fn get_nonzero_min(
old_value: v6::NonZeroTimeValue,
new_value: v6::TimeValue,
expected_value: v6::NonZeroTimeValue,
) {
assert_eq!(super::get_nonzero_min(old_value, new_value), expected_value);
}
#[test_case(
v6::NonZeroTimeValue::Infinity,
T1_MIN_LIFETIME_RATIO,
v6::NonZeroTimeValue::Infinity
)]
#[test_case(
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(100).expect("should succeed")),
T1_MIN_LIFETIME_RATIO,
v6::NonZeroTimeValue::Finite(v6::NonZeroOrMaxU32::new(50).expect("should succeed"))
)]
#[test_case(v6::NonZeroTimeValue::Infinity, T2_T1_RATIO, v6::NonZeroTimeValue::Infinity)]
#[test_case(
v6::NonZeroTimeValue::Finite(
v6::NonZeroOrMaxU32::new(INFINITY - 1)
.expect("should succeed")
),
T2_T1_RATIO,
v6::NonZeroTimeValue::Infinity
)]
fn compute_t(min: v6::NonZeroTimeValue, ratio: Ratio<u32>, expected_t: v6::NonZeroTimeValue) {
assert_eq!(super::compute_t(min, ratio), expected_t);
}
}