| // 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::{ |
| identifier::ClientIdentifier, DhcpOption, FidlCompatible, FromFidlExt, IntoFidlExt, Message, |
| MessageType, OpCode, OptionCode, ProtocolError, |
| }; |
| use anyhow::{Context as _, Error}; |
| use fuchsia_zircon::Status; |
| use serde::{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<DS: DataStore, TS: SystemTimeSource = StdSystemTime> { |
| cache: CachedClients, |
| pool: AddressPool, |
| params: ServerParameters, |
| store: DS, |
| options_repo: HashMap<OptionCode, DhcpOption>, |
| time_source: TS, |
| } |
| |
| // An interface for Server to retrieve the current time. |
| pub trait SystemTimeSource { |
| fn with_current_time() -> Self; |
| fn now(&self) -> std::time::SystemTime; |
| } |
| |
| // SystemTimeSource that uses std::time::SystemTime::now(). |
| pub struct StdSystemTime; |
| |
| impl SystemTimeSource for StdSystemTime { |
| fn with_current_time() -> Self { |
| StdSystemTime |
| } |
| |
| fn now(&self) -> std::time::SystemTime { |
| std::time::SystemTime::now() |
| } |
| } |
| |
| /// An interface for storing and loading DHCP server data. |
| #[async_trait::async_trait] |
| pub trait DataStore { |
| type Error: std::error::Error + std::marker::Send + std::marker::Sync + 'static; |
| |
| /// Stores the client configuration parameters, including any IP address lease, associated with |
| /// the client identifier. |
| fn store_client_config( |
| &self, |
| client_id: &ClientIdentifier, |
| client_config: &CachedConfig, |
| ) -> Result<(), Self::Error>; |
| |
| /// Stores the DHCP option values served by the server. |
| fn store_options(&self, opts: &[DhcpOption]) -> Result<(), Self::Error>; |
| |
| /// Stores the DHCP server's configuration parameters. |
| fn store_parameters(&self, params: &ServerParameters) -> Result<(), Self::Error>; |
| |
| /// Loads a mapping of client identifiers to client configurations parameters. |
| async fn load_client_configs(&self) -> Result<CachedClients, Self::Error>; |
| |
| /// Loads DHCP option values. |
| async fn load_options(&self) -> Result<HashMap<OptionCode, DhcpOption>, Self::Error>; |
| |
| /// Loads the DHCP server's configuration parameters. |
| async fn load_parameters(&self) -> Result<ServerParameters, Self::Error>; |
| |
| /// Deletes the client configuration parameters associated with the client |
| /// identifier. |
| fn delete(&self, client_id: &ClientIdentifier) -> Result<(), Self::Error>; |
| } |
| |
| /// The default string used by the Server to identify itself to the Stash service. |
| pub const DEFAULT_STASH_ID: &str = "dhcpd"; |
| |
| /// 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 identifier: {}", _0)] |
| UnknownClientId(ClientIdentifier), |
| |
| #[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("missing server identifier in response")] |
| // According to RFC 2131, page 28, all server responses MUST include server identifier. |
| // |
| // https://tools.ietf.org/html/rfc2131#page-29 |
| MissingServerIdentifier, |
| |
| #[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, |
| |
| #[error("inconsistent initial server state: {}", _0)] |
| InconsistentInitialServerState(AddressPoolError), |
| |
| #[error("client request message missing requested ip addr")] |
| MissingRequestedAddr, |
| } |
| |
| 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<DS: DataStore, TS: SystemTimeSource> Server<DS, TS> { |
| /// Attempts to instantiate a new `Server` value from the persisted state contained in the |
| /// provided parts. If the client leases and address pool contained in the provided parts are |
| /// inconsistent with one another, then instantiation will fail. |
| pub fn new_from_state( |
| store: DS, |
| params: ServerParameters, |
| options_repo: HashMap<OptionCode, DhcpOption>, |
| cache: CachedClients, |
| ) -> Result<Self, Error> { |
| Self::new_with_time_source(store, params, options_repo, cache, TS::with_current_time()) |
| } |
| |
| pub fn new_with_time_source( |
| store: DS, |
| params: ServerParameters, |
| options_repo: HashMap<OptionCode, DhcpOption>, |
| cache: CachedClients, |
| time_source: TS, |
| ) -> Result<Self, Error> { |
| let mut pool = AddressPool::new(params.managed_addrs.pool_range()); |
| for client_addr in cache.iter().filter_map(|(_id, config)| config.client_addr) { |
| let () = pool |
| .allocate_addr(client_addr) |
| .map_err(ServerError::InconsistentInitialServerState)?; |
| } |
| let mut server = Self { cache, pool, params, store, options_repo, time_source }; |
| let () = server.release_expired_leases()?; |
| Ok(server) |
| } |
| |
| /// Instantiates a new `Server`, without persisted state, from the supplied parameters. |
| pub fn new(store: DS, params: ServerParameters) -> Self { |
| Self { |
| cache: HashMap::new(), |
| pool: AddressPool::new(params.managed_addrs.pool_range()), |
| params, |
| store, |
| options_repo: HashMap::new(), |
| time_source: TS::with_current_time(), |
| } |
| } |
| |
| /// 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. |
| // |
| // Notice according to RFC 2131, Page 36, `yiaddr` in client request |
| // is always unspecified, so don't rely on that. |
| // |
| // See https://tools.ietf.org/html/rfc2131#page-37 |
| // |
| // 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 |
| | MessageType::DHCPREQUEST |
| | MessageType::DHCPINFORM => Some(Ipv4Addr::BROADCAST), |
| MessageType::DHCPACK |
| | MessageType::DHCPNAK |
| | MessageType::DHCPOFFER |
| | MessageType::DHCPDECLINE |
| | MessageType::DHCPRELEASE => None, |
| } |
| }) |
| } |
| } |
| |
| fn handle_discover(&mut self, disc: Message) -> Result<ServerAction, ServerError> { |
| let client_id = ClientIdentifier::from(&disc); |
| 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), client_id, &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(&ClientIdentifier::from(client)) { |
| if let Some(client_addr) = config.client_addr { |
| let now = |
| self.time_source.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(client_addr) |
| { |
| panic!("server tried to release unallocated ip {}", addr) |
| } |
| return Ok(client_addr); |
| } else { |
| if self.pool.addr_is_available(&client_addr) { |
| return Ok(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(|x| *x).map_err(AddressPoolError::into) |
| } |
| |
| fn store_client_config( |
| &mut self, |
| client_addr: Ipv4Addr, |
| client_id: ClientIdentifier, |
| 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( |
| Some(client_addr), |
| options, |
| self.time_source.now(), |
| lease_length_seconds, |
| )?; |
| self.store |
| .store_client_config(&client_id, &config) |
| .context("failed to store client in stash")?; |
| self.cache.insert(client_id, 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 = *get_requested_ip_addr(&req) |
| .ok_or(ServerError::MissingRequiredDhcpOption(OptionCode::RequestedIpAddress))?; |
| 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> { |
| let client_id = ClientIdentifier::from(req); |
| if let Some(client_config) = self.cache.get(&client_id) { |
| let now = self |
| .time_source |
| .now() |
| .duration_since(std::time::UNIX_EPOCH) |
| .map_err(|std::time::SystemTimeError { .. }| ServerError::ServerTimeError)?; |
| if let Some(client_addr) = client_config.client_addr { |
| if client_addr != *requested_ip { |
| Err(ServerError::RequestedIpOfferIpMismatch(*requested_ip, 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::MissingRequestedAddr) |
| } |
| } else { |
| Err(ServerError::UnknownClientId(client_id)) |
| } |
| } |
| |
| 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)); |
| } |
| let client_id = ClientIdentifier::from(&req); |
| if !self.cache.contains_key(&client_id) { |
| return Err(ServerError::UnknownClientId(client_id)); |
| } |
| 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); |
| let requested_ip = *requested_ip; |
| 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(&ClientIdentifier::from(&dec)); |
| Ok(ServerAction::AddressDecline(*declined_ip)) |
| } |
| |
| fn handle_release(&mut self, rel: Message) -> Result<ServerAction, ServerError> { |
| let client_id = ClientIdentifier::from(&rel); |
| if let Some(config) = self.cache.get_mut(&client_id) { |
| // From https://tools.ietf.org/html/rfc2131#section-4.3.4: |
| // |
| // Upon receipt of a DHCPRELEASE message, the server marks the network address as not |
| // allocated. The server SHOULD retain a record of the client's initialization |
| // parameters for possible reuse in response to subsequent requests from the client. |
| let () = self.pool.release_addr(rel.ciaddr)?; |
| config.client_addr = None; |
| let () = self.store.store_client_config(&client_id, config).map_err(|e| { |
| ServerError::ServerCacheUpdateFailure(StashError { error: anyhow::Error::from(e) }) |
| })?; |
| Ok(ServerAction::AddressRelease(rel.ciaddr)) |
| } else { |
| Err(ServerError::UnknownClientId(client_id)) |
| } |
| } |
| |
| 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 client_id = ClientIdentifier::from(&req); |
| let options = match self.cache.get(&client_id) { |
| Some(config) => { |
| let mut options = Vec::with_capacity(config.options.len() + 1); |
| options.push(DhcpOption::DhcpMessageType(MessageType::DHCPACK)); |
| options.extend(config.options.iter().cloned()); |
| options |
| } |
| None => return Err(ServerError::UnknownClientId(client_id)), |
| }; |
| 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)) |
| } |
| } |
| |
| /// Determines the server identifier to use in DHCP responses. This |
| /// identifier is also the address the server should use to communicate with |
| /// the client. |
| /// |
| /// RFC 2131, Section 4.1, https://tools.ietf.org/html/rfc2131#section-4.1 |
| /// |
| /// The 'server identifier' field is used both to identify a DHCP server |
| /// in a DHCP message and as a destination address from clients to |
| /// servers. A server with multiple network addresses MUST be prepared |
| /// to to accept any of its network addresses as identifying that server |
| /// in a DHCP message. To accommodate potentially incomplete network |
| /// connectivity, a server MUST choose an address as a 'server |
| /// identifier' that, to the best of the server's knowledge, is reachable |
| /// from the client. For example, if the DHCP server and the DHCP client |
| /// are connected to the same subnet (i.e., the 'giaddr' field in the |
| /// message from the client is zero), the server SHOULD select the IP |
| /// address the server is using for communication on that subnet as the |
| /// 'server identifier'. |
| fn get_server_ip(&self, req: &Message) -> Result<Ipv4Addr, ServerError> { |
| match get_server_id_from(&req) { |
| Some(addr) => { |
| if self.params.server_ips.contains(&addr) { |
| Ok(addr) |
| } else { |
| Err(ServerError::IncorrectDHCPServer(addr)) |
| } |
| } |
| // TODO(fxbug.dev/21423): This IP should be chosen based on the |
| // subnet of the client. |
| None => 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 = self |
| .time_source |
| .now() |
| .duration_since(std::time::UNIX_EPOCH) |
| .map_err(|std::time::SystemTimeError { .. }| ServerError::ServerTimeError)?; |
| let expired_clients: Vec<(ClientIdentifier, Option<Ipv4Addr>)> = self |
| .cache |
| .iter() |
| .filter(|(_id, config)| config.expired(now)) |
| .map(|(id, CachedConfig { client_addr, .. })| (id.clone(), *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 (id, ip) in expired_clients.into_iter() { |
| if let Some(ip) = ip { |
| // 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(&id); |
| // 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.store.delete(&id) { |
| // 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={}: {}", id, e) |
| } |
| } |
| Ok(()) |
| } |
| |
| /// Saves current parameters to stash. |
| fn save_params(&self) -> Result<(), Status> { |
| self.store.store_parameters(&self.params).map_err(|e| { |
| log::warn!("store_parameters({:?}) in stash failed: {}", self.params, e); |
| fuchsia_zircon::Status::INTERNAL |
| }) |
| } |
| } |
| |
| /// 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, DHCP |
| /// server parameters, and leases issued to clients, and support the trait methods to retrieve and |
| /// modify these stores. |
| pub trait ServerDispatcher { |
| /// Validates the current set of server parameters returning a reference to |
| /// the parameters if the configuration is valid or an error otherwise. |
| fn try_validate_parameters(&self) -> Result<&ServerParameters, Status>; |
| |
| /// 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>; |
| /// Clears all leases from the store maintained by the ServerDispatcher. |
| fn dispatch_clear_leases(&mut self) -> Result<(), Status>; |
| } |
| |
| impl<DS: DataStore, TS: SystemTimeSource> ServerDispatcher for Server<DS, TS> { |
| fn try_validate_parameters(&self) -> Result<&ServerParameters, Status> { |
| if !self.params.is_valid() { |
| return Err(Status::INVALID_ARGS); |
| } |
| |
| // TODO(fxbug.dev/62558): rethink this check and this function. |
| if self.pool.is_empty() { |
| log::error!("Server validation failed: Address pool is empty"); |
| return Err(Status::INVALID_ARGS); |
| } |
| Ok(&self.params) |
| } |
| |
| 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.store.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) => { |
| // Be overzealous and do not allow the managed addresses to |
| // change if we currently have leases. |
| if !self.cache.is_empty() { |
| return Err(Status::BAD_STATE); |
| } |
| |
| 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); |
| } |
| }; |
| // Update the pool with the new parameters. |
| self.pool = AddressPool::new(self.params.managed_addrs.pool_range()); |
| } |
| 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::ParameterUnknown!() => return Err(Status::INVALID_ARGS), |
| }; |
| let () = self.save_params()?; |
| 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.store.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.save_params()?; |
| Ok(()) |
| } |
| |
| fn dispatch_clear_leases(&mut self) -> Result<(), Status> { |
| for (id, config) in &self.cache { |
| if let Some(client_addr) = config.client_addr { |
| let () = self.pool.release_addr(client_addr).unwrap_or_else(|e| { |
| // Log and panic because server has irrecoverable inconsistent state. |
| log::error!("release_addr({}) failed: {:?}", client_addr, e); |
| panic!("server tried to release unallocated addr {}", client_addr); |
| }); |
| } |
| let () = self.store.delete(&id).map_err(|e| { |
| log::warn!("delete({}) failed: {:?}", id, e); |
| fuchsia_zircon::Status::INTERNAL |
| })?; |
| } |
| Ok(self.cache.clear()) |
| } |
| } |
| |
| /// 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<ClientIdentifier, 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(Clone, Debug, Deserialize, Serialize)] |
| pub struct CachedConfig { |
| client_addr: Option<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: None, |
| options: Vec::new(), |
| 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: Option<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 CachedConfig { lease_start_epoch_seconds, lease_length_seconds, .. } = self; |
| let end = std::time::Duration::from_secs( |
| *lease_start_epoch_seconds + u64::from(*lease_length_seconds), |
| ); |
| 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 get_client_state(msg: &Message) -> Result<ClientState, ()> { |
| let server_id = get_server_id_from(&msg); |
| let requested_ip = get_requested_ip_addr(&msg); |
| |
| // State classification from: https://tools.ietf.org/html/rfc2131#section-4.3.2 |
| // |
| // DHCPREQUEST generated during SELECTING state: |
| // |
| // Client inserts the address of the selected server in 'server identifier', 'ciaddr' MUST be |
| // zero, 'requested IP address' MUST be filled in with the yiaddr value from the chosen |
| // DHCPOFFER. |
| // |
| // DHCPREQUEST generated during INIT-REBOOT state: |
| // |
| // 'server identifier' MUST NOT be filled in, 'requested IP address' option MUST be |
| // filled in with client's notion of its previously assigned address. 'ciaddr' MUST be |
| // zero. |
| // |
| // DHCPREQUEST generated during RENEWING state: |
| // |
| // 'server identifier' MUST NOT be filled in, 'requested IP address' option MUST NOT be filled |
| // in, 'ciaddr' MUST be filled in with client's IP address. |
| // |
| // TODO(fxbug.dev/64978): Distinguish between clients in RENEWING and REBINDING states |
| if server_id.is_some() && msg.ciaddr.is_unspecified() && requested_ip.is_some() { |
| Ok(ClientState::Selecting) |
| } else if server_id.is_none() && requested_ip.is_some() && msg.ciaddr.is_unspecified() { |
| Ok(ClientState::InitReboot) |
| } else if server_id.is_none() && requested_ip.is_none() && !msg.ciaddr.is_unspecified() { |
| Ok(ClientState::Renewing) |
| } else { |
| Err(()) |
| } |
| } |
| |
| 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() |
| } |
| |
| pub fn get_server_id_from(req: &Message) -> Option<Ipv4Addr> { |
| req.options.iter().find_map(|opt| match opt { |
| DhcpOption::ServerIdentifier(addr) => Some(*addr), |
| _ => None, |
| }) |
| } |
| |
| #[cfg(test)] |
| pub mod tests { |
| |
| use crate::configuration::{ |
| LeaseLength, ManagedAddresses, PermittedMacs, StaticAssignments, SubnetMask, |
| }; |
| use crate::protocol::{ |
| DhcpOption, FidlCompatible as _, IntoFidlExt as _, Message, MessageType, OpCode, |
| OptionCode, ProtocolError, |
| }; |
| use crate::server::{ |
| get_client_state, AddressPool, AddressPoolError, CachedConfig, ClientIdentifier, |
| ClientState, DataStore, ServerAction, ServerDispatcher, ServerError, ServerParameters, |
| SystemTimeSource, |
| }; |
| use anyhow::{Context as _, Error}; |
| use datastore::{ActionRecordingDataStore, DataStoreAction}; |
| use fidl_fuchsia_hardware_ethernet_ext::MacAddress as MacAddr; |
| use fuchsia_zircon::Status; |
| use net_declare::{fidl_ip_v4, std_ip_v4}; |
| use rand::Rng; |
| use std::cell::RefCell; |
| use std::collections::{HashMap, HashSet}; |
| use std::convert::TryFrom as _; |
| use std::iter::FromIterator as _; |
| use std::net::Ipv4Addr; |
| use std::rc::Rc; |
| use std::time::{Duration, SystemTime}; |
| |
| mod datastore { |
| use super::default_server_params; |
| use crate::protocol::{DhcpOption, OptionCode}; |
| use crate::server::{ |
| CachedClients, CachedConfig, ClientIdentifier, DataStore, ServerParameters, |
| }; |
| use std::collections::HashMap; |
| |
| // A Mutex is used here for Sync/Send interior mutability on a struct implementing an async |
| // trait whose methods take &self. |
| pub struct ActionRecordingDataStore { |
| actions: std::sync::Mutex<Vec<DataStoreAction>>, |
| } |
| |
| #[derive(Clone, Debug, PartialEq)] |
| pub enum DataStoreAction { |
| StoreClientConfig { client_id: ClientIdentifier, client_config: CachedConfig }, |
| StoreOptions { opts: Vec<DhcpOption> }, |
| StoreParameters { params: ServerParameters }, |
| LoadClientConfigs, |
| LoadOptions, |
| LoadParameters, |
| Delete { client_id: ClientIdentifier }, |
| } |
| |
| #[derive(Debug, thiserror::Error)] |
| #[error(transparent)] |
| pub struct ActionRecordingError(#[from] anyhow::Error); |
| |
| impl ActionRecordingDataStore { |
| pub fn new() -> Self { |
| Self { actions: std::sync::Mutex::new(Vec::new()) } |
| } |
| |
| pub fn push_action(&self, cmd: DataStoreAction) -> () { |
| let Self { actions } = self; |
| actions.lock().unwrap().push(cmd) |
| } |
| |
| pub fn actions(&mut self) -> std::vec::Drain<'_, DataStoreAction> { |
| let Self { actions } = self; |
| actions.get_mut().unwrap().drain(..) |
| } |
| } |
| |
| impl Drop for ActionRecordingDataStore { |
| fn drop(&mut self) { |
| let Self { actions } = self; |
| assert!(actions.lock().unwrap().is_empty()) |
| } |
| } |
| |
| #[async_trait::async_trait] |
| impl DataStore for ActionRecordingDataStore { |
| type Error = ActionRecordingError; |
| |
| fn store_client_config( |
| &self, |
| client_id: &ClientIdentifier, |
| client_config: &CachedConfig, |
| ) -> Result<(), Self::Error> { |
| Ok(self.push_action(DataStoreAction::StoreClientConfig { |
| client_id: client_id.clone(), |
| client_config: client_config.clone(), |
| })) |
| } |
| |
| fn store_options(&self, opts: &[DhcpOption]) -> Result<(), Self::Error> { |
| Ok(self.push_action(DataStoreAction::StoreOptions { opts: Vec::from(opts) })) |
| } |
| |
| fn store_parameters(&self, params: &ServerParameters) -> Result<(), Self::Error> { |
| Ok(self.push_action(DataStoreAction::StoreParameters { params: params.clone() })) |
| } |
| |
| async fn load_client_configs(&self) -> Result<CachedClients, Self::Error> { |
| let () = self.push_action(DataStoreAction::LoadClientConfigs); |
| Ok(HashMap::new()) |
| } |
| |
| async fn load_options(&self) -> Result<HashMap<OptionCode, DhcpOption>, Self::Error> { |
| let () = self.push_action(DataStoreAction::LoadOptions); |
| Ok(HashMap::new()) |
| } |
| |
| async fn load_parameters(&self) -> Result<ServerParameters, Self::Error> { |
| let () = self.push_action(DataStoreAction::LoadParameters); |
| Ok(default_server_params()?) |
| } |
| |
| fn delete(&self, client_id: &ClientIdentifier) -> Result<(), Self::Error> { |
| Ok(self.push_action(DataStoreAction::Delete { client_id: client_id.clone() })) |
| } |
| } |
| } |
| |
| // UTC time can go backwards (https://fuchsia.dev/fuchsia-src/concepts/time/utc/behavior), |
| // using `SystemTime::now` has the possibility to introduce flakiness to tests. This struct |
| // makes sure we can get non-decreasing `SystemTime`s in a test environment. |
| #[derive(Clone)] |
| struct TestSystemTime(Rc<RefCell<SystemTime>>); |
| |
| impl SystemTimeSource for TestSystemTime { |
| fn with_current_time() -> Self { |
| Self(Rc::new(RefCell::new(SystemTime::now()))) |
| } |
| fn now(&self) -> SystemTime { |
| let TestSystemTime(current) = self; |
| *current.borrow() |
| } |
| } |
| |
| impl TestSystemTime { |
| pub(super) fn move_forward(&mut self, duration: Duration) { |
| let TestSystemTime(current) = self; |
| *current.borrow_mut() += duration; |
| } |
| } |
| |
| type Server<DS = ActionRecordingDataStore> = super::Server<DS, TestSystemTime>; |
| |
| fn default_server_params() -> Result<ServerParameters, Error> { |
| test_server_params( |
| Vec::new(), |
| LeaseLength { default_seconds: 60 * 60 * 24, max_seconds: 60 * 60 * 24 * 7 }, |
| ) |
| } |
| |
| fn test_server_params( |
| server_ips: Vec<Ipv4Addr>, |
| lease_length: LeaseLength, |
| ) -> Result<ServerParameters, Error> { |
| Ok(ServerParameters { |
| server_ips, |
| lease_length, |
| managed_addrs: ManagedAddresses { |
| network_id: net_declare::std::ip_v4!("182.168.0.0"), |
| broadcast: net_declare::std::ip_v4!("192.168.0.255"), |
| mask: SubnetMask::try_from(24)?, |
| pool_range_start: net_declare::std::ip_v4!("192.168.0.0"), |
| pool_range_stop: net_declare::std::ip_v4!("192.168.0.0"), |
| }, |
| permitted_macs: PermittedMacs(Vec::new()), |
| static_assignments: StaticAssignments(HashMap::new()), |
| arp_probe: false, |
| bound_device_names: Vec::new(), |
| }) |
| } |
| |
| 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) |
| } |
| } |
| |
| fn get_router<DS: DataStore>(server: &Server<DS>) -> Result<Vec<Ipv4Addr>, ProtocolError> { |
| let code = OptionCode::Router; |
| match server.options_repo.get(&code) { |
| Some(DhcpOption::Router(router)) => Some(router.clone()), |
| option => panic!("unexpected entry {} => {:?}", &code, option), |
| } |
| .ok_or(ProtocolError::MissingOption(code)) |
| } |
| |
| fn get_dns_server<DS: DataStore>(server: &Server<DS>) -> Result<Vec<Ipv4Addr>, ProtocolError> { |
| let code = OptionCode::DomainNameServer; |
| match server.options_repo.get(&code) { |
| Some(DhcpOption::DomainNameServer(dns_server)) => Some(dns_server.clone()), |
| option => panic!("unexpected entry {} => {:?}", &code, option), |
| } |
| .ok_or(ProtocolError::MissingOption(code)) |
| } |
| |
| fn new_test_minimal_server_with_time_source() -> Result<(Server, TestSystemTime), Error> { |
| let time_source = TestSystemTime::with_current_time(); |
| let params = test_server_params( |
| vec![random_ipv4_generator()], |
| LeaseLength { default_seconds: 100, max_seconds: 60 * 60 * 24 * 7 }, |
| )?; |
| Ok(( |
| super::Server { |
| cache: HashMap::new(), |
| pool: AddressPool::new(params.managed_addrs.pool_range()), |
| params, |
| store: ActionRecordingDataStore::new(), |
| options_repo: HashMap::from_iter(vec![ |
| (OptionCode::Router, DhcpOption::Router(vec![random_ipv4_generator()])), |
| ( |
| OptionCode::DomainNameServer, |
| DhcpOption::DomainNameServer(vec![ |
| std_ip_v4!("1.2.3.4"), |
| std_ip_v4!("4.3.2.1"), |
| ]), |
| ), |
| ]), |
| time_source: time_source.clone(), |
| }, |
| time_source.clone(), |
| )) |
| } |
| |
| fn new_test_minimal_server() -> Result<Server, Error> { |
| let (server, _time_source) = new_test_minimal_server_with_time_source()?; |
| Ok(server) |
| } |
| |
| fn new_client_message(message_type: MessageType) -> Message { |
| new_client_message_with_options(message_type, std::iter::empty()) |
| } |
| |
| fn new_client_message_with_options( |
| message_type: MessageType, |
| options: impl Iterator<Item = DhcpOption>, |
| ) -> Message { |
| Message { |
| op: OpCode::BOOTREQUEST, |
| xid: rand::thread_rng().gen(), |
| secs: 0, |
| bdcast_flag: false, |
| ciaddr: Ipv4Addr::UNSPECIFIED, |
| yiaddr: Ipv4Addr::UNSPECIFIED, |
| siaddr: Ipv4Addr::UNSPECIFIED, |
| giaddr: Ipv4Addr::UNSPECIFIED, |
| chaddr: random_mac_generator(), |
| sname: String::new(), |
| file: String::new(), |
| options: std::iter::once(DhcpOption::DhcpMessageType(message_type)) |
| .chain(std::iter::once(DhcpOption::ParameterRequestList(vec![ |
| OptionCode::SubnetMask, |
| OptionCode::Router, |
| OptionCode::DomainNameServer, |
| ]))) |
| .chain(options) |
| .collect(), |
| } |
| } |
| |
| fn new_test_discover() -> Message { |
| new_test_discover_with_options(std::iter::empty()) |
| } |
| |
| fn new_test_discover_with_options(options: impl Iterator<Item = DhcpOption>) -> Message { |
| new_client_message_with_options(MessageType::DHCPDISCOVER, options) |
| } |
| |
| fn new_server_message<DS: DataStore>( |
| message_type: MessageType, |
| client_message: &Message, |
| server: &Server<DS>, |
| ) -> Message { |
| let Message { |
| op: _, |
| xid, |
| secs: _, |
| bdcast_flag: _, |
| ciaddr: _, |
| yiaddr: _, |
| siaddr: _, |
| giaddr: _, |
| chaddr, |
| sname: _, |
| file: _, |
| options: _, |
| } = client_message; |
| Message { |
| op: OpCode::BOOTREPLY, |
| xid: *xid, |
| secs: 0, |
| bdcast_flag: false, |
| ciaddr: Ipv4Addr::UNSPECIFIED, |
| yiaddr: Ipv4Addr::UNSPECIFIED, |
| siaddr: Ipv4Addr::UNSPECIFIED, |
| giaddr: Ipv4Addr::UNSPECIFIED, |
| chaddr: *chaddr, |
| sname: String::new(), |
| file: String::new(), |
| options: vec![ |
| DhcpOption::DhcpMessageType(message_type), |
| DhcpOption::ServerIdentifier( |
| server.get_server_ip(client_message).unwrap_or(Ipv4Addr::UNSPECIFIED), |
| ), |
| ], |
| } |
| } |
| |
| fn new_server_message_with_lease<DS: DataStore>( |
| message_type: MessageType, |
| client_message: &Message, |
| server: &Server<DS>, |
| ) -> Message { |
| let mut msg = new_server_message(message_type, client_message, server); |
| msg.options.extend( |
| [ |
| DhcpOption::IpAddressLeaseTime(100), |
| DhcpOption::RenewalTimeValue(50), |
| DhcpOption::RebindingTimeValue(75), |
| ] |
| // TODO(https://github.com/rust-lang/rust/issues/25725): use into_iter. |
| .iter() |
| .cloned(), |
| ); |
| let () = add_server_options(&mut msg, server); |
| msg |
| } |
| |
| fn add_server_options<DS: DataStore>(msg: &mut Message, server: &Server<DS>) { |
| msg.options.push(DhcpOption::SubnetMask(std_ip_v4!("255.255.255.0"))); |
| if let Some(routers) = match server.options_repo.get(&OptionCode::Router) { |
| Some(DhcpOption::Router(v)) => Some(v), |
| _ => None, |
| } { |
| msg.options.push(DhcpOption::Router(routers.clone())); |
| } |
| if let Some(servers) = match server.options_repo.get(&OptionCode::DomainNameServer) { |
| Some(DhcpOption::DomainNameServer(v)) => Some(v), |
| _ => None, |
| } { |
| msg.options.push(DhcpOption::DomainNameServer(servers.clone())); |
| } |
| } |
| |
| fn new_test_offer<DS: DataStore>(disc: &Message, server: &Server<DS>) -> Message { |
| new_server_message_with_lease(MessageType::DHCPOFFER, disc, server) |
| } |
| |
| fn new_test_request() -> Message { |
| new_client_message(MessageType::DHCPREQUEST) |
| } |
| |
| fn new_test_request_selecting_state<DS: DataStore>( |
| server: &Server<DS>, |
| requested_ip: Ipv4Addr, |
| ) -> Message { |
| let mut req = new_test_request(); |
| req.options.push(DhcpOption::RequestedIpAddress(requested_ip)); |
| req.options.push(DhcpOption::ServerIdentifier( |
| server.get_server_ip(&req).unwrap_or(Ipv4Addr::UNSPECIFIED), |
| )); |
| req |
| } |
| |
| fn new_test_ack<DS: DataStore>(req: &Message, server: &Server<DS>) -> Message { |
| new_server_message_with_lease(MessageType::DHCPACK, req, server) |
| } |
| |
| fn new_test_nak<DS: DataStore>(req: &Message, server: &Server<DS>, error: String) -> Message { |
| let mut nak = new_server_message(MessageType::DHCPNAK, req, server); |
| nak.options.push(DhcpOption::Message(error)); |
| nak |
| } |
| |
| fn new_test_release() -> Message { |
| new_client_message(MessageType::DHCPRELEASE) |
| } |
| |
| fn new_test_inform() -> Message { |
| new_client_message(MessageType::DHCPINFORM) |
| } |
| |
| fn new_test_inform_ack<DS: DataStore>(req: &Message, server: &Server<DS>) -> Message { |
| let mut msg = new_server_message(MessageType::DHCPACK, req, server); |
| let () = add_server_options(&mut msg, server); |
| msg |
| } |
| |
| fn new_test_decline<DS: DataStore>(server: &Server<DS>) -> Message { |
| let mut decline = new_client_message(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()?; |
| let mut disc = new_test_discover(); |
| disc.giaddr = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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))) |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| let mut disc = new_test_discover(); |
| disc.ciaddr = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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))) |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| let mut disc = new_test_discover(); |
| disc.bdcast_flag = true; |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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))) |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| let disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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))) |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| let mut disc = new_test_discover(); |
| disc.giaddr = random_ipv4_generator(); |
| disc.ciaddr = random_ipv4_generator(); |
| disc.bdcast_flag = true; |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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))) |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| let mut disc = new_test_discover(); |
| disc.ciaddr = random_ipv4_generator(); |
| disc.bdcast_flag = true; |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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))) |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_discover_updates_server_state() -> Result<(), Error> { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| let disc = new_test_discover(); |
| |
| let offer_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| server.pool.available_addrs.insert(offer_ip); |
| |
| let server_id = server.params.server_ips.first().unwrap(); |
| let router = get_router(&server)?; |
| let dns_server = get_dns_server(&server)?; |
| let expected_client_config = CachedConfig::new( |
| Some(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(std_ip_v4!("255.255.255.0")), |
| DhcpOption::Router(router), |
| DhcpOption::DomainNameServer(dns_server), |
| ], |
| time_source.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_id), Some(&expected_client_config)); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| Ok(()) |
| } |
| |
| async fn dispatch_with_discover_updates_stash_helper( |
| additional_options: impl Iterator<Item = DhcpOption>, |
| ) -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| let disc = new_test_discover_with_options(additional_options); |
| |
| let client_id = ClientIdentifier::from(&disc); |
| |
| server.pool.available_addrs.insert(random_ipv4_generator()); |
| |
| let server_action = server.dispatch(disc); |
| assert!(server_action.is_ok()); |
| |
| let client_config = server |
| .cache |
| .get(&client_id) |
| .ok_or(anyhow::anyhow!("server cache missing entry for {}", client_id))? |
| .clone(); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreClientConfig { client_id: id, client_config: config }, |
| ] if *id == client_id && *config == client_config |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_discover_updates_stash() -> Result<(), Error> { |
| dispatch_with_discover_updates_stash_helper(std::iter::empty()).await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_discover_with_client_id_updates_stash() -> Result<(), Error> { |
| dispatch_with_discover_updates_stash_helper(std::iter::once(DhcpOption::ClientIdentifier( |
| vec![1, 2, 3, 4, 5], |
| ))) |
| .await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_discover_client_binding_returns_bound_addr() -> Result<(), Error> { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| let disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| let bound_client_ip = random_ipv4_generator(); |
| |
| server.pool.allocated_addrs.insert(bound_client_ip); |
| |
| server.cache.insert( |
| ClientIdentifier::from(&disc), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.now(), std::u32::MAX)?, |
| ); |
| |
| let response = server.dispatch(disc).unwrap(); |
| |
| assert_eq!(extract_message(response).yiaddr, bound_client_ip); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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, time_source) = new_test_minimal_server_with_time_source().unwrap(); |
| let disc = new_test_discover(); |
| |
| let bound_client_ip = random_ipv4_generator(); |
| |
| server.cache.insert( |
| ClientIdentifier::from(&disc), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| let disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| let bound_client_ip = random_ipv4_generator(); |
| |
| server.pool.available_addrs.insert(bound_client_ip); |
| |
| server.cache.insert( |
| ClientIdentifier::from(&disc), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.now(), std::u32::MIN)?, |
| ); |
| |
| let response = server.dispatch(disc).unwrap(); |
| |
| assert_eq!(extract_message(response).yiaddr, bound_client_ip); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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, time_source) = new_test_minimal_server_with_time_source()?; |
| let disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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( |
| ClientIdentifier::from(&disc), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.now(), std::u32::MIN)?, |
| ); |
| |
| let response = server.dispatch(disc).unwrap(); |
| |
| assert_eq!(extract_message(response).yiaddr, free_ip); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_discover_expired_client_binding_returns_available_requested_addr( |
| ) -> Result<(), Error> { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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( |
| ClientIdentifier::from(&disc), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.now(), std::u32::MIN)?, |
| ); |
| |
| let response = server.dispatch(disc).unwrap(); |
| |
| assert_eq!(extract_message(response).yiaddr, requested_ip); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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( |
| ClientIdentifier::from(&disc), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.now(), std::u32::MIN)?, |
| ); |
| |
| let response = server.dispatch(disc).unwrap(); |
| |
| assert_eq!(extract_message(response).yiaddr, free_ip); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| let mut disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| let mut disc = new_test_discover(); |
| let client_id = ClientIdentifier::from(&disc); |
| |
| 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); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| 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()?; |
| let disc = new_test_discover(); |
| server.pool.available_addrs.clear(); |
| |
| assert_eq!( |
| server.dispatch(disc), |
| Err(ServerError::ServerAddressPoolFailure(AddressPoolError::Ipv4AddrExhaustion)) |
| ); |
| Ok(()) |
| } |
| |
| async fn test_dispatch_with_bogus_client_message_returns_error( |
| message_type: MessageType, |
| ) -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| |
| assert_eq!( |
| server.dispatch(Message { |
| op: OpCode::BOOTREQUEST, |
| xid: 0, |
| secs: 0, |
| bdcast_flag: false, |
| ciaddr: Ipv4Addr::UNSPECIFIED, |
| yiaddr: Ipv4Addr::UNSPECIFIED, |
| siaddr: Ipv4Addr::UNSPECIFIED, |
| giaddr: Ipv4Addr::UNSPECIFIED, |
| chaddr: MacAddr { octets: [0; 6] }, |
| sname: String::new(), |
| file: String::new(), |
| options: vec![DhcpOption::DhcpMessageType(message_type),], |
| }), |
| Err(ServerError::UnexpectedClientMessageType(message_type)) |
| ); |
| |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_client_offer_message_returns_error() -> Result<(), Error> { |
| test_dispatch_with_bogus_client_message_returns_error(MessageType::DHCPOFFER).await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_client_ack_message_returns_error() -> Result<(), Error> { |
| test_dispatch_with_bogus_client_message_returns_error(MessageType::DHCPACK).await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_client_nak_message_returns_error() -> Result<(), Error> { |
| test_dispatch_with_bogus_client_message_returns_error(MessageType::DHCPNAK).await |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_selecting_request_returns_correct_ack() -> Result<(), Error> { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| let requested_ip = random_ipv4_generator(); |
| let req = new_test_request_selecting_state(&server, requested_ip); |
| |
| server.pool.allocated_addrs.insert(requested_ip); |
| |
| let server_id = server.params.server_ips.first().unwrap(); |
| let router = get_router(&server)?; |
| let dns_server = get_dns_server(&server)?; |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new( |
| Some(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(std_ip_v4!("255.255.255.0")), |
| DhcpOption::Router(router), |
| DhcpOption::DomainNameServer(dns_server), |
| ], |
| time_source.now(), |
| std::u32::MAX, |
| )?, |
| ); |
| |
| let mut expected_ack = new_test_ack(&req, &server); |
| expected_ack.yiaddr = requested_ip; |
| |
| assert_eq!( |
| server.dispatch(req), |
| Ok(ServerAction::SendResponse(expected_ack, Some(Ipv4Addr::BROADCAST))) |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_selecting_request_maintains_server_invariants() -> Result<(), Error> |
| { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| let requested_ip = random_ipv4_generator(); |
| let req = new_test_request_selecting_state(&server, requested_ip); |
| |
| let client_id = ClientIdentifier::from(&req); |
| |
| server.pool.allocated_addrs.insert(requested_ip); |
| server.cache.insert( |
| client_id.clone(), |
| CachedConfig::new(Some(requested_ip), Vec::new(), time_source.now(), std::u32::MAX)?, |
| ); |
| let _response = server.dispatch(req).unwrap(); |
| assert!(server.cache.contains_key(&client_id)); |
| 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()?; |
| let mut req = new_test_request_selecting_state(&server, 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()?; |
| let requested_ip = random_ipv4_generator(); |
| let req = new_test_request_selecting_state(&server, requested_ip); |
| |
| let client_id = ClientIdentifier::from(&req); |
| |
| assert_eq!(server.dispatch(req), Err(ServerError::UnknownClientId(client_id.clone()))); |
| assert!(!server.cache.contains_key(&client_id)); |
| 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, time_source) = new_test_minimal_server_with_time_source()?; |
| let client_requested_ip = random_ipv4_generator(); |
| let req = new_test_request_selecting_state(&server, client_requested_ip); |
| |
| let server_offered_ip = random_ipv4_generator(); |
| |
| server.pool.allocated_addrs.insert(server_offered_ip); |
| |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new( |
| Some(server_offered_ip), |
| Vec::new(), |
| time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| let requested_ip = random_ipv4_generator(); |
| let req = new_test_request_selecting_state(&server, requested_ip); |
| |
| server.pool.allocated_addrs.insert(requested_ip); |
| |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new(Some(requested_ip), Vec::new(), time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| let requested_ip = random_ipv4_generator(); |
| let req = new_test_request_selecting_state(&server, requested_ip); |
| |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new(Some(requested_ip), Vec::new(), time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| 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 = std_ip_v4!("192.168.1.60"); |
| server.params.server_ips = vec![std_ip_v4!("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 = get_router(&server)?; |
| let dns_server = get_dns_server(&server)?; |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new( |
| Some(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(std_ip_v4!("255.255.255.0")), |
| DhcpOption::Router(router), |
| DhcpOption::DomainNameServer(dns_server), |
| ], |
| time_source.now(), |
| std::u32::MAX, |
| )?, |
| ); |
| |
| let mut expected_ack = new_test_ack(&req, &server); |
| expected_ack.yiaddr = init_reboot_client_ip; |
| |
| assert_eq!( |
| server.dispatch(req), |
| Ok(ServerAction::SendResponse(expected_ack, Some(Ipv4Addr::BROADCAST))) |
| ); |
| 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()?; |
| 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()?; |
| 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()?; |
| let mut req = new_test_request(); |
| |
| let client_id = ClientIdentifier::from(&req); |
| |
| // Update requested ip and server ip to be on the same subnet. |
| req.options.push(DhcpOption::RequestedIpAddress(std_ip_v4!("192.165.30.45"))); |
| server.params.server_ips = vec![std_ip_v4!("192.165.30.1")]; |
| |
| assert_eq!(server.dispatch(req), Err(ServerError::UnknownClientId(client_id))); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_init_boot_request_mismatched_requested_addr_returns_nak( |
| ) -> Result<(), Error> { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut req = new_test_request(); |
| |
| // Update requested ip and server ip to be on the same subnet. |
| let init_reboot_client_ip = std_ip_v4!("192.165.25.4"); |
| req.options.push(DhcpOption::RequestedIpAddress(init_reboot_client_ip)); |
| server.params.server_ips = vec![std_ip_v4!("192.165.25.1")]; |
| |
| let server_cached_ip = std_ip_v4!("192.165.25.10"); |
| server.pool.allocated_addrs.insert(server_cached_ip); |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new( |
| Some(server_cached_ip), |
| Vec::new(), |
| time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut req = new_test_request(); |
| |
| let init_reboot_client_ip = std_ip_v4!("192.165.25.4"); |
| req.options.push(DhcpOption::RequestedIpAddress(init_reboot_client_ip)); |
| server.params.server_ips = vec![std_ip_v4!("192.165.25.1")]; |
| |
| server.pool.allocated_addrs.insert(init_reboot_client_ip); |
| // Expire client binding to make it invalid. |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new( |
| Some(init_reboot_client_ip), |
| Vec::new(), |
| time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut req = new_test_request(); |
| |
| let init_reboot_client_ip = std_ip_v4!("192.165.25.4"); |
| req.options.push(DhcpOption::RequestedIpAddress(init_reboot_client_ip)); |
| server.params.server_ips = vec![std_ip_v4!("192.165.25.1")]; |
| |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new( |
| Some(init_reboot_client_ip), |
| Vec::new(), |
| time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| 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 = get_router(&server)?; |
| let dns_server = get_dns_server(&server)?; |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new( |
| Some(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(std_ip_v4!("255.255.255.0")), |
| DhcpOption::Router(router), |
| DhcpOption::DomainNameServer(dns_server), |
| ], |
| time_source.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()?; |
| let mut req = new_test_request(); |
| |
| let bound_client_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&req); |
| |
| req.ciaddr = bound_client_ip; |
| |
| assert_eq!(server.dispatch(req), Err(ServerError::UnknownClientId(client_id))); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_renewing_request_mismatched_requested_addr_returns_error( |
| ) -> Result<(), Error> { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| 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( |
| ClientIdentifier::from(&req), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| 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( |
| ClientIdentifier::from(&req), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut req = new_test_request(); |
| |
| let bound_client_ip = random_ipv4_generator(); |
| req.ciaddr = bound_client_ip; |
| |
| server.cache.insert( |
| ClientIdentifier::from(&req), |
| CachedConfig::new(Some(bound_client_ip), Vec::new(), time_source.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()?; |
| |
| 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 requested ip populated. |
| req.options.push(DhcpOption::ServerIdentifier(random_ipv4_generator())); |
| req.options.push(DhcpOption::RequestedIpAddress(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()?; |
| 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, mut time_source) = new_test_minimal_server_with_time_source()?; |
| server.pool.available_addrs.clear(); |
| |
| // Insert client 1 bindings. |
| let client_1_ip = random_ipv4_generator(); |
| let client_1_id = ClientIdentifier::from(random_mac_generator()); |
| let client_opts = [DhcpOption::IpAddressLeaseTime(std::u32::MAX)]; |
| server.pool.available_addrs.insert(client_1_ip); |
| server.store_client_config(client_1_ip, client_1_id.clone(), &client_opts)?; |
| |
| // Insert client 2 bindings. |
| let client_2_ip = random_ipv4_generator(); |
| let client_2_id = ClientIdentifier::from(random_mac_generator()); |
| server.pool.available_addrs.insert(client_2_ip); |
| server.store_client_config(client_2_ip, client_2_id.clone(), &client_opts)?; |
| |
| // Insert client 3 bindings. |
| let client_3_ip = random_ipv4_generator(); |
| let client_3_id = ClientIdentifier::from(random_mac_generator()); |
| server.pool.available_addrs.insert(client_3_ip); |
| server.store_client_config(client_3_ip, client_3_id.clone(), &client_opts)?; |
| |
| let () = time_source.move_forward(Duration::from_secs(1)); |
| let () = server.release_expired_leases()?; |
| |
| let client_ips = |
| // TODO(https://github.com/rust-lang/rust/issues/25725): use into_iter. |
| [client_1_ip, client_2_ip, client_3_ip].iter().cloned().collect::<HashSet<_>>(); |
| matches::assert_matches!( |
| &server.cache.iter().collect::<Vec<_>>()[..], |
| [ |
| (id1, CachedConfig {client_addr: Some(ip1), ..}), |
| (id2, CachedConfig {client_addr: Some(ip2), ..}), |
| (id3, CachedConfig {client_addr: Some(ip3), ..}), |
| ] if [id1, id2, id3].iter().all(|id| { |
| [&client_1_id, &client_2_id, &client_3_id].contains(id) |
| }) && [ip1, ip2, ip3].iter().all(|ip| client_ips.contains(*ip)) |
| ); |
| assert!(server.pool.available_addrs.is_empty(), "{:?}", server.pool.available_addrs); |
| assert_eq!(server.pool.allocated_addrs, client_ips); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreClientConfig { client_id: id_1, .. }, |
| DataStoreAction::StoreClientConfig { client_id: id_2, .. }, |
| DataStoreAction::StoreClientConfig { client_id: id_3, .. }, |
| ] if *id_1 == client_1_id && *id_2 == client_2_id && *id_3 == client_3_id |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_release_expired_leases_with_all_expired_releases_all() -> Result<(), Error> { |
| let (mut server, mut time_source) = new_test_minimal_server_with_time_source()?; |
| server.pool.available_addrs.clear(); |
| |
| let client_1_ip = random_ipv4_generator(); |
| server.pool.available_addrs.insert(client_1_ip); |
| let client_1_id = ClientIdentifier::from(random_mac_generator()); |
| let () = server.store_client_config( |
| client_1_ip, |
| client_1_id.clone(), |
| &[DhcpOption::IpAddressLeaseTime(0)], |
| )?; |
| |
| let client_2_ip = random_ipv4_generator(); |
| server.pool.available_addrs.insert(client_2_ip); |
| let client_2_id = ClientIdentifier::from(random_mac_generator()); |
| let () = server.store_client_config( |
| client_2_ip, |
| client_2_id.clone(), |
| &[DhcpOption::IpAddressLeaseTime(0)], |
| )?; |
| |
| let client_3_ip = random_ipv4_generator(); |
| server.pool.available_addrs.insert(client_3_ip); |
| let client_3_id = ClientIdentifier::from(random_mac_generator()); |
| let () = server.store_client_config( |
| client_3_ip, |
| client_3_id.clone(), |
| &[DhcpOption::IpAddressLeaseTime(0)], |
| )?; |
| |
| let () = time_source.move_forward(Duration::from_secs(1)); |
| let () = server.release_expired_leases()?; |
| |
| assert!(server.cache.is_empty(), "{:?}", server.cache); |
| assert_eq!( |
| server.pool.available_addrs, |
| // TODO(https://github.com/rust-lang/rust/issues/25725): use into_iter. |
| [client_1_ip, client_2_ip, client_3_ip].iter().cloned().collect() |
| ); |
| assert!(server.pool.allocated_addrs.is_empty(), "{:?}", server.pool.allocated_addrs); |
| // Delete actions occur in non-deterministic (HashMap iteration) order, so we must not |
| // assert on the ordering of the deleted ids. |
| matches::assert_matches!( |
| &server.store.actions().as_slice()[..], |
| [ |
| DataStoreAction::StoreClientConfig { client_id: id_1, .. }, |
| DataStoreAction::StoreClientConfig { client_id: id_2, .. }, |
| DataStoreAction::StoreClientConfig { client_id: id_3, .. }, |
| DataStoreAction::Delete { client_id: del_id_1 }, |
| DataStoreAction::Delete { client_id: del_id_2 }, |
| DataStoreAction::Delete { client_id: del_id_3 }, |
| ] if *id_1 == client_1_id && *id_2 == client_2_id && *id_3 == client_3_id && |
| [del_id_1, del_id_2, del_id_3].iter().all(|id| { |
| [&client_1_id, &client_2_id, &client_3_id].contains(id) |
| }) |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_release_expired_leases_with_some_expired_releases_expired() -> Result<(), Error> { |
| let (mut server, mut time_source) = new_test_minimal_server_with_time_source()?; |
| server.pool.available_addrs.clear(); |
| |
| let client_1_ip = random_ipv4_generator(); |
| server.pool.available_addrs.insert(client_1_ip); |
| let client_1_id = ClientIdentifier::from(random_mac_generator()); |
| let () = server.store_client_config( |
| client_1_ip, |
| client_1_id.clone(), |
| &[DhcpOption::IpAddressLeaseTime(std::u32::MAX)], |
| )?; |
| |
| let client_2_ip = random_ipv4_generator(); |
| server.pool.available_addrs.insert(client_2_ip); |
| let client_2_id = ClientIdentifier::from(random_mac_generator()); |
| let () = server.store_client_config( |
| client_2_ip, |
| client_2_id.clone(), |
| &[DhcpOption::IpAddressLeaseTime(0)], |
| )?; |
| |
| let client_3_ip = random_ipv4_generator(); |
| server.pool.available_addrs.insert(client_3_ip); |
| let client_3_id = ClientIdentifier::from(random_mac_generator()); |
| let () = server.store_client_config( |
| client_3_ip, |
| client_3_id.clone(), |
| &[DhcpOption::IpAddressLeaseTime(std::u32::MAX)], |
| )?; |
| |
| let () = time_source.move_forward(Duration::from_secs(1)); |
| let () = server.release_expired_leases()?; |
| |
| let client_ips = |
| // TODO(https://github.com/rust-lang/rust/issues/25725): use into_iter. |
| [client_1_ip, client_3_ip].iter().cloned().collect::<HashSet<_>>(); |
| matches::assert_matches!( |
| &server.cache.iter().collect::<Vec<_>>()[..], |
| [ |
| (id1, CachedConfig {client_addr: Some(ip1), ..}), |
| (id3, CachedConfig {client_addr: Some(ip3), ..}), |
| ] if [id1, id3].iter().all(|id| [&client_1_id, &client_3_id].contains(id)) && |
| [ip1, ip3].iter().all(|ip| client_ips.contains(*ip)) |
| ); |
| assert_eq!( |
| server.pool.available_addrs, |
| // TODO(https://github.com/rust-lang/rust/issues/25725): use into_iter. |
| [client_2_ip].iter().cloned().collect() |
| ); |
| assert_eq!(server.pool.allocated_addrs, client_ips); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreClientConfig { client_id: id_1, .. }, |
| DataStoreAction::StoreClientConfig { client_id: id_2, .. }, |
| DataStoreAction::StoreClientConfig { client_id: id_3, .. }, |
| DataStoreAction::Delete { client_id: id_4 }, |
| ] if *id_1 == client_1_id && *id_2 == client_2_id && *id_3 == client_3_id && *id_4 == client_2_id |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_dispatch_with_known_release_updates_address_pool_retains_client_config( |
| ) -> Result<(), Error> { |
| let (mut server, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut release = new_test_release(); |
| |
| let release_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&release); |
| |
| server.pool.allocated_addrs.insert(release_ip); |
| release.ciaddr = release_ip; |
| |
| let test_client_config = |client_addr: Option<Ipv4Addr>, dns: Ipv4Addr| { |
| CachedConfig::new( |
| client_addr, |
| vec![DhcpOption::DomainNameServer(vec![dns])], |
| time_source.now(), |
| std::u32::MAX, |
| ) |
| .unwrap() |
| }; |
| |
| let dns = random_ipv4_generator(); |
| server.cache.insert(client_id.clone(), test_client_config(Some(release_ip), dns)); |
| |
| assert_eq!(server.dispatch(release), Ok(ServerAction::AddressRelease(release_ip))); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreClientConfig { client_id: id, client_config } |
| ] if *id == client_id && *client_config == test_client_config(None, dns) |
| ); |
| 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_id), "client config not retained"); |
| assert_eq!( |
| server.cache.get(&client_id).unwrap(), |
| &test_client_config(None, dns), |
| "retained client config changed other field than client_addr" |
| ); |
| |
| 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()?; |
| let mut release = new_test_release(); |
| |
| let release_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&release); |
| |
| server.pool.allocated_addrs.insert(release_ip); |
| release.ciaddr = release_ip; |
| |
| assert_eq!(server.dispatch(release), Err(ServerError::UnknownClientId(client_id))); |
| |
| 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()?; |
| 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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut decline = new_test_decline(&server); |
| |
| let declined_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&decline); |
| |
| decline.options.push(DhcpOption::RequestedIpAddress(declined_ip)); |
| |
| server.pool.allocated_addrs.insert(declined_ip); |
| |
| server.cache.insert( |
| client_id.clone(), |
| CachedConfig::new(Some(declined_ip), Vec::new(), time_source.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_id), "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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut decline = new_test_decline(&server); |
| |
| let declined_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&decline); |
| |
| 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_id.clone(), |
| CachedConfig::new( |
| Some(client_ip_according_to_server), |
| Vec::new(), |
| time_source.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_id), "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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut decline = new_test_decline(&server); |
| |
| let declined_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&decline); |
| |
| decline.options.push(DhcpOption::RequestedIpAddress(declined_ip)); |
| |
| server.pool.available_addrs.insert(declined_ip); |
| |
| server.cache.insert( |
| client_id.clone(), |
| CachedConfig::new(Some(declined_ip), Vec::new(), time_source.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_id), "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, time_source) = new_test_minimal_server_with_time_source()?; |
| let mut decline = new_test_decline(&server); |
| |
| let declined_ip = random_ipv4_generator(); |
| |
| decline.options.push(DhcpOption::RequestedIpAddress(declined_ip)); |
| |
| // Server contains client bindings which reflect a different address |
| // than the one being declined. |
| server.cache.insert( |
| ClientIdentifier::from(&decline), |
| CachedConfig::new( |
| Some(random_ipv4_generator()), |
| Vec::new(), |
| time_source.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()?; |
| 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, time_source) = new_test_minimal_server_with_time_source()?; |
| server.params.server_ips = vec![std_ip_v4!("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(std_ip_v4!("1.2.3.4"))); |
| |
| let declined_ip = random_ipv4_generator(); |
| let client_id = ClientIdentifier::from(&decline); |
| |
| decline.options.push(DhcpOption::RequestedIpAddress(declined_ip)); |
| |
| server.pool.allocated_addrs.insert(declined_ip); |
| server.cache.insert( |
| client_id.clone(), |
| CachedConfig::new(Some(declined_ip), Vec::new(), time_source.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_id), "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()?; |
| 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_id = ClientIdentifier::from(&disc); |
| |
| let client_requested_time: u32 = 20; |
| |
| disc.options.push(DhcpOption::IpAddressLeaseTime(client_requested_time)); |
| |
| let mut server = new_test_minimal_server()?; |
| 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_id).unwrap().lease_length_seconds, |
| client_requested_time, |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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_id = ClientIdentifier::from(&disc); |
| |
| 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()?; |
| server.pool.available_addrs.insert(std_ip_v4!("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_id).unwrap().lease_length_seconds, |
| server_max_lease_time, |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreClientConfig {client_id: id, ..}] if *id == client_id |
| ); |
| 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()?; |
| 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()?; |
| let option = || fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_ip_v4!("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()?; |
| 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()?; |
| let option = || fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_ip_v4!("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)); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreOptions { opts }, |
| ] if opts.contains(&stored_option) |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_server_dispatcher_set_option_saves_to_stash() -> Result<(), Error> { |
| let mask = [255, 255, 255, 0]; |
| let fidl_mask = fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_fuchsia_net::Ipv4Address { |
| addr: mask, |
| }); |
| let params = default_server_params()?; |
| let mut server: Server = super::Server { |
| cache: HashMap::new(), |
| pool: AddressPool::new(params.managed_addrs.pool_range()), |
| params, |
| store: ActionRecordingDataStore::new(), |
| options_repo: HashMap::new(), |
| time_source: TestSystemTime::with_current_time(), |
| }; |
| let () = server.dispatch_set_option(fidl_mask)?; |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreOptions { opts }, |
| ] if *opts == vec![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 fidl_lease = |
| fidl_fuchsia_net_dhcp::Parameter::Lease(fidl_fuchsia_net_dhcp::LeaseLength { |
| default: Some(default), |
| max: Some(max), |
| ..fidl_fuchsia_net_dhcp::LeaseLength::EMPTY |
| }); |
| let mut server = new_test_minimal_server()?; |
| let () = server.dispatch_set_parameter(fidl_lease)?; |
| matches::assert_matches!( |
| server.store.actions().next(), |
| Some(DataStoreAction::StoreParameters { |
| params: ServerParameters { |
| lease_length: LeaseLength { default_seconds: 42, max_seconds: 100 }, |
| .. |
| }, |
| }) |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_server_dispatcher_set_parameter() -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| 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, |
| ..fidl_fuchsia_net_dhcp::LeaseLength::EMPTY |
| }); |
| let bad_mask = |
| fidl_fuchsia_net_dhcp::Parameter::AddressPool(fidl_fuchsia_net_dhcp::AddressPool { |
| network_id: Some(fidl_ip_v4!("192.168.0.0")), |
| broadcast: Some(fidl_ip_v4!("192.168.0.255")), |
| mask: Some(fidl_ip_v4!("255.255.0.255")), |
| pool_range_start: Some(fidl_ip_v4!("192.168.0.2")), |
| pool_range_stop: Some(fidl_ip_v4!("192.168.0.254")), |
| ..fidl_fuchsia_net_dhcp::AddressPool::EMPTY |
| }); |
| 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::EMPTY |
| }, |
| 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::EMPTY |
| }, |
| ]); |
| |
| 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 |
| ); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreParameters { params }] if *params == server.params |
| ); |
| 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()?; |
| let mask = || fidl_fuchsia_net_dhcp::Option_::SubnetMask(fidl_ip_v4!("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()?; |
| 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()?; |
| 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.store.load_options().await?; |
| assert_eq!(empty_map, stored_opts); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreOptions { opts }, |
| DataStoreAction::LoadOptions |
| ] if opts.is_empty() |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_server_dispatcher_reset_parameters() -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| let default_params = test_server_params( |
| vec![std_ip_v4!("192.168.0.1")], |
| LeaseLength { default_seconds: 86400, max_seconds: 86400 }, |
| )?; |
| assert_ne!(default_params, server.params); |
| let () = server.dispatch_reset_parameters(&default_params)?; |
| assert_eq!(default_params, server.params); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreParameters { params }] if *params == default_params |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_server_dispatcher_clear_leases() -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| server.params.managed_addrs.pool_range_stop = std_ip_v4!("192.168.0.4"); |
| server.pool = AddressPool::new(server.params.managed_addrs.pool_range()); |
| let client = std_ip_v4!("192.168.0.2"); |
| let () = server |
| .pool |
| .allocate_addr(client) |
| .with_context(|| format!("allocate_addr({}) failed", client))?; |
| let client_id = ClientIdentifier::from(random_mac_generator()); |
| server.cache = [( |
| client_id.clone(), |
| CachedConfig { |
| client_addr: Some(client), |
| options: Vec::new(), |
| lease_start_epoch_seconds: 0, |
| lease_length_seconds: 42, |
| }, |
| )] |
| // TODO(https://github.com/rust-lang/rust/issues/25725): use into_iter. |
| .iter() |
| .cloned() |
| .collect(); |
| let () = server.dispatch_clear_leases().context("dispatch_clear_leases() failed")?; |
| let empty_map = HashMap::new(); |
| assert_eq!(empty_map, server.cache); |
| assert!(server.pool.addr_is_available(&client)); |
| assert!(!server.pool.addr_is_allocated(&client)); |
| let stored_leases = |
| server.store.load_client_configs().await.context("load_client_configs() failed")?; |
| assert_eq!(empty_map, stored_leases); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::Delete { client_id: id }, |
| DataStoreAction::LoadClientConfigs |
| ] if *id == client_id |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_server_dispatcher_validate_params() -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| let () = server.pool.available_addrs.clear(); |
| assert_eq!(server.try_validate_parameters(), Err(Status::INVALID_ARGS)); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_set_address_pool_fails_if_leases_present() -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| server.cache.insert( |
| ClientIdentifier::from(MacAddr { octets: [1, 2, 3, 4, 5, 6] }), |
| CachedConfig::default(), |
| ); |
| assert_eq!( |
| server.dispatch_set_parameter(fidl_fuchsia_net_dhcp::Parameter::AddressPool( |
| fidl_fuchsia_net_dhcp::AddressPool { |
| network_id: Some(fidl_ip_v4!("192.168.0.0")), |
| broadcast: Some(fidl_ip_v4!("192.168.0.255")), |
| mask: Some(fidl_ip_v4!("255.255.255.0")), |
| pool_range_start: Some(fidl_ip_v4!("192.168.0.2")), |
| pool_range_stop: Some(fidl_ip_v4!("192.168.0.254")), |
| ..fidl_fuchsia_net_dhcp::AddressPool::EMPTY |
| } |
| )), |
| Err(Status::BAD_STATE) |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_set_address_pool_updates_internal_pool() -> Result<(), Error> { |
| let mut server = new_test_minimal_server()?; |
| let () = server.pool.available_addrs.clear(); |
| let () = server |
| .dispatch_set_parameter(fidl_fuchsia_net_dhcp::Parameter::AddressPool( |
| fidl_fuchsia_net_dhcp::AddressPool { |
| network_id: Some(fidl_ip_v4!("192.168.0.0")), |
| broadcast: Some(fidl_ip_v4!("192.168.0.255")), |
| mask: Some(fidl_ip_v4!("255.255.255.0")), |
| pool_range_start: Some(fidl_ip_v4!("192.168.0.2")), |
| pool_range_stop: Some(fidl_ip_v4!("192.168.0.5")), |
| ..fidl_fuchsia_net_dhcp::AddressPool::EMPTY |
| }, |
| )) |
| .context("failed to set parameter")?; |
| assert_eq!(server.pool.available_addrs.len(), 3); |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [DataStoreAction::StoreParameters { params }] if *params == server.params |
| ); |
| Ok(()) |
| } |
| |
| #[fuchsia_async::run_singlethreaded(test)] |
| async fn test_recovery_from_expired_persistent_config() -> Result<(), Error> { |
| let client_ip = net_declare::std::ip_v4!("192.168.0.1"); |
| let mut time_source = TestSystemTime::with_current_time(); |
| const LEASE_EXPIRATION_SECONDS: u32 = 60; |
| // The previous server has stored a stale client config. |
| let store = ActionRecordingDataStore::new(); |
| let client_id = ClientIdentifier::from(random_mac_generator()); |
| let client_config = CachedConfig::new( |
| Some(client_ip), |
| Vec::new(), |
| time_source.now(), |
| LEASE_EXPIRATION_SECONDS, |
| )?; |
| let () = store.store_client_config(&client_id, &client_config)?; |
| // The config should become expired now. |
| let () = time_source.move_forward(Duration::from_secs(LEASE_EXPIRATION_SECONDS.into())); |
| |
| // Only 192.168.0.1 is available. |
| let params = ServerParameters { |
| server_ips: Vec::new(), |
| lease_length: LeaseLength { |
| default_seconds: 60 * 60 * 24, |
| max_seconds: 60 * 60 * 24 * 7, |
| }, |
| managed_addrs: ManagedAddresses { |
| network_id: net_declare::std::ip_v4!("192.168.0.0"), |
| broadcast: net_declare::std::ip_v4!("192.168.0.255"), |
| mask: SubnetMask::try_from(24)?, |
| pool_range_start: client_ip, |
| pool_range_stop: net_declare::std::ip_v4!("192.168.0.2"), |
| }, |
| permitted_macs: PermittedMacs(Vec::new()), |
| static_assignments: StaticAssignments(HashMap::new()), |
| arp_probe: false, |
| bound_device_names: Vec::new(), |
| }; |
| |
| // The server should recover to a consistent state on the next start. |
| let cache: HashMap<_, _> = |
| Some((client_id.clone(), client_config.clone())).into_iter().collect(); |
| let mut server: Server = |
| Server::new_with_time_source(store, params, HashMap::new(), cache, time_source)?; |
| assert!(server.cache.is_empty()); |
| assert!(server.pool.allocated_addrs.is_empty()); |
| |
| matches::assert_matches!( |
| &server.pool.available_addrs.into_iter().collect::<Vec<_>>()[..], |
| [addr] if *addr == client_ip |
| ); |
| |
| matches::assert_matches!( |
| server.store.actions().as_slice(), |
| [ |
| DataStoreAction::StoreClientConfig{ client_id: id1, client_config: config }, |
| DataStoreAction::Delete{ client_id: id2 }, |
| ] if id1 == id2 && id2 == &client_id && config == &client_config |
| ); |
| Ok(()) |
| } |
| } |