blob: 52ac2664f29ade3827316d5a2208b22128779849 [file] [log] [blame]
// Copyright 2018 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.
use crate::configuration::ServerParameters;
use crate::protocol::{
DhcpOption, FidlCompatible, FromFidlExt, IntoFidlExt, Message, MessageType, OpCode, OptionCode,
ProtocolError,
};
use crate::stash::Stash;
use anyhow::{Context as _, Error};
use fidl_fuchsia_hardware_ethernet_ext::MacAddress as MacAddr;
use fuchsia_zircon::Status;
use serde_derive::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap, HashSet};
use std::convert::TryFrom;
use std::fmt;
use std::net::Ipv4Addr;
use thiserror::Error;
/// A minimal DHCP server.
///
/// This comment will be expanded upon in future CLs as the server design
/// is iterated upon.
pub struct Server {
cache: CachedClients,
pool: AddressPool,
params: ServerParameters,
stash: Stash,
options_repo: HashMap<OptionCode, DhcpOption>,
}
/// The default string used by the Server to identify itself to the Stash service.
pub const DEFAULT_STASH_ID: &str = "dhcpd";
/// The default prefix used by the Server in the keys for values stored in the Stash service.
pub const DEFAULT_STASH_PREFIX: &str = "";
/// This enumerates the actions a DHCP server can take in response to a
/// received client message. A `SendResponse(Message, Ipv4Addr)` indicates
/// that a `Message` needs to be delivered back to the client.
/// The server may optionally send a destination `Ipv4Addr` (if the protocol
/// warrants it) to direct the response `Message` to.
/// The other two variants indicate a successful processing of a client
/// `Decline` or `Release`.
/// Implements `PartialEq` for test assertions.
#[derive(Debug, PartialEq)]
pub enum ServerAction {
SendResponse(Message, Option<Ipv4Addr>),
AddressDecline(Ipv4Addr),
AddressRelease(Ipv4Addr),
}
/// A wrapper around the error types which can be returned by DHCP Server
/// in response to client requests.
/// Implements `PartialEq` for test assertions.
#[derive(Debug, Error, PartialEq)]
pub enum ServerError {
#[error("unexpected client message type: {}", _0)]
UnexpectedClientMessageType(MessageType),
#[error("requested ip parsing failure: {}", _0)]
BadRequestedIpv4Addr(String),
#[error("local address pool manipulation error: {}", _0)]
ServerAddressPoolFailure(AddressPoolError),
#[error("incorrect server ip in client message: {}", _0)]
IncorrectDHCPServer(Ipv4Addr),
#[error("requested ip mismatch with offered ip: {} {}", _0, _1)]
RequestedIpOfferIpMismatch(Ipv4Addr, Ipv4Addr),
#[error("expired client config")]
ExpiredClientConfig,
#[error("requested ip absent from server pool: {}", _0)]
UnidentifiedRequestedIp(Ipv4Addr),
#[error("unknown client mac: {}", _0)]
UnknownClientMac(MacAddr),
#[error("init reboot request did not include ip")]
NoRequestedAddrAtInitReboot,
#[error("unidentified client state during request")]
UnknownClientStateDuringRequest,
#[error("decline request did not include ip")]
NoRequestedAddrForDecline,
#[error("client request error: {}", _0)]
ClientMessageError(ProtocolError),
#[error("error manipulating server cache: {}", _0)]
ServerCacheUpdateFailure(StashError),
#[error("server not configured with an ip address")]
ServerMissingIpAddr,
#[error("missing required dhcp option: {:?}", _0)]
MissingRequiredDhcpOption(OptionCode),
#[error("unable to get system time")]
// The underlying error is not provided to this variant as it (std::time::SystemTimeError) does
// not implement PartialEq.
ServerTimeError,
}
impl From<AddressPoolError> for ServerError {
fn from(e: AddressPoolError) -> Self {
ServerError::ServerAddressPoolFailure(e)
}
}
/// This struct is used to hold the `anyhow::Error` returned by the server's
/// Stash manipulation methods. We manually implement `PartialEq` so this
/// struct could be included in the `ServerError` enum,
/// which are asserted for equality in tests.
#[derive(Debug)]
pub struct StashError {
error: Error,
}
impl PartialEq for StashError {
fn eq(&self, _other: &Self) -> bool {
false
}
}
impl fmt::Display for StashError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self, f)
}
}
impl Server {
/// Instantiates a server with a random stash identifier.
/// Used in tests to ensure that each test has an isolated stash instance.
#[cfg(test)]
pub async fn new_test_server() -> Result<Server, Error> {
use rand::Rng;
let rand_string: String =
rand::thread_rng().sample_iter(&rand::distributions::Alphanumeric).take(8).collect();
let lease_length = crate::configuration::LeaseLength {
default_seconds: 60 * 60 * 24,
max_seconds: 60 * 60 * 24 * 7,
};
let params = ServerParameters {
server_ips: vec![],
lease_length,
managed_addrs: crate::configuration::ManagedAddresses {
network_id: Ipv4Addr::new(192, 168, 0, 0),
broadcast: Ipv4Addr::new(192, 168, 0, 255),
mask: crate::configuration::SubnetMask::try_from(24)?,
pool_range_start: Ipv4Addr::new(192, 168, 0, 0),
pool_range_stop: Ipv4Addr::new(192, 168, 0, 0),
},
permitted_macs: crate::configuration::PermittedMacs(Vec::new()),
static_assignments: crate::configuration::StaticAssignments(HashMap::new()),
arp_probe: false,
bound_device_names: vec![],
};
let stash = Stash::new(&rand_string, DEFAULT_STASH_PREFIX)?;
Ok(Self {
cache: HashMap::new(),
pool: AddressPool::new(params.managed_addrs.pool_range()),
params,
stash,
options_repo: HashMap::new(),
})
}
/// Instantiates a new `Server` value from the provided parts.
pub fn new(
stash: Stash,
params: ServerParameters,
options_repo: HashMap<OptionCode, DhcpOption>,
cache: CachedClients,
) -> Self {
Self {
cache,
pool: AddressPool::new(params.managed_addrs.pool_range()),
params,
stash,
options_repo,
}
}
/// Returns true if the server has a populated address pool and is therefore serving requests.
pub fn is_serving(&self) -> bool {
!self.pool.is_empty()
}
/// Dispatches an incoming DHCP message to the appropriate handler for processing.
///
/// If the incoming message is a valid client DHCP message, then the server will attempt to
/// take appropriate action to serve the client's request, update the internal server state,
/// and return the suitable response.
/// If the incoming message is invalid, or the server is unable to serve the request,
/// or the processing of the client's request resulted in an error, then `dispatch()`
/// will return the fitting `Err` indicating what went wrong.
pub fn dispatch(&mut self, msg: Message) -> Result<ServerAction, ServerError> {
match msg.get_dhcp_type().map_err(ServerError::ClientMessageError)? {
MessageType::DHCPDISCOVER => self.handle_discover(msg),
MessageType::DHCPOFFER => {
Err(ServerError::UnexpectedClientMessageType(MessageType::DHCPOFFER))
}
MessageType::DHCPREQUEST => self.handle_request(msg),
MessageType::DHCPDECLINE => self.handle_decline(msg),
MessageType::DHCPACK => {
Err(ServerError::UnexpectedClientMessageType(MessageType::DHCPACK))
}
MessageType::DHCPNAK => {
Err(ServerError::UnexpectedClientMessageType(MessageType::DHCPNAK))
}
MessageType::DHCPRELEASE => self.handle_release(msg),
MessageType::DHCPINFORM => self.handle_inform(msg),
}
}
/// This method calculates the destination address of the server response
/// based on the conditions specified in -
/// https://tools.ietf.org/html/rfc2131#section-4.1 Page 22, Paragraph 4.
fn get_destination_addr(&mut self, client_msg: &Message) -> Option<Ipv4Addr> {
if !client_msg.giaddr.is_unspecified() {
Some(client_msg.giaddr)
} else if !client_msg.ciaddr.is_unspecified() {
Some(client_msg.ciaddr)
} else if client_msg.bdcast_flag {
Some(Ipv4Addr::BROADCAST)
} else {
client_msg.get_dhcp_type().ok().and_then(|typ| {
match typ {
// TODO(fxbug.dev/35087): Revisit the first match arm.
// Instead of returning BROADCAST address, server must update ARP table.
//
// Current Implementation =>
// When client's message has unspecified `giaddr`, 'ciaddr' with
// broadcast bit is not set, we broadcast the response on the subnet.
//
// Desired Implementation =>
// Message should be unicast to client's mac address specified in `chaddr`.
//
// See https://tools.ietf.org/html/rfc2131#section-4.1 Page 22, Paragraph 4.
MessageType::DHCPDISCOVER => Some(Ipv4Addr::BROADCAST),
MessageType::DHCPREQUEST | MessageType::DHCPINFORM => Some(client_msg.yiaddr),
MessageType::DHCPACK
| MessageType::DHCPNAK
| MessageType::DHCPOFFER
| MessageType::DHCPDECLINE
| MessageType::DHCPRELEASE => None,
}
})
}
}
fn handle_discover(&mut self, disc: Message) -> Result<ServerAction, ServerError> {
let offered_ip = self.get_addr(&disc)?;
let dest = self.get_destination_addr(&disc);
let offer = self.build_offer(disc, offered_ip)?;
match self.store_client_config(Ipv4Addr::from(offer.yiaddr), offer.chaddr, &offer.options) {
Ok(()) => Ok(ServerAction::SendResponse(offer, dest)),
Err(e) => Err(ServerError::ServerCacheUpdateFailure(StashError { error: e })),
}
}
fn get_addr(&mut self, client: &Message) -> Result<Ipv4Addr, ServerError> {
if let Some(config) = self.cache.get(&client.chaddr) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|std::time::SystemTimeError { .. }| ServerError::ServerTimeError)?;
if !config.expired(now) {
// Release cached address so that it can be reallocated to same client.
// This should NEVER return an `Err`. If it does it indicates
// the server's notion of client bindings is wrong.
// Its non-recoverable and we therefore panic.
if let Err(AddressPoolError::UnallocatedIpv4AddrRelease(addr)) =
self.pool.release_addr(config.client_addr)
{
panic!("server tried to release unallocated ip {}", addr)
}
return Ok(config.client_addr);
} else if self.pool.addr_is_available(config.client_addr) {
return Ok(config.client_addr);
}
}
if let Some(requested_addr) = get_requested_ip_addr(&client) {
if self.pool.addr_is_available(requested_addr) {
return Ok(requested_addr);
}
}
self.pool.get_next_available_addr().map_err(AddressPoolError::into)
}
fn store_client_config(
&mut self,
client_addr: Ipv4Addr,
client_mac: MacAddr,
client_opts: &[DhcpOption],
) -> Result<(), Error> {
let lease_length_seconds = client_opts
.iter()
.filter_map(|opt| match opt {
DhcpOption::IpAddressLeaseTime(v) => Some(*v),
_ => None,
})
.next()
.ok_or(ServerError::MissingRequiredDhcpOption(OptionCode::IpAddressLeaseTime))?;
let options = client_opts
.iter()
.filter(|opt| {
// DhcpMessageType is part of transaction semantics and should not be stored.
opt.code() != OptionCode::DhcpMessageType
})
.cloned()
.collect();
let config = CachedConfig::new(
client_addr,
options,
std::time::SystemTime::now(),
lease_length_seconds,
)?;
self.stash
.store_client_config(&client_mac, &config)
.context("failed to store client in stash")?;
self.cache.insert(client_mac, config);
// This should NEVER return an `Err`. If it does it indicates
// server's state has changed in the middle of request handling.
// This is non-recoverable and we therefore panic.
if let Err(AddressPoolError::UnavailableIpv4AddrAllocation(addr)) =
self.pool.allocate_addr(client_addr)
{
panic!("server tried to allocate unavailable ip {}", addr)
}
Ok(())
}
fn handle_request(&mut self, req: Message) -> Result<ServerAction, ServerError> {
match get_client_state(&req).map_err(|()| ServerError::UnknownClientStateDuringRequest)? {
ClientState::Selecting => self.handle_request_selecting(req),
ClientState::InitReboot => self.handle_request_init_reboot(req),
ClientState::Renewing => self.handle_request_renewing(req),
}
}
fn handle_request_selecting(&mut self, req: Message) -> Result<ServerAction, ServerError> {
let requested_ip = req.ciaddr;
if !is_recipient(&self.params.server_ips, &req) {
Err(ServerError::IncorrectDHCPServer(
*self.params.server_ips.first().ok_or(ServerError::ServerMissingIpAddr)?,
))
} else {
let () = self.validate_requested_addr_with_client(&req, requested_ip)?;
let dest = self.get_destination_addr(&req);
Ok(ServerAction::SendResponse(self.build_ack(req, requested_ip)?, dest))
}
}
/// The function below validates if the `requested_ip` is correctly
/// associated with the client whose request `req` is being processed.
///
/// It first checks if the client bindings can be found in server cache.
/// If not, the association is wrong and it returns an `Err()`.
///
/// If the server can correctly locate the client bindings in its cache,
/// it further verifies if the `requested_ip` is the same as the ip address
/// represented in the bindings and the binding is not expired and that the
/// `requested_ip` is no longer available in the server address pool. If
/// all the above conditions are met, it returns an `Ok(())` else the
/// appropriate `Err()` value is returned.
fn validate_requested_addr_with_client(
&self,
req: &Message,
requested_ip: Ipv4Addr,
) -> Result<(), ServerError> {
if let Some(client_config) = self.cache.get(&req.chaddr) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|std::time::SystemTimeError { .. }| ServerError::ServerTimeError)?;
if client_config.client_addr != requested_ip {
Err(ServerError::RequestedIpOfferIpMismatch(
requested_ip,
client_config.client_addr,
))
} else if client_config.expired(now) {
Err(ServerError::ExpiredClientConfig)
} else if !self.pool.addr_is_allocated(requested_ip) {
Err(ServerError::UnidentifiedRequestedIp(requested_ip))
} else {
Ok(())
}
} else {
Err(ServerError::UnknownClientMac(req.chaddr))
}
}
fn handle_request_init_reboot(&mut self, req: Message) -> Result<ServerAction, ServerError> {
let requested_ip =
get_requested_ip_addr(&req).ok_or(ServerError::NoRequestedAddrAtInitReboot)?;
if !is_in_subnet(&req, &self.params) {
let error_msg = "client and server are in different subnets";
let (nak, dest) = self.build_nak(req, error_msg)?;
return Ok(ServerAction::SendResponse(nak, dest));
}
if !is_client_mac_known(req.chaddr, &self.cache) {
return Err(ServerError::UnknownClientMac(req.chaddr));
}
if self.validate_requested_addr_with_client(&req, requested_ip).is_err() {
let error_msg = "requested ip is not assigned to client";
let (nak, dest) = self.build_nak(req, error_msg)?;
return Ok(ServerAction::SendResponse(nak, dest));
}
let dest = self.get_destination_addr(&req);
Ok(ServerAction::SendResponse(self.build_ack(req, requested_ip)?, dest))
}
fn handle_request_renewing(&mut self, req: Message) -> Result<ServerAction, ServerError> {
let client_ip = req.ciaddr;
let () = self.validate_requested_addr_with_client(&req, client_ip)?;
let dest = self.get_destination_addr(&req);
Ok(ServerAction::SendResponse(self.build_ack(req, client_ip)?, dest))
}
/// TODO(fxbug.dev/21422): Ensure server behavior is as intended.
fn handle_decline(&mut self, dec: Message) -> Result<ServerAction, ServerError> {
let declined_ip =
get_requested_ip_addr(&dec).ok_or_else(|| ServerError::NoRequestedAddrForDecline)?;
if is_recipient(&self.params.server_ips, &dec)
&& self.validate_requested_addr_with_client(&dec, declined_ip).is_err()
{
let () = self.pool.allocate_addr(declined_ip)?;
}
self.cache.remove(&dec.chaddr);
Ok(ServerAction::AddressDecline(declined_ip))
}
fn handle_release(&mut self, rel: Message) -> Result<ServerAction, ServerError> {
if self.cache.contains_key(&rel.chaddr) {
let () = self.pool.release_addr(rel.ciaddr)?;
Ok(ServerAction::AddressRelease(rel.ciaddr))
} else {
Err(ServerError::UnknownClientMac(rel.chaddr))
}
}
fn handle_inform(&mut self, inf: Message) -> Result<ServerAction, ServerError> {
// When responding to an INFORM, the server must leave yiaddr zeroed.
let yiaddr = Ipv4Addr::UNSPECIFIED;
let dest = self.get_destination_addr(&inf);
let ack = self.build_inform_ack(inf, yiaddr)?;
Ok(ServerAction::SendResponse(ack, dest))
}
fn build_offer(&self, disc: Message, offered_ip: Ipv4Addr) -> Result<Message, ServerError> {
let mut options = Vec::new();
options.push(DhcpOption::DhcpMessageType(MessageType::DHCPOFFER));
options.push(DhcpOption::ServerIdentifier(self.get_server_ip(&disc)?));
let seconds = match disc
.options
.iter()
.filter_map(|opt| match opt {
DhcpOption::IpAddressLeaseTime(seconds) => Some(*seconds),
_ => None,
})
.next()
{
Some(seconds) => std::cmp::min(seconds, self.params.lease_length.max_seconds),
None => self.params.lease_length.default_seconds,
};
options.push(DhcpOption::IpAddressLeaseTime(seconds));
options.push(DhcpOption::RenewalTimeValue(seconds / 2));
options.push(DhcpOption::RebindingTimeValue((seconds * 3) / 4));
options.extend_from_slice(&self.get_requested_options(&disc.options));
let offer = Message {
op: OpCode::BOOTREPLY,
secs: 0,
yiaddr: offered_ip,
ciaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED,
sname: String::new(),
file: String::new(),
options,
..disc
};
Ok(offer)
}
fn get_requested_options(&self, client_opts: &[DhcpOption]) -> Vec<DhcpOption> {
if let Some(requested_opts) = client_opts
.iter()
.filter_map(|opt| match opt {
DhcpOption::ParameterRequestList(v) => Some(v),
_ => None,
})
.next()
{
let offered_opts: Vec<DhcpOption> = requested_opts
.iter()
.filter_map(|code| match self.options_repo.get(code) {
Some(opt) => Some(opt.clone()),
None => match code {
OptionCode::SubnetMask => {
Some(DhcpOption::SubnetMask(self.params.managed_addrs.mask.into()))
}
_ => None,
},
})
.collect();
offered_opts
} else {
Vec::new()
}
}
fn build_ack(&self, req: Message, requested_ip: Ipv4Addr) -> Result<Message, ServerError> {
let options = match self.cache.get(&req.chaddr) {
Some(config) => {
let mut options = config.options.clone();
options.push(DhcpOption::DhcpMessageType(MessageType::DHCPACK));
options
}
None => return Err(ServerError::UnknownClientMac(req.chaddr)),
};
let ack = Message { op: OpCode::BOOTREPLY, secs: 0, yiaddr: requested_ip, options, ..req };
Ok(ack)
}
fn build_inform_ack(&self, inf: Message, client_ip: Ipv4Addr) -> Result<Message, ServerError> {
let server_ip = self.get_server_ip(&inf)?;
let mut options = Vec::new();
options.push(DhcpOption::DhcpMessageType(MessageType::DHCPACK));
options.push(DhcpOption::ServerIdentifier(server_ip));
options.extend_from_slice(&self.get_requested_options(&inf.options));
let ack = Message { op: OpCode::BOOTREPLY, secs: 0, yiaddr: client_ip, options, ..inf };
Ok(ack)
}
fn build_nak(
&self,
req: Message,
error: &str,
) -> Result<(Message, Option<Ipv4Addr>), ServerError> {
let options = vec![
DhcpOption::DhcpMessageType(MessageType::DHCPNAK),
DhcpOption::ServerIdentifier(self.get_server_ip(&req)?),
DhcpOption::Message(error.to_owned()),
];
let mut nak = Message {
op: OpCode::BOOTREPLY,
secs: 0,
ciaddr: Ipv4Addr::UNSPECIFIED,
yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED,
options,
..req
};
// https://tools.ietf.org/html/rfc2131#section-4.3.2
// Page 31, Paragraph 2-3.
if nak.giaddr.is_unspecified() {
Ok((nak, Some(Ipv4Addr::BROADCAST)))
} else {
nak.bdcast_flag = true;
Ok((nak, None))
}
}
fn get_server_ip(&self, req: &Message) -> Result<Ipv4Addr, ServerError> {
match req
.options
.iter()
.filter_map(|opt| match opt {
DhcpOption::ServerIdentifier(v) => Some(v),
_ => None,
})
.next()
{
Some(v) if self.params.server_ips.contains(v) => Ok(*v),
_ => Ok(*self.params.server_ips.first().ok_or(ServerError::ServerMissingIpAddr)?),
}
}
/// Releases all allocated IP addresses whose leases have expired back to
/// the pool of addresses available for allocation.
pub fn release_expired_leases(&mut self) -> Result<(), ServerError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|std::time::SystemTimeError { .. }| ServerError::ServerTimeError)?;
let expired_clients: Vec<(MacAddr, Ipv4Addr)> = self
.cache
.iter()
.filter(|(_mac, config)| config.expired(now))
.map(|(mac, config)| (*mac, config.client_addr))
.collect();
// Expired client entries must be removed in a separate statement because otherwise we
// would be attempting to change a cache as we iterate over it.
for (mac, ip) in expired_clients.iter() {
// We ignore the `Result` here since a failed release of the `ip`
// in this iteration will be reattempted in the next.
let _release_result = self.pool.release_addr(*ip);
self.cache.remove(mac);
// The call to delete will immediately be committed to the Stash. Since DHCP lease
// acquisitions occur on a human timescale, e.g. a cellphone is brought in range of an
// AP, and at a time resolution of a second, it will be rare for expired_clients to
// contain sufficient numbers of entries that committing with each deletion will impact
// performance.
if let Err(e) = self.stash.delete(&mac) {
// We log the failed deletion here because it would be the action taken by the
// caller and we do not want to stop the deletion loop on account of a single
// failure.
log::warn!("stash failed to delete client={}: {}", mac, e)
}
}
Ok(())
}
}
/// Clears the stash instance at the end of a test.
///
/// This implementation is solely for unit testing, where we do not want data stored in
/// the stash to persist past the execution of the test.
#[cfg(test)]
impl Drop for Server {
fn drop(&mut self) {
if !cfg!(test) {
panic!("dhcp::server::Server implements std::ops::Drop in a non-test cfg");
}
let _result = self.stash.clear();
}
}
/// The ability to dispatch fuchsia.net.dhcp.Server protocol requests and return a value.
///
/// Implementers of this trait can be used as the backing server-side logic of the
/// fuchsia.net.dhcp.Server protocol. Implementers must maintain a store of DHCP Options and
/// DHCP server parameters and support the trait methods to retrieve and modify them.
pub trait ServerDispatcher {
/// Retrieves the stored DHCP option value that corresponds to the OptionCode argument.
fn dispatch_get_option(
&self,
code: fidl_fuchsia_net_dhcp::OptionCode,
) -> Result<fidl_fuchsia_net_dhcp::Option_, Status>;
/// Retrieves the stored DHCP server parameter value that corresponds to the ParameterName argument.
fn dispatch_get_parameter(
&self,
name: fidl_fuchsia_net_dhcp::ParameterName,
) -> Result<fidl_fuchsia_net_dhcp::Parameter, Status>;
/// Updates the stored DHCP option value to the argument.
fn dispatch_set_option(&mut self, value: fidl_fuchsia_net_dhcp::Option_) -> Result<(), Status>;
/// Updates the stored DHCP server parameter to the argument.
fn dispatch_set_parameter(
&mut self,
value: fidl_fuchsia_net_dhcp::Parameter,
) -> Result<(), Status>;
/// Retrieves all of the stored DHCP option values.
fn dispatch_list_options(&self) -> Result<Vec<fidl_fuchsia_net_dhcp::Option_>, Status>;
/// Retrieves all of the stored DHCP parameter values.
fn dispatch_list_parameters(&self) -> Result<Vec<fidl_fuchsia_net_dhcp::Parameter>, Status>;
/// Resets all DHCP options to have no value.
fn dispatch_reset_options(&mut self) -> Result<(), Status>;
/// Resets all DHCP server parameters to their default values in `defaults`.
fn dispatch_reset_parameters(&mut self, defaults: &ServerParameters) -> Result<(), Status>;
}
impl ServerDispatcher for Server {
fn dispatch_get_option(
&self,
code: fidl_fuchsia_net_dhcp::OptionCode,
) -> Result<fidl_fuchsia_net_dhcp::Option_, Status> {
let opt_code =
OptionCode::try_from(code as u8).map_err(|_protocol_error| Status::INVALID_ARGS)?;
let option = self.options_repo.get(&opt_code).ok_or(Status::NOT_FOUND)?;
let option = option.clone();
let fidl_option = option.try_into_fidl().map_err(|protocol_error| {
log::warn!(
"server dispatcher could not convert dhcp option for fidl transport: {}",
protocol_error
);
Status::INTERNAL
})?;
Ok(fidl_option)
}
fn dispatch_get_parameter(
&self,
name: fidl_fuchsia_net_dhcp::ParameterName,
) -> Result<fidl_fuchsia_net_dhcp::Parameter, Status> {
match name {
fidl_fuchsia_net_dhcp::ParameterName::IpAddrs => {
Ok(fidl_fuchsia_net_dhcp::Parameter::IpAddrs(
self.params.server_ips.clone().into_fidl(),
))
}
fidl_fuchsia_net_dhcp::ParameterName::AddressPool => {
Ok(fidl_fuchsia_net_dhcp::Parameter::AddressPool(
self.params.managed_addrs.clone().into_fidl(),
))
}
fidl_fuchsia_net_dhcp::ParameterName::LeaseLength => {
Ok(fidl_fuchsia_net_dhcp::Parameter::Lease(
self.params.lease_length.clone().into_fidl(),
))
}
fidl_fuchsia_net_dhcp::ParameterName::PermittedMacs => {
Ok(fidl_fuchsia_net_dhcp::Parameter::PermittedMacs(
self.params.permitted_macs.clone().into_fidl(),
))
}
fidl_fuchsia_net_dhcp::ParameterName::StaticallyAssignedAddrs => {
Ok(fidl_fuchsia_net_dhcp::Parameter::StaticallyAssignedAddrs(
self.params.static_assignments.clone().into_fidl(),
))
}
fidl_fuchsia_net_dhcp::ParameterName::ArpProbe => {
Ok(fidl_fuchsia_net_dhcp::Parameter::ArpProbe(self.params.arp_probe))
}
fidl_fuchsia_net_dhcp::ParameterName::BoundDeviceNames => {
Ok(fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(
self.params.bound_device_names.clone(),
))
}
}
}
fn dispatch_set_option(&mut self, value: fidl_fuchsia_net_dhcp::Option_) -> Result<(), Status> {
let option = DhcpOption::try_from_fidl(value).map_err(|protocol_error| {
log::warn!(
"server dispatcher could not convert fidl argument into dhcp option: {}",
protocol_error
);
Status::INVALID_ARGS
})?;
let _old = self.options_repo.insert(option.code(), option);
let opts: Vec<DhcpOption> = self.options_repo.values().cloned().collect();
let () = self.stash.store_options(&opts).map_err(|e| {
log::warn!("store_options({:?}) in stash failed: {}", opts, e);
fuchsia_zircon::Status::INTERNAL
})?;
Ok(())
}
fn dispatch_set_parameter(
&mut self,
value: fidl_fuchsia_net_dhcp::Parameter,
) -> Result<(), Status> {
let () = match value {
fidl_fuchsia_net_dhcp::Parameter::IpAddrs(ip_addrs) => {
self.params.server_ips = Vec::<Ipv4Addr>::from_fidl(ip_addrs)
}
fidl_fuchsia_net_dhcp::Parameter::AddressPool(managed_addrs) => {
self.params.managed_addrs =
match crate::configuration::ManagedAddresses::try_from_fidl(managed_addrs) {
Ok(managed_addrs) => managed_addrs,
Err(e) => {
log::info!(
"dispatch_set_parameter() got invalid AddressPool argument: {}",
e
);
return Err(Status::INVALID_ARGS);
}
}
}
fidl_fuchsia_net_dhcp::Parameter::Lease(lease_length) => {
self.params.lease_length =
match crate::configuration::LeaseLength::try_from_fidl(lease_length) {
Ok(lease_length) => lease_length,
Err(e) => {
log::info!(
"dispatch_set_parameter() got invalid LeaseLength argument: {}",
e
);
return Err(Status::INVALID_ARGS);
}
}
}
fidl_fuchsia_net_dhcp::Parameter::PermittedMacs(permitted_macs) => {
self.params.permitted_macs =
crate::configuration::PermittedMacs::from_fidl(permitted_macs)
}
fidl_fuchsia_net_dhcp::Parameter::StaticallyAssignedAddrs(static_assignments) => {
self.params.static_assignments =
match crate::configuration::StaticAssignments::try_from_fidl(static_assignments)
{
Ok(static_assignments) => static_assignments,
Err(e) => {
log::info!("dispatch_set_parameter() got invalid StaticallyAssignedAddrs argument: {}", e);
return Err(Status::INVALID_ARGS);
}
}
}
fidl_fuchsia_net_dhcp::Parameter::ArpProbe(arp_probe) => {
self.params.arp_probe = arp_probe
}
fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(bound_device_names) => {
self.params.bound_device_names = bound_device_names
}
fidl_fuchsia_net_dhcp::Parameter::__UnknownVariant { .. } => {
return Err(Status::INVALID_ARGS)
}
};
let () = self.stash.store_parameters(&self.params).map_err(|e| {
log::warn!("store_parameters({:?}) in stash failed: {}", self.params, e);
fuchsia_zircon::Status::INTERNAL
})?;
Ok(())
}
fn dispatch_list_options(&self) -> Result<Vec<fidl_fuchsia_net_dhcp::Option_>, Status> {
let options = self
.options_repo
.values()
.filter_map(|option| {
option
.clone()
.try_into_fidl()
.map_err(|protocol_error| {
log::warn!(
"server dispatcher could not convert dhcp option for fidl transport: {}",
protocol_error
);
Status::INTERNAL
})
.ok()
})
.collect::<Vec<fidl_fuchsia_net_dhcp::Option_>>();
Ok(options)
}
fn dispatch_list_parameters(&self) -> Result<Vec<fidl_fuchsia_net_dhcp::Parameter>, Status> {
// Without this redundant borrow, the compiler will interpret this statement as a moving destructure.
let ServerParameters {
server_ips,
managed_addrs,
lease_length,
permitted_macs,
static_assignments,
arp_probe,
bound_device_names,
} = &self.params;
Ok(vec![
fidl_fuchsia_net_dhcp::Parameter::IpAddrs(server_ips.clone().into_fidl()),
fidl_fuchsia_net_dhcp::Parameter::AddressPool(managed_addrs.clone().into_fidl()),
fidl_fuchsia_net_dhcp::Parameter::Lease(lease_length.clone().into_fidl()),
fidl_fuchsia_net_dhcp::Parameter::PermittedMacs(permitted_macs.clone().into_fidl()),
fidl_fuchsia_net_dhcp::Parameter::StaticallyAssignedAddrs(
static_assignments.clone().into_fidl(),
),
fidl_fuchsia_net_dhcp::Parameter::ArpProbe(*arp_probe),
fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(bound_device_names.clone()),
])
}
fn dispatch_reset_options(&mut self) -> Result<(), Status> {
let () = self.options_repo.clear();
let opts: Vec<DhcpOption> = self.options_repo.values().cloned().collect();
let () = self.stash.store_options(&opts).map_err(|e| {
log::warn!("store_options({:?}) in stash failed: {}", opts, e);
fuchsia_zircon::Status::INTERNAL
})?;
Ok(())
}
fn dispatch_reset_parameters(&mut self, defaults: &ServerParameters) -> Result<(), Status> {
self.params = defaults.clone();
let () = self.stash.store_parameters(&self.params).map_err(|e| {
log::warn!("store_parameters({:?}) in stash failed: {}", self.params, e);
fuchsia_zircon::Status::INTERNAL
})?;
Ok(())
}
}
/// A cache mapping clients to their configuration data.
///
/// The server should store configuration data for all clients
/// to which it has sent a DHCPOFFER message. Entries in the cache
/// will eventually timeout, although such functionality is currently
/// unimplemented.
pub type CachedClients = HashMap<MacAddr, CachedConfig>;
/// A representation of a DHCP client's stored configuration settings.
///
/// A client's `MacAddr` maps to the `CachedConfig`: this mapping
/// is stored in the `Server`s `CachedClients` instance at runtime, and in
/// `fuchsia.stash` persistent storage.
#[derive(Debug, Deserialize, Serialize)]
pub struct CachedConfig {
client_addr: Ipv4Addr,
options: Vec<DhcpOption>,
lease_start_epoch_seconds: u64,
lease_length_seconds: u32,
}
#[cfg(test)]
impl Default for CachedConfig {
fn default() -> Self {
CachedConfig {
client_addr: Ipv4Addr::UNSPECIFIED,
options: vec![],
lease_start_epoch_seconds: std::u64::MIN,
lease_length_seconds: std::u32::MAX,
}
}
}
impl PartialEq for CachedConfig {
fn eq(&self, other: &Self) -> bool {
// Only compare directly comparable fields.
let CachedConfig {
client_addr,
options,
lease_start_epoch_seconds: _not_comparable,
lease_length_seconds,
} = self;
let CachedConfig {
client_addr: other_client_addr,
options: other_options,
lease_start_epoch_seconds: _other_not_comparable,
lease_length_seconds: other_lease_length_seconds,
} = other;
client_addr == other_client_addr
&& options == other_options
&& lease_length_seconds == other_lease_length_seconds
}
}
impl CachedConfig {
fn new(
client_addr: Ipv4Addr,
options: Vec<DhcpOption>,
lease_start: std::time::SystemTime,
lease_length_seconds: u32,
) -> Result<Self, Error> {
let lease_start_epoch_seconds =
lease_start.duration_since(std::time::UNIX_EPOCH)?.as_secs();
Ok(Self { client_addr, options, lease_start_epoch_seconds, lease_length_seconds })
}
fn expired(&self, since_unix_epoch: std::time::Duration) -> bool {
let end = std::time::Duration::from_secs(
self.lease_start_epoch_seconds + self.lease_length_seconds as u64,
);
since_unix_epoch >= end
}
}
/// The pool of addresses managed by the server.
///
/// Any address managed by the server should be stored in only one
/// of the available/allocated sets at a time. In other words, an
/// address in `available_addrs` must not be in `allocated_addrs` and
/// vice-versa.
#[derive(Debug)]
struct AddressPool {
// available_addrs uses a BTreeSet so that addresses are allocated
// in a deterministic order.
available_addrs: BTreeSet<Ipv4Addr>,
allocated_addrs: HashSet<Ipv4Addr>,
}
//This is a wrapper around different errors that could be returned by
// the DHCP server address pool during address allocation/de-allocation.
#[derive(Debug, Error, PartialEq)]
pub enum AddressPoolError {
#[error("address pool does not have any available ip to hand out")]
Ipv4AddrExhaustion,
#[error("attempted to allocate unavailable ip: {}", _0)]
UnavailableIpv4AddrAllocation(Ipv4Addr),
#[error(" attempted to release unallocated ip: {}", _0)]
UnallocatedIpv4AddrRelease(Ipv4Addr),
}
impl AddressPool {
fn new<T: Iterator<Item = Ipv4Addr>>(addrs: T) -> Self {
Self { available_addrs: addrs.collect(), allocated_addrs: HashSet::new() }
}
/// TODO(fxbug.dev/21423): The ip should be handed out based on client subnet
/// Currently, the server blindly hands out the next available ip
/// from its available ip pool, without any subnet analysis.
///
/// RFC2131#section-4.3.1
///
/// A new address allocated from the server's pool of available
/// addresses; the address is selected based on the subnet from which
/// the message was received (if `giaddr` is 0) or on the address of
/// the relay agent that forwarded the message (`giaddr` when not 0).
fn get_next_available_addr(&self) -> Result<Ipv4Addr, AddressPoolError> {
let mut iter = self.available_addrs.iter();
match iter.next() {
Some(addr) => Ok(*addr),
None => Err(AddressPoolError::Ipv4AddrExhaustion),
}
}
fn allocate_addr(&mut self, addr: Ipv4Addr) -> Result<(), AddressPoolError> {
if self.available_addrs.remove(&addr) {
self.allocated_addrs.insert(addr);
Ok(())
} else {
Err(AddressPoolError::UnavailableIpv4AddrAllocation(addr))
}
}
fn release_addr(&mut self, addr: Ipv4Addr) -> Result<(), AddressPoolError> {
if self.allocated_addrs.remove(&addr) {
self.available_addrs.insert(addr);
Ok(())
} else {
Err(AddressPoolError::UnallocatedIpv4AddrRelease(addr))
}
}
fn addr_is_available(&self, addr: Ipv4Addr) -> bool {
self.available_addrs.contains(&addr) && !self.allocated_addrs.contains(&addr)
}
fn addr_is_allocated(&self, addr: Ipv4Addr) -> bool {
!self.available_addrs.contains(&addr) && self.allocated_addrs.contains(&addr)
}
fn is_empty(&self) -> bool {
self.available_addrs.len() == 0 && self.allocated_addrs.len() == 0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ClientState {
Selecting,
InitReboot,
Renewing,
}
fn is_recipient(server_ips: &Vec<Ipv4Addr>, req: &Message) -> bool {
if let Some(server_id) = get_server_id_from(&req) {
server_ips.contains(&server_id)
} else {
false
}
}
fn is_in_subnet(req: &Message, config: &ServerParameters) -> bool {
let client_ip = match get_requested_ip_addr(&req) {
Some(ip) => ip,
None => return false,
};
config.server_ips.iter().any(|server_ip| {
config.managed_addrs.mask.apply_to(client_ip)
== config.managed_addrs.mask.apply_to(*server_ip)
})
}
fn is_client_mac_known(mac: MacAddr, cache: &CachedClients) -> bool {
cache.get(&mac).is_some()
}
fn get_client_state(msg: &Message) -> Result<ClientState, ()> {
let have_server_id = get_server_id_from(&msg).is_some();
let have_requested_ip = get_requested_ip_addr(&msg).is_some();
if msg.ciaddr.is_unspecified() {
if have_requested_ip {
Ok(ClientState::InitReboot)
} else {
Err(())
}
} else {
if have_server_id && !have_requested_ip {
Ok(ClientState::Selecting)
} else {
Ok(ClientState::Renewing)
}
}
}
fn get_requested_ip_addr(req: &Message) -> Option<Ipv4Addr> {
req.options
.iter()
.filter_map(
|opt| {
if let DhcpOption::RequestedIpAddress(addr) = opt {
Some(*addr)
} else {
None
}
},
)
.next()
}
fn get_server_id_from(req: &Message) -> Option<Ipv4Addr> {
req.options
.iter()
.filter_map(
|opt| {
if let DhcpOption::ServerIdentifier(addr) = opt {
Some(*addr)
} else {
None
}
},
)
.next()
}
#[cfg(test)]
pub mod tests {
use super::*;
use crate::configuration::LeaseLength;
use crate::protocol::{DhcpOption, Message, MessageType, OpCode, OptionCode};
use rand::Rng;
use std::net::Ipv4Addr;
pub fn random_ipv4_generator() -> Ipv4Addr {
let octet1: u8 = rand::thread_rng().gen();
let octet2: u8 = rand::thread_rng().gen();
let octet3: u8 = rand::thread_rng().gen();
let octet4: u8 = rand::thread_rng().gen();
Ipv4Addr::new(octet1, octet2, octet3, octet4)
}
pub fn random_mac_generator() -> MacAddr {
let octet1: u8 = rand::thread_rng().gen();
let octet2: u8 = rand::thread_rng().gen();
let octet3: u8 = rand::thread_rng().gen();
let octet4: u8 = rand::thread_rng().gen();
let octet5: u8 = rand::thread_rng().gen();
let octet6: u8 = rand::thread_rng().gen();
MacAddr { octets: [octet1, octet2, octet3, octet4, octet5, octet6] }
}
fn extract_message(server_response: ServerAction) -> Message {
if let ServerAction::SendResponse(message, _destination) = server_response {
message
} else {
panic!("expected a message in server response, received {:?}", server_response)
}
}
async fn new_test_minimal_server() -> Result<Server, Error> {
let mut server = Server::new_test_server().await.context("failed to instantiate server")?;
server.params.server_ips = vec![random_ipv4_generator()];
let ll = LeaseLength { default_seconds: 100, max_seconds: 60 * 60 * 24 * 7 };
server.params.lease_length = ll;
server
.options_repo
.insert(OptionCode::Router, DhcpOption::Router(vec![random_ipv4_generator()]));
server.options_repo.insert(
OptionCode::DomainNameServer,
DhcpOption::DomainNameServer(vec![
Ipv4Addr::new(8, 8, 8, 8),
Ipv4Addr::new(8, 8, 4, 4),
]),
);
Ok(server)
}
fn new_test_discover() -> Message {
let mut disc = Message::new();
disc.xid = rand::thread_rng().gen();
disc.chaddr = random_mac_generator();
disc.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPDISCOVER));
disc.options.push(DhcpOption::ParameterRequestList(vec![
OptionCode::SubnetMask,
OptionCode::Router,
OptionCode::DomainNameServer,
]));
disc
}
// Creating a new offer needs a reference to `discover` and `server`
// so it can copy over the essential randomly generated options.
fn new_test_offer(disc: &Message, server: &Server) -> Message {
let mut offer = Message::new();
offer.op = OpCode::BOOTREPLY;
offer.xid = disc.xid;
offer.chaddr = disc.chaddr;
offer.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPOFFER));
offer.options.push(DhcpOption::ServerIdentifier(
server.get_server_ip(&disc).unwrap_or(Ipv4Addr::UNSPECIFIED),
));
offer.options.push(DhcpOption::IpAddressLeaseTime(100));
offer.options.push(DhcpOption::RenewalTimeValue(50));
offer.options.push(DhcpOption::RebindingTimeValue(75));
offer.options.push(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
if let Some(routers) = match server.options_repo.get(&OptionCode::Router) {
Some(DhcpOption::Router(v)) => Some(v),
_ => None,
} {
offer.options.push(DhcpOption::Router(routers.clone()));
}
if let Some(servers) = match server.options_repo.get(&OptionCode::DomainNameServer) {
Some(DhcpOption::DomainNameServer(v)) => Some(v),
_ => None,
} {
offer.options.push(DhcpOption::DomainNameServer(servers.clone()));
}
offer
}
fn new_test_request() -> Message {
let mut req = Message::new();
req.xid = rand::thread_rng().gen();
req.chaddr = random_mac_generator();
req.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPREQUEST));
req.options.push(DhcpOption::ParameterRequestList(vec![
OptionCode::SubnetMask,
OptionCode::Router,
OptionCode::DomainNameServer,
]));
req
}
fn new_test_request_selecting_state(server: &Server) -> Message {
let mut req = new_test_request();
req.options.push(DhcpOption::ServerIdentifier(
server.get_server_ip(&req).unwrap_or(Ipv4Addr::UNSPECIFIED),
));
req
}
fn new_test_ack(req: &Message, server: &Server) -> Message {
let mut ack = Message::new();
ack.op = OpCode::BOOTREPLY;
ack.xid = req.xid;
ack.chaddr = req.chaddr;
ack.options.push(DhcpOption::ServerIdentifier(
server.get_server_ip(&req).unwrap_or(Ipv4Addr::UNSPECIFIED),
));
ack.options.push(DhcpOption::IpAddressLeaseTime(100));
ack.options.push(DhcpOption::RenewalTimeValue(50));
ack.options.push(DhcpOption::RebindingTimeValue(75));
ack.options.push(DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)));
if let Some(routers) = match server.options_repo.get(&OptionCode::Router) {
Some(DhcpOption::Router(v)) => Some(v),
_ => None,
} {
ack.options.push(DhcpOption::Router(routers.clone()));
}
if let Some(servers) = match server.options_repo.get(&OptionCode::DomainNameServer) {
Some(DhcpOption::DomainNameServer(v)) => Some(v),
_ => None,
} {
ack.options.push(DhcpOption::DomainNameServer(servers.clone()));
}
ack.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPACK));
ack
}
fn new_test_nak(req: &Message, server: &Server, error: String) -> Message {
let mut nak = Message::new();
nak.op = OpCode::BOOTREPLY;
nak.xid = req.xid;
nak.chaddr = req.chaddr;
nak.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPNAK));
nak.options.push(DhcpOption::ServerIdentifier(
server.get_server_ip(&req).unwrap_or(Ipv4Addr::UNSPECIFIED),
));
nak.options.push(DhcpOption::Message(error));
nak
}
fn new_test_release() -> Message {
let mut release = Message::new();
release.xid = rand::thread_rng().gen();
release.chaddr = random_mac_generator();
release.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPRELEASE));
release
}
fn new_test_inform() -> Message {
let mut inform = Message::new();
inform.xid = rand::thread_rng().gen();
inform.chaddr = random_mac_generator();
inform.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPINFORM));
inform
}
fn new_test_inform_ack(req: &Message, server: &Server) -> Message {
let mut ack = Message::new();
ack.op = OpCode::BOOTREPLY;
ack.xid = req.xid;
ack.chaddr = req.chaddr;
ack.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPACK));
ack.options.push(DhcpOption::ServerIdentifier(
server.get_server_ip(&req).unwrap_or(Ipv4Addr::UNSPECIFIED),
));
ack
}
fn new_test_decline(server: &Server) -> Message {
let mut decline = Message::new();
decline.xid = rand::thread_rng().gen();
decline.chaddr = random_mac_generator();
decline.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPDECLINE));
decline.options.push(DhcpOption::ServerIdentifier(
server.get_server_ip(&decline).unwrap_or(Ipv4Addr::UNSPECIFIED),
));
decline
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_returns_correct_offer_and_dest_giaddr_when_giaddr_set(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
disc.giaddr = random_ipv4_generator();
let offer_ip = random_ipv4_generator();
server.pool.available_addrs.insert(offer_ip);
let mut expected_offer = new_test_offer(&disc, &server);
expected_offer.yiaddr = offer_ip;
expected_offer.giaddr = disc.giaddr;
let expected_dest = disc.giaddr;
assert_eq!(
server.dispatch(disc),
Ok(ServerAction::SendResponse(expected_offer, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_returns_correct_offer_and_dest_ciaddr_when_giaddr_unspecified(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
disc.ciaddr = random_ipv4_generator();
let offer_ip = random_ipv4_generator();
server.pool.available_addrs.insert(offer_ip);
let mut expected_offer = new_test_offer(&disc, &server);
expected_offer.yiaddr = offer_ip;
let expected_dest = disc.ciaddr;
assert_eq!(
server.dispatch(disc),
Ok(ServerAction::SendResponse(expected_offer, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_broadcast_bit_set_returns_correct_offer_and_dest_broadcast_when_giaddr_and_ciaddr_unspecified(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
disc.bdcast_flag = true;
let offer_ip = random_ipv4_generator();
server.pool.available_addrs.insert(offer_ip);
let mut expected_offer = new_test_offer(&disc, &server);
expected_offer.yiaddr = offer_ip;
expected_offer.bdcast_flag = true;
assert_eq!(
server.dispatch(disc),
Ok(ServerAction::SendResponse(expected_offer, Some(Ipv4Addr::BROADCAST)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_returns_correct_offer_and_dest_broadcast_when_giaddr_and_ciaddr_unspecified_and_broadcast_bit_unset(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let disc = new_test_discover();
let offer_ip = random_ipv4_generator();
server.pool.available_addrs.insert(offer_ip);
let mut expected_offer = new_test_offer(&disc, &server);
expected_offer.yiaddr = offer_ip;
// TODO(fxbug.dev/35087): Instead of returning BROADCAST address, server must update ARP table.
assert_eq!(
server.dispatch(disc),
Ok(ServerAction::SendResponse(expected_offer, Some(Ipv4Addr::BROADCAST)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_returns_correct_offer_and_dest_giaddr_if_giaddr_ciaddr_broadcast_bit_is_set(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
disc.giaddr = random_ipv4_generator();
disc.ciaddr = random_ipv4_generator();
disc.bdcast_flag = true;
let offer_ip = random_ipv4_generator();
server.pool.available_addrs.insert(offer_ip);
let mut expected_offer = new_test_offer(&disc, &server);
expected_offer.yiaddr = offer_ip;
expected_offer.giaddr = disc.giaddr;
expected_offer.bdcast_flag = true;
let expected_dest = disc.giaddr;
assert_eq!(
server.dispatch(disc),
Ok(ServerAction::SendResponse(expected_offer, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_returns_correct_offer_and_dest_ciaddr_if_ciaddr_broadcast_bit_is_set(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
disc.ciaddr = random_ipv4_generator();
disc.bdcast_flag = true;
let offer_ip = random_ipv4_generator();
server.pool.available_addrs.insert(offer_ip);
let mut expected_offer = new_test_offer(&disc, &server);
expected_offer.yiaddr = offer_ip;
expected_offer.bdcast_flag = true;
let expected_dest = disc.ciaddr;
assert_eq!(
server.dispatch(disc),
Ok(ServerAction::SendResponse(expected_offer, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_updates_server_state() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let disc = new_test_discover();
let offer_ip = random_ipv4_generator();
let client_mac = disc.chaddr;
server.pool.available_addrs.insert(offer_ip);
let server_id = server.params.server_ips.first().unwrap();
let router = match server.options_repo.get(&OptionCode::Router) {
Some(DhcpOption::Router(router)) => Some(router.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::Router))?;
let dns_server = match server.options_repo.get(&OptionCode::DomainNameServer) {
Some(DhcpOption::DomainNameServer(dns_server)) => Some(dns_server.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::DomainNameServer))?;
let expected_client_config = CachedConfig::new(
offer_ip,
vec![
DhcpOption::ServerIdentifier(*server_id),
DhcpOption::IpAddressLeaseTime(server.params.lease_length.default_seconds),
DhcpOption::RenewalTimeValue(server.params.lease_length.default_seconds / 2),
DhcpOption::RebindingTimeValue(
(server.params.lease_length.default_seconds * 3) / 4,
),
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::Router(router),
DhcpOption::DomainNameServer(dns_server),
],
std::time::SystemTime::now(),
server.params.lease_length.default_seconds,
)?;
let _response = server.dispatch(disc);
assert_eq!(server.pool.available_addrs.len(), 0);
assert_eq!(server.pool.allocated_addrs.len(), 1);
assert_eq!(server.cache.len(), 1);
assert_eq!(server.cache.get(&client_mac), Some(&expected_client_config));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_updates_stash() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let disc = new_test_discover();
let offer_ip = random_ipv4_generator();
let client_mac = disc.chaddr;
server.pool.available_addrs.insert(offer_ip);
let offer = extract_message(server.dispatch(disc).unwrap());
let accessor = server.stash.clone_proxy();
let value = accessor
.get_value(&format!("{}-{}", DEFAULT_STASH_PREFIX, client_mac))
.await
.context("failed to get value from stash")?;
let value = value.ok_or(anyhow::format_err!("value not contained in stash"))?;
let serialized_config = match value.as_ref() {
fidl_fuchsia_stash::Value::Stringval(s) => Ok(s),
val => Err(anyhow::format_err!("unexpected value in stash: {:?}", val)),
}?;
let deserialized_config = serde_json::from_str::<CachedConfig>(serialized_config)
.context("failed to deserialize config")?;
assert_eq!(deserialized_config.client_addr, offer.yiaddr);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_client_binding_returns_bound_addr() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let disc = new_test_discover();
let bound_client_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(bound_client_ip);
server.cache.insert(
disc.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
let response = server.dispatch(disc).unwrap();
assert_eq!(extract_message(response).yiaddr, bound_client_ip);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
#[should_panic(expected = "tried to release unallocated ip")]
async fn test_dispatch_with_discover_client_binding_panics_when_addr_previously_not_allocated()
{
let mut server = new_test_minimal_server().await.unwrap();
let disc = new_test_discover();
let bound_client_ip = random_ipv4_generator();
server.cache.insert(
disc.chaddr,
CachedConfig::new(bound_client_ip, vec![], std::time::SystemTime::now(), std::u32::MAX)
.unwrap(),
);
let _ = server.dispatch(disc);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_expired_client_binding_returns_available_old_addr(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let disc = new_test_discover();
let bound_client_ip = random_ipv4_generator();
server.pool.available_addrs.insert(bound_client_ip);
server.cache.insert(
disc.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MIN,
)?,
);
let response = server.dispatch(disc).unwrap();
assert_eq!(extract_message(response).yiaddr, bound_client_ip);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_expired_client_binding_unavailable_addr_returns_next_free_addr(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let disc = new_test_discover();
let bound_client_ip = random_ipv4_generator();
let free_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(bound_client_ip);
server.pool.available_addrs.insert(free_ip);
server.cache.insert(
disc.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MIN,
)?,
);
let response = server.dispatch(disc).unwrap();
assert_eq!(extract_message(response).yiaddr, free_ip);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_expired_client_binding_returns_available_requested_addr(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
let bound_client_ip = random_ipv4_generator();
let requested_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(bound_client_ip);
server.pool.available_addrs.insert(requested_ip);
disc.options.push(DhcpOption::RequestedIpAddress(requested_ip));
server.cache.insert(
disc.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MIN,
)?,
);
let response = server.dispatch(disc).unwrap();
assert_eq!(extract_message(response).yiaddr, requested_ip);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_expired_client_binding_returns_next_addr_for_unavailable_requested_addr(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
let bound_client_ip = random_ipv4_generator();
let requested_ip = random_ipv4_generator();
let free_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(bound_client_ip);
server.pool.allocated_addrs.insert(requested_ip);
server.pool.available_addrs.insert(free_ip);
disc.options.push(DhcpOption::RequestedIpAddress(requested_ip));
server.cache.insert(
disc.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MIN,
)?,
);
let response = server.dispatch(disc).unwrap();
assert_eq!(extract_message(response).yiaddr, free_ip);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_available_requested_addr_returns_requested_addr(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
let requested_ip = random_ipv4_generator();
let free_ip_1 = random_ipv4_generator();
let free_ip_2 = random_ipv4_generator();
server.pool.available_addrs.insert(free_ip_1);
server.pool.available_addrs.insert(requested_ip);
server.pool.available_addrs.insert(free_ip_2);
// Update discover message to request for a specific ip
// which is available in server pool.
disc.options.push(DhcpOption::RequestedIpAddress(requested_ip));
let response = server.dispatch(disc).unwrap();
assert_eq!(extract_message(response).yiaddr, requested_ip);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_unavailable_requested_addr_returns_next_free_addr(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
let requested_ip = random_ipv4_generator();
let free_ip_1 = random_ipv4_generator();
server.pool.allocated_addrs.insert(requested_ip);
server.pool.available_addrs.insert(free_ip_1);
disc.options.push(DhcpOption::RequestedIpAddress(requested_ip));
let response = server.dispatch(disc).unwrap();
assert_eq!(extract_message(response).yiaddr, free_ip_1);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_unavailable_requested_addr_no_available_addr_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut disc = new_test_discover();
let requested_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(requested_ip);
disc.options.push(DhcpOption::RequestedIpAddress(requested_ip));
assert_eq!(
server.dispatch(disc),
Err(ServerError::ServerAddressPoolFailure(AddressPoolError::Ipv4AddrExhaustion))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_discover_no_requested_addr_no_available_addr_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let disc = new_test_discover();
server.pool.available_addrs.clear();
assert_eq!(
server.dispatch(disc),
Err(ServerError::ServerAddressPoolFailure(AddressPoolError::Ipv4AddrExhaustion))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_client_offer_message_returns_error() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
// Construct a simple offer sent by client.
let mut client_offer = Message::new();
client_offer.op = OpCode::BOOTREQUEST;
client_offer.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPOFFER));
assert_eq!(
server.dispatch(client_offer),
Err(ServerError::UnexpectedClientMessageType(MessageType::DHCPOFFER))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_client_ack_message_returns_error() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
// Construct a simple ack sent by client.
let mut client_ack = Message::new();
client_ack.op = OpCode::BOOTREQUEST;
client_ack.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPACK));
assert_eq!(
server.dispatch(client_ack),
Err(ServerError::UnexpectedClientMessageType(MessageType::DHCPACK))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_client_nak_message_returns_error() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
// Construct a simple nak sent by client.
let mut client_nak = Message::new();
client_nak.op = OpCode::BOOTREQUEST;
client_nak.options.push(DhcpOption::DhcpMessageType(MessageType::DHCPNAK));
assert_eq!(
server.dispatch(client_nak),
Err(ServerError::UnexpectedClientMessageType(MessageType::DHCPNAK))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_selecting_request_returns_correct_ack() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request_selecting_state(&server);
let requested_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(requested_ip);
// Update message to request for ip previously offered by server.
req.ciaddr = requested_ip;
let server_id = server.params.server_ips.first().unwrap();
let router = match server.options_repo.get(&OptionCode::Router) {
Some(DhcpOption::Router(router)) => Some(router.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::Router))?;
let dns_server = match server.options_repo.get(&OptionCode::DomainNameServer) {
Some(DhcpOption::DomainNameServer(dns_server)) => Some(dns_server.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::DomainNameServer))?;
server.cache.insert(
req.chaddr,
CachedConfig::new(
requested_ip,
vec![
DhcpOption::ServerIdentifier(*server_id),
DhcpOption::IpAddressLeaseTime(server.params.lease_length.default_seconds),
DhcpOption::RenewalTimeValue(server.params.lease_length.default_seconds / 2),
DhcpOption::RebindingTimeValue(
(server.params.lease_length.default_seconds * 3) / 4,
),
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::Router(router),
DhcpOption::DomainNameServer(dns_server),
],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
let mut expected_ack = new_test_ack(&req, &server);
expected_ack.ciaddr = requested_ip;
expected_ack.yiaddr = requested_ip;
let expected_dest = req.ciaddr;
assert_eq!(
server.dispatch(req),
Ok(ServerAction::SendResponse(expected_ack, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_selecting_request_maintains_server_invariants() -> Result<(), Error>
{
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request_selecting_state(&server);
let requested_ip = random_ipv4_generator();
let client_mac = req.chaddr;
server.pool.allocated_addrs.insert(requested_ip);
req.ciaddr = requested_ip;
server.cache.insert(
client_mac,
CachedConfig::new(requested_ip, vec![], std::time::SystemTime::now(), std::u32::MAX)?,
);
let _response = server.dispatch(req).unwrap();
assert!(server.cache.contains_key(&client_mac));
assert!(server.pool.addr_is_allocated(requested_ip));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_selecting_request_wrong_server_ip_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request_selecting_state(&server);
// Update message to request for any ip.
req.ciaddr = random_ipv4_generator();
// Update request to have a server ip different from actual server ip.
req.options.remove(req.options.len() - 1);
req.options.push(DhcpOption::ServerIdentifier(random_ipv4_generator()));
let server_ip =
*server.params.server_ips.first().ok_or(ServerError::ServerMissingIpAddr)?;
assert_eq!(server.dispatch(req), Err(ServerError::IncorrectDHCPServer(server_ip)));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_selecting_request_unknown_client_mac_returns_error_maintains_server_invariants(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request_selecting_state(&server);
let requested_ip = random_ipv4_generator();
let client_mac = req.chaddr;
req.ciaddr = requested_ip;
assert_eq!(server.dispatch(req), Err(ServerError::UnknownClientMac(client_mac)));
assert!(!server.cache.contains_key(&client_mac));
assert!(!server.pool.addr_is_allocated(requested_ip));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_selecting_request_mismatched_requested_addr_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request_selecting_state(&server);
let client_requested_ip = random_ipv4_generator();
let server_offered_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(server_offered_ip);
req.ciaddr = client_requested_ip;
server.cache.insert(
req.chaddr,
CachedConfig::new(
server_offered_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
assert_eq!(
server.dispatch(req),
Err(ServerError::RequestedIpOfferIpMismatch(client_requested_ip, server_offered_ip,),)
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_selecting_request_expired_client_binding_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request_selecting_state(&server);
let requested_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(requested_ip);
req.ciaddr = requested_ip;
server.cache.insert(
req.chaddr,
CachedConfig::new(requested_ip, vec![], std::time::SystemTime::now(), std::u32::MIN)?,
);
assert_eq!(server.dispatch(req), Err(ServerError::ExpiredClientConfig));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_selecting_request_no_reserved_addr_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request_selecting_state(&server);
let requested_ip = random_ipv4_generator();
req.ciaddr = requested_ip;
server.cache.insert(
req.chaddr,
CachedConfig::new(requested_ip, vec![], std::time::SystemTime::now(), std::u32::MAX)?,
);
assert_eq!(server.dispatch(req), Err(ServerError::UnidentifiedRequestedIp(requested_ip)));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_init_boot_request_returns_correct_ack() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
// For init-reboot, server and requested ip must be on the same subnet.
// Hard-coding ip values here to achieve that.
let init_reboot_client_ip = Ipv4Addr::new(192, 168, 1, 60);
server.params.server_ips = vec![Ipv4Addr::new(192, 168, 1, 1)];
server.pool.allocated_addrs.insert(init_reboot_client_ip);
// Update request to have the test requested ip.
req.options.push(DhcpOption::RequestedIpAddress(init_reboot_client_ip));
let server_id = server.params.server_ips.first().unwrap();
let router = match server.options_repo.get(&OptionCode::Router) {
Some(DhcpOption::Router(router)) => Some(router.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::Router))?;
let dns_server = match server.options_repo.get(&OptionCode::DomainNameServer) {
Some(DhcpOption::DomainNameServer(dns_server)) => Some(dns_server.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::DomainNameServer))?;
server.cache.insert(
req.chaddr,
CachedConfig::new(
init_reboot_client_ip,
vec![
DhcpOption::ServerIdentifier(*server_id),
DhcpOption::IpAddressLeaseTime(server.params.lease_length.default_seconds),
DhcpOption::RenewalTimeValue(server.params.lease_length.default_seconds / 2),
DhcpOption::RebindingTimeValue(
(server.params.lease_length.default_seconds * 3) / 4,
),
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::Router(router),
DhcpOption::DomainNameServer(dns_server),
],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
let mut expected_ack = new_test_ack(&req, &server);
expected_ack.yiaddr = init_reboot_client_ip;
let expected_dest = req.yiaddr;
assert_eq!(
server.dispatch(req),
Ok(ServerAction::SendResponse(expected_ack, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_init_boot_request_client_on_wrong_subnet_returns_nak(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
// Update request to have requested ip not on same subnet as server.
req.options.push(DhcpOption::RequestedIpAddress(random_ipv4_generator()));
// The returned nak should be from this recipient server.
let expected_nak =
new_test_nak(&req, &server, "client and server are in different subnets".to_owned());
assert_eq!(
server.dispatch(req),
Ok(ServerAction::SendResponse(expected_nak, Some(Ipv4Addr::BROADCAST)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_init_boot_request_with_giaddr_set_returns_nak_with_broadcast_bit_set(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
req.giaddr = random_ipv4_generator();
// Update request to have requested ip not on same subnet as server,
// to ensure we get a nak.
req.options.push(DhcpOption::RequestedIpAddress(random_ipv4_generator()));
let response = server.dispatch(req).unwrap();
assert!(extract_message(response).bdcast_flag);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_init_boot_request_unknown_client_mac_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let client_mac = req.chaddr;
// Update requested ip and server ip to be on the same subnet.
req.options.push(DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 165, 30, 45)));
server.params.server_ips = vec![Ipv4Addr::new(192, 165, 30, 1)];
assert_eq!(server.dispatch(req), Err(ServerError::UnknownClientMac(client_mac)));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_init_boot_request_mismatched_requested_addr_returns_nak(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
// Update requested ip and server ip to be on the same subnet.
let init_reboot_client_ip = Ipv4Addr::new(192, 165, 25, 4);
req.options.push(DhcpOption::RequestedIpAddress(init_reboot_client_ip));
server.params.server_ips = vec![Ipv4Addr::new(192, 165, 25, 1)];
let server_cached_ip = Ipv4Addr::new(192, 165, 25, 10);
server.pool.allocated_addrs.insert(server_cached_ip);
server.cache.insert(
req.chaddr,
CachedConfig::new(
server_cached_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
let expected_nak =
new_test_nak(&req, &server, "requested ip is not assigned to client".to_owned());
assert_eq!(
server.dispatch(req),
Ok(ServerAction::SendResponse(expected_nak, Some(Ipv4Addr::BROADCAST)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_init_boot_request_expired_client_binding_returns_nak(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let init_reboot_client_ip = Ipv4Addr::new(192, 165, 25, 4);
req.options.push(DhcpOption::RequestedIpAddress(init_reboot_client_ip));
server.params.server_ips = vec![Ipv4Addr::new(192, 165, 25, 1)];
server.pool.allocated_addrs.insert(init_reboot_client_ip);
// Expire client binding to make it invalid.
server.cache.insert(
req.chaddr,
CachedConfig::new(
init_reboot_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MIN,
)?,
);
let expected_nak =
new_test_nak(&req, &server, "requested ip is not assigned to client".to_owned());
assert_eq!(
server.dispatch(req),
Ok(ServerAction::SendResponse(expected_nak, Some(Ipv4Addr::BROADCAST)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_init_boot_request_no_reserved_addr_returns_nak() -> Result<(), Error>
{
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let init_reboot_client_ip = Ipv4Addr::new(192, 165, 25, 4);
req.options.push(DhcpOption::RequestedIpAddress(init_reboot_client_ip));
server.params.server_ips = vec![Ipv4Addr::new(192, 165, 25, 1)];
server.cache.insert(
req.chaddr,
CachedConfig::new(
init_reboot_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
let expected_nak =
new_test_nak(&req, &server, "requested ip is not assigned to client".to_owned());
assert_eq!(
server.dispatch(req),
Ok(ServerAction::SendResponse(expected_nak, Some(Ipv4Addr::BROADCAST)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_renewing_request_returns_correct_ack() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let bound_client_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(bound_client_ip);
req.ciaddr = bound_client_ip;
let server_id = server.params.server_ips.first().unwrap();
let router = match server.options_repo.get(&OptionCode::Router) {
Some(DhcpOption::Router(router)) => Some(router.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::Router))?;
let dns_server = match server.options_repo.get(&OptionCode::DomainNameServer) {
Some(DhcpOption::DomainNameServer(dns_server)) => Some(dns_server.clone()),
_ => None,
}
.ok_or(ProtocolError::MissingOption(OptionCode::DomainNameServer))?;
server.cache.insert(
req.chaddr,
CachedConfig::new(
bound_client_ip,
vec![
DhcpOption::ServerIdentifier(*server_id),
DhcpOption::IpAddressLeaseTime(server.params.lease_length.default_seconds),
DhcpOption::RenewalTimeValue(server.params.lease_length.default_seconds / 2),
DhcpOption::RebindingTimeValue(
(server.params.lease_length.default_seconds * 3) / 4,
),
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::Router(router),
DhcpOption::DomainNameServer(dns_server),
],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
let mut expected_ack = new_test_ack(&req, &server);
expected_ack.yiaddr = bound_client_ip;
expected_ack.ciaddr = bound_client_ip;
let expected_dest = req.ciaddr;
assert_eq!(
server.dispatch(req),
Ok(ServerAction::SendResponse(expected_ack, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_renewing_request_unknown_client_mac_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let bound_client_ip = random_ipv4_generator();
let client_mac = req.chaddr;
req.ciaddr = bound_client_ip;
assert_eq!(server.dispatch(req), Err(ServerError::UnknownClientMac(client_mac)));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_renewing_request_mismatched_requested_addr_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let client_renewal_ip = random_ipv4_generator();
let bound_client_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(bound_client_ip);
req.ciaddr = client_renewal_ip;
server.cache.insert(
req.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
assert_eq!(
server.dispatch(req),
Err(ServerError::RequestedIpOfferIpMismatch(client_renewal_ip, bound_client_ip))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_renewing_request_expired_client_binding_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let bound_client_ip = random_ipv4_generator();
server.pool.allocated_addrs.insert(bound_client_ip);
req.ciaddr = bound_client_ip;
server.cache.insert(
req.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MIN,
)?,
);
assert_eq!(server.dispatch(req), Err(ServerError::ExpiredClientConfig));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_renewing_request_no_reserved_addr_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut req = new_test_request();
let bound_client_ip = random_ipv4_generator();
req.ciaddr = bound_client_ip;
server.cache.insert(
req.chaddr,
CachedConfig::new(
bound_client_ip,
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
assert_eq!(
server.dispatch(req),
Err(ServerError::UnidentifiedRequestedIp(bound_client_ip))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_unknown_client_state_returns_error() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let req = new_test_request();
assert_eq!(server.dispatch(req), Err(ServerError::UnknownClientStateDuringRequest));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_client_state_with_selecting_returns_selecting() -> Result<(), Error> {
let mut req = new_test_request();
// Selecting state request must have server id and ciaddr populated.
req.ciaddr = random_ipv4_generator();
req.options.push(DhcpOption::ServerIdentifier(random_ipv4_generator()));
assert_eq!(get_client_state(&req), Ok(ClientState::Selecting));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_client_state_with_initreboot_returns_initreboot() -> Result<(), Error> {
let mut req = new_test_request();
// Init reboot state request must have requested ip populated.
req.options.push(DhcpOption::RequestedIpAddress(random_ipv4_generator()));
assert_eq!(get_client_state(&req), Ok(ClientState::InitReboot));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_client_state_with_renewing_returns_renewing() -> Result<(), Error> {
let mut req = new_test_request();
// Renewing state request must have ciaddr populated.
req.ciaddr = random_ipv4_generator();
assert_eq!(get_client_state(&req), Ok(ClientState::Renewing));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_get_client_state_with_unknown_returns_unknown() -> Result<(), Error> {
let msg = new_test_request();
assert_eq!(get_client_state(&msg), Err(()));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_client_msg_missing_message_type_option_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut msg = new_test_request();
msg.options.clear();
assert_eq!(
server.dispatch(msg),
Err(ServerError::ClientMessageError(ProtocolError::MissingOption(
OptionCode::DhcpMessageType
)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_release_expired_leases_with_none_expired_releases_none() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
server.pool.available_addrs.clear();
// Insert client 1 bindings.
let client_1_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_1_ip);
server.store_client_config(
client_1_ip,
random_mac_generator(),
&[DhcpOption::IpAddressLeaseTime(std::u32::MAX)],
)?;
// Insert client 2 bindings.
let client_2_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_2_ip);
server.store_client_config(
client_2_ip,
random_mac_generator(),
&[DhcpOption::IpAddressLeaseTime(std::u32::MAX)],
)?;
// Insert client 3 bindings.
let client_3_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_3_ip);
server.store_client_config(
client_3_ip,
random_mac_generator(),
&[DhcpOption::IpAddressLeaseTime(std::u32::MAX)],
)?;
let () = server.release_expired_leases()?;
assert_eq!(server.cache.len(), 3);
assert_eq!(server.pool.available_addrs.len(), 0);
assert_eq!(server.pool.allocated_addrs.len(), 3);
let keys = get_keys(&mut server).await.context("failed to get keys")?;
assert_eq!(keys.len(), 3);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_release_expired_leases_with_all_expired_releases_all() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
server.pool.available_addrs.clear();
let client_1_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_1_ip);
let () = server.store_client_config(
client_1_ip,
random_mac_generator(),
&[DhcpOption::IpAddressLeaseTime(0)],
)?;
let client_2_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_2_ip);
let () = server.store_client_config(
client_2_ip,
random_mac_generator(),
&[DhcpOption::IpAddressLeaseTime(0)],
)?;
let client_3_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_3_ip);
let () = server.store_client_config(
client_3_ip,
random_mac_generator(),
&[DhcpOption::IpAddressLeaseTime(0)],
)?;
let () = server.release_expired_leases()?;
assert_eq!(server.cache.len(), 0);
assert_eq!(server.pool.available_addrs.len(), 3);
assert_eq!(server.pool.allocated_addrs.len(), 0);
let keys = get_keys(&mut server).await.context("failed to get keys")?;
assert_eq!(keys.len(), 0);
Ok(())
}
async fn get_keys(server: &mut Server) -> Result<Vec<fidl_fuchsia_stash::KeyValue>, Error> {
let accessor = server.stash.clone_proxy();
let (iter, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_stash::GetIteratorMarker>()
.context("failed to create iterator")?;
let () = accessor
.get_prefix(&format!("{}", DEFAULT_STASH_PREFIX), server_end)
.context("failed to get prefix")?;
let keys = iter.get_next().await.context("failed to get next")?;
Ok(keys)
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_release_expired_leases_with_some_expired_releases_expired() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
server.pool.available_addrs.clear();
let client_1_mac = random_mac_generator();
let client_1_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_1_ip);
let () = server.store_client_config(
client_1_ip,
client_1_mac,
&[DhcpOption::IpAddressLeaseTime(std::u32::MAX)],
)?;
let client_2_mac = random_mac_generator();
let client_2_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_2_ip);
let () = server.store_client_config(
client_2_ip,
client_2_mac,
&[DhcpOption::IpAddressLeaseTime(0)],
)?;
let client_3_mac = random_mac_generator();
let client_3_ip = random_ipv4_generator();
server.pool.available_addrs.insert(client_3_ip);
let () = server.store_client_config(
client_3_ip,
client_3_mac,
&[DhcpOption::IpAddressLeaseTime(std::u32::MAX)],
)?;
let () = server.release_expired_leases()?;
assert_eq!(server.cache.len(), 2);
assert!(!server.cache.contains_key(&client_2_mac));
assert_eq!(server.pool.available_addrs.len(), 1);
assert_eq!(server.pool.allocated_addrs.len(), 2);
let keys = get_keys(&mut server).await.context("failed to get keys")?;
assert_eq!(keys.len(), 2);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_known_release_updates_address_pool_retains_client_config(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut release = new_test_release();
let release_ip = random_ipv4_generator();
let client_mac = release.chaddr;
server.pool.allocated_addrs.insert(release_ip);
release.ciaddr = release_ip;
let test_client_config = || {
CachedConfig::new(release_ip, vec![], std::time::SystemTime::now(), std::u32::MAX)
.unwrap()
};
server.cache.insert(client_mac, test_client_config());
assert_eq!(server.dispatch(release), Ok(ServerAction::AddressRelease(release_ip)));
assert!(!server.pool.addr_is_allocated(release_ip), "addr marked allocated");
assert!(server.pool.addr_is_available(release_ip), "addr not marked available");
assert!(server.cache.contains_key(&client_mac), "client config not retained");
assert_eq!(
server.cache.get(&client_mac).unwrap(),
&test_client_config(),
"retained client config changed"
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_unknown_release_maintains_server_state_returns_unknown_mac_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut release = new_test_release();
let release_ip = random_ipv4_generator();
let client_mac = release.chaddr;
server.pool.allocated_addrs.insert(release_ip);
release.ciaddr = release_ip;
assert_eq!(server.dispatch(release), Err(ServerError::UnknownClientMac(client_mac,)));
assert!(server.pool.addr_is_allocated(release_ip), "addr not marked allocated");
assert!(!server.pool.addr_is_available(release_ip), "addr still marked available");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_inform_returns_correct_ack() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut inform = new_test_inform();
let inform_client_ip = random_ipv4_generator();
inform.ciaddr = inform_client_ip;
let mut expected_ack = new_test_inform_ack(&inform, &server);
expected_ack.ciaddr = inform_client_ip;
let expected_dest = inform.ciaddr;
assert_eq!(
server.dispatch(inform),
Ok(ServerAction::SendResponse(expected_ack, Some(expected_dest)))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_decline_for_valid_client_binding_updates_cache() -> Result<(), Error>
{
let mut server = new_test_minimal_server().await?;
let mut decline = new_test_decline(&server);
let declined_ip = random_ipv4_generator();
let client_mac = decline.chaddr;
decline.options.push(DhcpOption::RequestedIpAddress(declined_ip));
server.pool.allocated_addrs.insert(declined_ip);
server.cache.insert(
client_mac,
CachedConfig::new(declined_ip, vec![], std::time::SystemTime::now(), std::u32::MAX)?,
);
assert_eq!(server.dispatch(decline), Ok(ServerAction::AddressDecline(declined_ip)));
assert!(!server.pool.addr_is_available(declined_ip), "addr still marked available");
assert!(server.pool.addr_is_allocated(declined_ip), "addr not marked allocated");
assert!(!server.cache.contains_key(&client_mac), "client config incorrectly retained");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_decline_for_invalid_client_binding_updates_pool_and_cache(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut decline = new_test_decline(&server);
let declined_ip = random_ipv4_generator();
let client_mac = decline.chaddr;
decline.options.push(DhcpOption::RequestedIpAddress(declined_ip));
// Even though declined client ip does not match client binding,
// the server must update its address pool and mark declined ip as
// allocated, and delete client bindings from its cache.
let client_ip_according_to_server = random_ipv4_generator();
server.pool.allocated_addrs.insert(client_ip_according_to_server);
server.pool.available_addrs.insert(declined_ip);
// Server contains client bindings which reflect a different address
// than the one being declined.
server.cache.insert(
client_mac,
CachedConfig::new(
client_ip_according_to_server,
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
assert_eq!(server.dispatch(decline), Ok(ServerAction::AddressDecline(declined_ip)));
assert!(!server.pool.addr_is_available(declined_ip), "addr still marked available");
assert!(server.pool.addr_is_allocated(declined_ip), "addr not marked allocated");
assert!(!server.cache.contains_key(&client_mac), "client config incorrectly retained");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_decline_for_expired_client_binding_updates_pool_and_cache(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut decline = new_test_decline(&server);
let declined_ip = random_ipv4_generator();
let client_mac = decline.chaddr;
decline.options.push(DhcpOption::RequestedIpAddress(declined_ip));
server.pool.available_addrs.insert(declined_ip);
server.cache.insert(
client_mac,
CachedConfig::new(declined_ip, vec![], std::time::SystemTime::now(), std::u32::MIN)?,
);
assert_eq!(server.dispatch(decline), Ok(ServerAction::AddressDecline(declined_ip)));
assert!(!server.pool.addr_is_available(declined_ip), "addr still marked available");
assert!(server.pool.addr_is_allocated(declined_ip), "addr not marked allocated");
assert!(!server.cache.contains_key(&client_mac), "client config incorrectly retained");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_decline_known_client_for_address_not_in_server_pool_returns_error(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut decline = new_test_decline(&server);
let declined_ip = random_ipv4_generator();
let client_mac = decline.chaddr;
decline.options.push(DhcpOption::RequestedIpAddress(declined_ip));
// Server contains client bindings which reflect a different address
// than the one being declined.
server.cache.insert(
client_mac,
CachedConfig::new(
random_ipv4_generator(),
vec![],
std::time::SystemTime::now(),
std::u32::MAX,
)?,
);
assert_eq!(
server.dispatch(decline),
Err(ServerError::ServerAddressPoolFailure(
AddressPoolError::UnavailableIpv4AddrAllocation(declined_ip)
))
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_decline_for_unknown_client_updates_pool() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mut decline = new_test_decline(&server);
let declined_ip = random_ipv4_generator();
decline.options.push(DhcpOption::RequestedIpAddress(declined_ip));
server.pool.available_addrs.insert(declined_ip);
assert_eq!(server.dispatch(decline), Ok(ServerAction::AddressDecline(declined_ip)));
assert!(!server.pool.addr_is_available(declined_ip), "addr still marked available");
assert!(server.pool.addr_is_allocated(declined_ip), "addr not marked allocated");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
// TODO(fxbug.dev/21422): Revisit when decline behavior is verified.
async fn test_dispatch_with_decline_for_incorrect_server_recepient_deletes_client_binding(
) -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
server.params.server_ips = vec![Ipv4Addr::new(192, 168, 1, 1)];
let mut decline = new_test_decline(&server);
// Updating decline request to have wrong server ip.
decline.options.remove(1);
decline.options.push(DhcpOption::ServerIdentifier(Ipv4Addr::new(1, 2, 3, 4)));
let declined_ip = random_ipv4_generator();
let client_mac = decline.chaddr;
decline.options.push(DhcpOption::RequestedIpAddress(declined_ip));
server.pool.allocated_addrs.insert(declined_ip);
server.cache.insert(
client_mac,
CachedConfig::new(declined_ip, vec![], std::time::SystemTime::now(), std::u32::MAX)?,
);
assert_eq!(server.dispatch(decline), Ok(ServerAction::AddressDecline(declined_ip)));
assert!(!server.pool.addr_is_available(declined_ip), "addr still marked available");
assert!(server.pool.addr_is_allocated(declined_ip), "addr not marked allocated");
assert!(!server.cache.contains_key(&client_mac), "client config incorrectly retained");
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_dispatch_with_decline_without_requested_addr_returns_error() -> Result<(), Error>
{
let mut server = new_test_minimal_server().await?;
let decline = new_test_decline(&server);
assert_eq!(server.dispatch(decline), Err(ServerError::NoRequestedAddrForDecline));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_client_requested_lease_time() -> Result<(), Error> {
let mut disc = new_test_discover();
let client_mac = disc.chaddr;
let client_requested_time: u32 = 20;
disc.options.push(DhcpOption::IpAddressLeaseTime(client_requested_time));
let mut server = new_test_minimal_server().await?;
server.pool.available_addrs.insert(random_ipv4_generator());
let response = server.dispatch(disc).unwrap();
assert_eq!(
extract_message(response)
.options
.iter()
.filter_map(|opt| {
if let DhcpOption::IpAddressLeaseTime(v) = opt {
Some(*v)
} else {
None
}
})
.next()
.unwrap(),
client_requested_time as u32
);
assert_eq!(
server.cache.get(&client_mac).unwrap().lease_length_seconds,
client_requested_time,
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_client_requested_lease_time_greater_than_max() -> Result<(), Error> {
let mut disc = new_test_discover();
let client_mac = disc.chaddr;
let client_requested_time: u32 = 20;
let server_max_lease_time: u32 = 10;
disc.options.push(DhcpOption::IpAddressLeaseTime(client_requested_time));
let mut server = new_test_minimal_server().await?;
server.pool.available_addrs.insert(Ipv4Addr::new(195, 168, 1, 45));
let ll = LeaseLength { default_seconds: 60 * 60 * 24, max_seconds: server_max_lease_time };
server.params.lease_length = ll;
let response = server.dispatch(disc).unwrap();
assert_eq!(
extract_message(response)
.options
.iter()
.filter_map(|opt| {
if let DhcpOption::IpAddressLeaseTime(v) = opt {
Some(*v)
} else {
None
}
})
.next()
.unwrap(),
server_max_lease_time
);
assert_eq!(
server.cache.get(&client_mac).unwrap().lease_length_seconds,
server_max_lease_time,
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_get_option_with_unset_option_returns_not_found(
) -> Result<(), Error> {
let server = new_test_minimal_server().await?;
let result = server.dispatch_get_option(fidl_fuchsia_net_dhcp::OptionCode::SubnetMask);
assert_eq!(result, Err(Status::NOT_FOUND));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_get_option_with_set_option_returns_option() -> Result<(), Error>
{
let mut server = new_test_minimal_server().await?;
let option = || {
fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_fuchsia_net::Ipv4Address {
addr: [255, 255, 255, 0],
})
};
server.options_repo.insert(OptionCode::SubnetMask, DhcpOption::try_from_fidl(option())?);
let result = server.dispatch_get_option(fidl_fuchsia_net_dhcp::OptionCode::SubnetMask)?;
assert_eq!(result, option());
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_get_parameter_returns_parameter() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let addr = random_ipv4_generator();
server.params.server_ips = vec![addr];
let expected = fidl_fuchsia_net_dhcp::Parameter::IpAddrs(vec![addr.into_fidl()]);
let result =
server.dispatch_get_parameter(fidl_fuchsia_net_dhcp::ParameterName::IpAddrs)?;
assert_eq!(result, expected);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_set_option_returns_unit() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let option = || {
fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_fuchsia_net::Ipv4Address {
addr: [255, 255, 255, 0],
})
};
let () = server.dispatch_set_option(option())?;
let stored_option: DhcpOption = DhcpOption::try_from_fidl(option())?;
let code = stored_option.code();
let result = server.options_repo.get(&code);
assert_eq!(result, Some(&stored_option));
Ok(())
}
enum StashSetArg {
Option(fidl_fuchsia_net_dhcp::Option_),
Parameter(fidl_fuchsia_net_dhcp::Parameter),
}
async fn test_dispatch_set_to_stash(arg: StashSetArg) -> Result<String, Error> {
let mut server = new_test_minimal_server().await?;
let key = match arg {
StashSetArg::Option(opt) => {
let () = server.dispatch_set_option(opt)?;
"options"
}
StashSetArg::Parameter(param) => {
let () = server.dispatch_set_parameter(param)?;
"parameters"
}
};
let proxy = server.stash.clone_proxy();
let raw_opts =
proxy.get_value(key).await?.ok_or(anyhow::anyhow!("failed to get value from stash"))?;
match *raw_opts {
fidl_fuchsia_stash::Value::Stringval(json) => Ok(json),
invalid_val => {
return Err(anyhow::anyhow!("invalid value found in stash: {:?}", invalid_val));
}
}
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_set_option_saves_to_stash() -> Result<(), Error> {
let mask = [255, 255, 255, 0];
let json = test_dispatch_set_to_stash(StashSetArg::Option(
fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_fuchsia_net::Ipv4Address {
addr: mask,
}),
))
.await?;
let opts: Vec<DhcpOption> = serde_json::from_str(&json)?;
assert!(opts.into_iter().any(|opt| opt == DhcpOption::SubnetMask(Ipv4Addr::from(mask))));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_set_parameter_saves_to_stash() -> Result<(), Error> {
let (default, max) = (42, 100);
let json = test_dispatch_set_to_stash(StashSetArg::Parameter(
fidl_fuchsia_net_dhcp::Parameter::Lease(fidl_fuchsia_net_dhcp::LeaseLength {
default: Some(default),
max: Some(max),
}),
))
.await?;
let params: ServerParameters = serde_json::from_str(&json)?;
assert_eq!(params.lease_length, LeaseLength { default_seconds: default, max_seconds: max });
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_set_parameter() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let addr = random_ipv4_generator();
let valid_parameter = || fidl_fuchsia_net_dhcp::Parameter::IpAddrs(vec![addr.into_fidl()]);
let empty_lease_length =
fidl_fuchsia_net_dhcp::Parameter::Lease(fidl_fuchsia_net_dhcp::LeaseLength {
default: None,
max: None,
});
let bad_mask =
fidl_fuchsia_net_dhcp::Parameter::AddressPool(fidl_fuchsia_net_dhcp::AddressPool {
network_id: Some(fidl_fuchsia_net::Ipv4Address { addr: [192, 168, 0, 0] }),
broadcast: Some(fidl_fuchsia_net::Ipv4Address { addr: [192, 168, 0, 255] }),
mask: Some(fidl_fuchsia_net::Ipv4Address { addr: [255, 255, 0, 255] }),
pool_range_start: Some(fidl_fuchsia_net::Ipv4Address { addr: [192, 168, 0, 2] }),
pool_range_stop: Some(fidl_fuchsia_net::Ipv4Address { addr: [192, 168, 0, 254] }),
});
let MacAddr { octets: mac } = random_mac_generator();
let duplicated_static_assignment =
fidl_fuchsia_net_dhcp::Parameter::StaticallyAssignedAddrs(vec![
fidl_fuchsia_net_dhcp::StaticAssignment {
host: Some(fidl_fuchsia_net::MacAddress { octets: mac.clone() }),
assigned_addr: Some(random_ipv4_generator().into_fidl()),
},
fidl_fuchsia_net_dhcp::StaticAssignment {
host: Some(fidl_fuchsia_net::MacAddress { octets: mac.clone() }),
assigned_addr: Some(random_ipv4_generator().into_fidl()),
},
]);
let () = server.dispatch_set_parameter(valid_parameter())?;
assert_eq!(
server.dispatch_get_parameter(fidl_fuchsia_net_dhcp::ParameterName::IpAddrs)?,
valid_parameter()
);
assert_eq!(
server.dispatch_set_parameter(empty_lease_length).unwrap_err(),
fuchsia_zircon::Status::INVALID_ARGS
);
assert_eq!(
server.dispatch_set_parameter(bad_mask).unwrap_err(),
fuchsia_zircon::Status::INVALID_ARGS
);
assert_eq!(
server.dispatch_set_parameter(duplicated_static_assignment).unwrap_err(),
fuchsia_zircon::Status::INVALID_ARGS
);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_list_options_returns_set_options() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let mask = || {
fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_fuchsia_net::Ipv4Address {
addr: [255, 255, 255, 0],
})
};
let hostname = || fidl_fuchsia_net_dhcp::Option_::HostName(String::from("testhostname"));
server.options_repo.insert(OptionCode::SubnetMask, DhcpOption::try_from_fidl(mask())?);
server.options_repo.insert(OptionCode::HostName, DhcpOption::try_from_fidl(hostname())?);
let result = server.dispatch_list_options()?;
assert_eq!(result.len(), server.options_repo.len());
assert!(result.contains(&mask()));
assert!(result.contains(&hostname()));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_list_parameters_returns_parameters() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let addr = random_ipv4_generator();
server.params.server_ips = vec![addr];
let expected = fidl_fuchsia_net_dhcp::Parameter::IpAddrs(vec![addr.into_fidl()]);
let result = server.dispatch_list_parameters()?;
let params_fields_ct = 7;
assert_eq!(result.len(), params_fields_ct);
assert!(result.contains(&expected));
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_reset_options() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let empty_map = HashMap::new();
assert_ne!(empty_map, server.options_repo);
let () = server.dispatch_reset_options()?;
assert_eq!(empty_map, server.options_repo);
let stored_opts = server.stash.load_options().await?;
assert_eq!(empty_map, stored_opts);
Ok(())
}
#[fuchsia_async::run_singlethreaded(test)]
async fn test_server_dispatcher_reset_parameters() -> Result<(), Error> {
let mut server = new_test_minimal_server().await?;
let default_params = ServerParameters {
server_ips: vec![Ipv4Addr::from([192, 168, 0, 1])],
lease_length: LeaseLength { default_seconds: 86400, max_seconds: 86400 },
managed_addrs: crate::configuration::ManagedAddresses {
network_id: Ipv4Addr::from([192, 168, 0, 0]),
broadcast: Ipv4Addr::from([192, 168, 0, 128]),
mask: crate::configuration::SubnetMask::try_from(25).unwrap(),
pool_range_start: Ipv4Addr::from([192, 168, 0, 0]),
pool_range_stop: Ipv4Addr::from([192, 168, 0, 0]),
},
permitted_macs: crate::configuration::PermittedMacs(vec![]),
static_assignments: crate::configuration::StaticAssignments(HashMap::new()),
arp_probe: false,
bound_device_names: vec![],
};
assert_ne!(default_params, server.params);
let () = server.dispatch_reset_parameters(&default_params)?;
assert_eq!(default_params, server.params);
let stored_params = server.stash.load_parameters().await?;
assert_eq!(default_params, stored_params);
Ok(())
}
}