| // 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::{ClientConfig, ServerConfig}; |
| use crate::protocol::{self, ConfigOption, Message, MessageType, OpCode, OptionCode}; |
| use byteorder::{BigEndian, ByteOrder}; |
| use fidl_fuchsia_hardware_ethernet_ext::MacAddress as MacAddr; |
| use std::cmp; |
| use std::collections::{BTreeSet, HashMap, HashSet}; |
| use std::net::Ipv4Addr; |
| use std::ops::Fn; |
| |
| /// A minimal DHCP server. |
| /// |
| /// This comment will be expanded upon in future CLs as the server design |
| /// is iterated upon. |
| pub struct Server<F> |
| where |
| F: Fn() -> i64, |
| { |
| cache: CachedClients, |
| pool: AddressPool, |
| config: ServerConfig, |
| time_provider: F, |
| } |
| |
| impl<F> Server<F> |
| where |
| F: Fn() -> i64, |
| { |
| /// Returns an initialized `Server` value. |
| pub fn new(time_provider: F) -> Server<F> { |
| Server { |
| cache: HashMap::new(), |
| pool: AddressPool::new(), |
| config: ServerConfig::new(), |
| time_provider: time_provider, |
| } |
| } |
| |
| /// Instantiates a `Server` value from the provided `ServerConfig`. |
| pub fn from_config(config: ServerConfig, time_provider: F) -> Server<F> { |
| let mut server = Server { |
| cache: HashMap::new(), |
| pool: AddressPool::new(), |
| config: config, |
| time_provider: time_provider, |
| }; |
| server.pool.load_pool(&server.config.managed_addrs); |
| server |
| } |
| |
| /// 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 a response message. If the incoming message is invalid, or the server is |
| /// unable to serve the request, then `dispatch()` will return `None`. |
| pub fn dispatch(&mut self, msg: Message) -> Option<Message> { |
| match msg.get_dhcp_type() { |
| Some(MessageType::DHCPDISCOVER) => self.handle_discover(msg), |
| Some(MessageType::DHCPOFFER) => None, |
| Some(MessageType::DHCPREQUEST) => self.handle_request(msg), |
| Some(MessageType::DHCPDECLINE) => self.handle_decline(msg), |
| Some(MessageType::DHCPACK) => None, |
| Some(MessageType::DHCPNAK) => None, |
| Some(MessageType::DHCPRELEASE) => self.handle_release(msg), |
| Some(MessageType::DHCPINFORM) => self.handle_inform(msg), |
| None => None, |
| } |
| } |
| |
| fn handle_discover(&mut self, disc: Message) -> Option<Message> { |
| let client_config = self.client_config(&disc); |
| let offered_ip = self.get_addr(&disc)?; |
| let mut offer = build_offer(disc, &self.config, &client_config); |
| offer.yiaddr = offered_ip; |
| self.update_server_cache( |
| Ipv4Addr::from(offer.yiaddr), |
| offer.chaddr, |
| vec![], |
| &client_config, |
| ); |
| |
| Some(offer) |
| } |
| |
| fn get_addr(&mut self, client: &Message) -> Option<Ipv4Addr> { |
| if let Some(config) = self.cache.get(&client.chaddr) { |
| if !config.expired((self.time_provider)()) { |
| // Free cached address so that it can be reallocated to same client. |
| self.pool.free_addr(config.client_addr); |
| return Some(config.client_addr); |
| } else if self.pool.addr_is_available(config.client_addr) { |
| return Some(config.client_addr); |
| } |
| } |
| if let Some(opt) = client.get_config_option(OptionCode::RequestedIpAddr) { |
| if opt.value.len() >= 4 { |
| let requested_addr = protocol::ip_addr_from_buf_at(&opt.value, 0) |
| .expect("out of range indexing on opt.value"); |
| if self.pool.addr_is_available(requested_addr) { |
| return Some(requested_addr); |
| } |
| } |
| } |
| self.pool.get_next_available_addr() |
| } |
| |
| fn update_server_cache( |
| &mut self, client_addr: Ipv4Addr, client_mac: MacAddr, client_opts: Vec<ConfigOption>, |
| client_config: &ClientConfig, |
| ) { |
| let config = CachedConfig { |
| client_addr: client_addr, |
| options: client_opts, |
| expiration: (self.time_provider)() + client_config.lease_time_s as i64, |
| }; |
| self.cache.insert(client_mac, config); |
| self.pool.allocate_addr(client_addr); |
| } |
| |
| fn handle_request(&mut self, req: Message) -> Option<Message> { |
| match get_client_state(&req) { |
| ClientState::Selecting => self.handle_request_selecting(req), |
| ClientState::InitReboot => self.handle_request_init_reboot(req), |
| ClientState::Renewing => self.handle_request_renewing(req), |
| ClientState::Unknown => None, |
| } |
| } |
| |
| fn handle_request_selecting(&mut self, req: Message) -> Option<Message> { |
| let requested_ip = req.ciaddr; |
| if !is_recipient(self.config.server_ip, &req) || !self.is_assigned(&req, requested_ip) { |
| return None; |
| } |
| Some(build_ack(req, requested_ip, &self.config)) |
| } |
| |
| fn is_assigned(&self, req: &Message, requested_ip: Ipv4Addr) -> bool { |
| if let Some(client_config) = self.cache.get(&req.chaddr) { |
| client_config.client_addr == requested_ip |
| && !client_config.expired((self.time_provider)()) |
| && self.pool.addr_is_allocated(requested_ip) |
| } else { |
| false |
| } |
| } |
| |
| fn handle_request_init_reboot(&mut self, req: Message) -> Option<Message> { |
| let requested_ip = get_requested_ip_addr(&req)?; |
| if !is_in_subnet(requested_ip, &self.config) { |
| return Some(build_nak(req, &self.config)); |
| } |
| if !is_client_mac_known(req.chaddr, &self.cache) { |
| return None; |
| } |
| if !self.is_assigned(&req, requested_ip) { |
| return Some(build_nak(req, &self.config)); |
| } |
| Some(build_ack(req, requested_ip, &self.config)) |
| } |
| |
| fn handle_request_renewing(&mut self, req: Message) -> Option<Message> { |
| let client_ip = req.ciaddr; |
| if !self.is_assigned(&req, client_ip) { |
| return None; |
| } |
| Some(build_ack(req, client_ip, &self.config)) |
| } |
| |
| fn handle_decline(&mut self, dec: Message) -> Option<Message> { |
| let declined_ip = get_requested_ip_addr(&dec)?; |
| if is_recipient(self.config.server_ip, &dec) && !self.is_assigned(&dec, declined_ip) { |
| self.pool.allocate_addr(declined_ip); |
| } |
| self.cache.remove(&dec.chaddr); |
| None |
| } |
| |
| fn handle_release(&mut self, rel: Message) -> Option<Message> { |
| if self.cache.contains_key(&rel.chaddr) { |
| self.pool.free_addr(rel.ciaddr); |
| } |
| None |
| } |
| |
| fn handle_inform(&mut self, inf: Message) -> Option<Message> { |
| // When responding to an INFORM, the server must leave yiaddr zeroed. |
| let yiaddr = Ipv4Addr::new(0, 0, 0, 0); |
| let mut ack = build_ack(inf, yiaddr, &self.config); |
| ack.options.clear(); |
| add_inform_ack_options(&mut ack, &self.config); |
| Some(ack) |
| } |
| |
| /// 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) { |
| let now = (self.time_provider)(); |
| let expired_clients: Vec<(MacAddr, Ipv4Addr)> = self |
| .cache |
| .iter() |
| .filter(|(_mac, config)| config.expired(now)) |
| .map(|(mac, config)| (*mac, config.client_addr)) |
| .collect(); |
| // Expired client entries must be removed in a separate statement because otherwise we |
| // would be attempting to change a cache as we iterate over it. |
| expired_clients.iter().for_each(|(mac, ip)| { |
| self.pool.free_addr(*ip); |
| self.cache.remove(mac); |
| }); |
| } |
| |
| pub fn client_config(&self, client_message: &Message) -> ClientConfig { |
| let requested_config = client_message.parse_to_config(); |
| ClientConfig { |
| lease_time_s: match requested_config.lease_time_s { |
| None => self.config.default_lease_time, |
| Some(t) => cmp::min(t, self.config.max_lease_time_s), |
| }, |
| } |
| } |
| } |
| |
| /// 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. |
| type CachedClients = HashMap<MacAddr, CachedConfig>; |
| |
| #[derive(Clone, Debug, PartialEq)] |
| struct CachedConfig { |
| client_addr: Ipv4Addr, |
| options: Vec<ConfigOption>, |
| expiration: i64, |
| } |
| |
| impl Default for CachedConfig { |
| fn default() -> Self { |
| CachedConfig { |
| client_addr: Ipv4Addr::new(0, 0, 0, 0), |
| options: vec![], |
| expiration: std::i64::MAX, |
| } |
| } |
| } |
| |
| impl CachedConfig { |
| fn expired(&self, now: i64) -> bool { |
| self.expiration <= now |
| } |
| } |
| |
| /// 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>, |
| } |
| |
| impl AddressPool { |
| fn new() -> Self { |
| AddressPool { |
| available_addrs: BTreeSet::new(), |
| allocated_addrs: HashSet::new(), |
| } |
| } |
| |
| fn load_pool(&mut self, addrs: &[Ipv4Addr]) { |
| for addr in addrs { |
| if !self.allocated_addrs.contains(&addr) { |
| self.available_addrs.insert(*addr); |
| } |
| } |
| } |
| |
| fn get_next_available_addr(&self) -> Option<Ipv4Addr> { |
| let mut iter = self.available_addrs.iter(); |
| match iter.next() { |
| Some(addr) => Some(*addr), |
| None => None, |
| } |
| } |
| |
| fn allocate_addr(&mut self, addr: Ipv4Addr) { |
| if self.available_addrs.remove(&addr) { |
| self.allocated_addrs.insert(addr); |
| } else { |
| panic!("Invalid Server State: attempted to allocate unavailable address"); |
| } |
| } |
| |
| fn free_addr(&mut self, addr: Ipv4Addr) { |
| if self.allocated_addrs.remove(&addr) { |
| self.available_addrs.insert(addr); |
| } else { |
| panic!("Invalid Server State: attempted to free unallocated address"); |
| } |
| } |
| |
| 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) |
| } |
| } |
| |
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
| enum ClientState { |
| Unknown, |
| Selecting, |
| InitReboot, |
| Renewing, |
| } |
| |
| fn build_offer(client: Message, config: &ServerConfig, client_config: &ClientConfig) -> Message { |
| let mut offer = client; |
| offer.op = OpCode::BOOTREPLY; |
| offer.secs = 0; |
| offer.ciaddr = Ipv4Addr::new(0, 0, 0, 0); |
| offer.siaddr = Ipv4Addr::new(0, 0, 0, 0); |
| offer.sname = String::new(); |
| offer.file = String::new(); |
| add_required_options(&mut offer, config, client_config, MessageType::DHCPOFFER); |
| add_recommended_options(&mut offer, config); |
| |
| offer |
| } |
| |
| fn add_required_options( |
| msg: &mut Message, config: &ServerConfig, client_config: &ClientConfig, msg_type: MessageType, |
| ) { |
| msg.options.clear(); |
| let mut lease = vec![0; 4]; |
| BigEndian::write_u32(&mut lease, client_config.lease_time_s); |
| msg.options.push(ConfigOption { |
| code: OptionCode::IpAddrLeaseTime, |
| value: lease, |
| }); |
| msg.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![msg_type.into()], |
| }); |
| msg.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: config.server_ip.octets().to_vec(), |
| }); |
| } |
| |
| fn add_recommended_options(msg: &mut Message, config: &ServerConfig) { |
| msg.options.push(ConfigOption { |
| code: OptionCode::Router, |
| value: ip_vec_to_bytes(&config.routers), |
| }); |
| msg.options.push(ConfigOption { |
| code: OptionCode::NameServer, |
| value: ip_vec_to_bytes(&config.name_servers), |
| }); |
| let mut renewal_time = vec![0, 0, 0, 0]; |
| BigEndian::write_u32(&mut renewal_time, config.default_lease_time / 2); |
| msg.options.push(ConfigOption { |
| code: OptionCode::RenewalTime, |
| value: renewal_time, |
| }); |
| let mut rebinding_time = vec![0, 0, 0, 0]; |
| BigEndian::write_u32(&mut rebinding_time, config.default_lease_time / 4); |
| msg.options.push(ConfigOption { |
| code: OptionCode::RebindingTime, |
| value: rebinding_time, |
| }); |
| } |
| |
| fn add_inform_ack_options(msg: &mut Message, config: &ServerConfig) { |
| msg.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPINFORM.into()], |
| }); |
| msg.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: config.server_ip.octets().to_vec(), |
| }); |
| msg.options.push(ConfigOption { |
| code: OptionCode::Router, |
| value: ip_vec_to_bytes(&config.routers), |
| }); |
| msg.options.push(ConfigOption { |
| code: OptionCode::NameServer, |
| value: ip_vec_to_bytes(&config.name_servers), |
| }); |
| } |
| |
| fn ip_vec_to_bytes<'a, T>(ips: T) -> Vec<u8> |
| where |
| T: IntoIterator<Item = &'a Ipv4Addr>, |
| { |
| ips.into_iter() |
| .flat_map(|ip| ip.octets().to_vec()) |
| .collect() |
| } |
| |
| fn is_recipient(server_ip: Ipv4Addr, req: &Message) -> bool { |
| if let Some(server_id) = get_server_id_from(&req) { |
| return server_id == server_ip; |
| } |
| false |
| } |
| |
| fn build_ack(req: Message, requested_ip: Ipv4Addr, config: &ServerConfig) -> Message { |
| let mut ack = req; |
| ack.op = OpCode::BOOTREPLY; |
| ack.secs = 0; |
| ack.yiaddr = requested_ip; |
| ack.options.clear(); |
| add_required_options( |
| &mut ack, |
| config, |
| &ClientConfig::new(config.default_lease_time), |
| MessageType::DHCPACK, |
| ); |
| add_recommended_options(&mut ack, config); |
| |
| ack |
| } |
| |
| fn is_in_subnet(ip: Ipv4Addr, config: &ServerConfig) -> bool { |
| apply_subnet_mask_to(config.subnet_mask, ip) |
| == apply_subnet_mask_to(config.subnet_mask, config.server_ip) |
| } |
| |
| fn is_client_mac_known(mac: MacAddr, cache: &CachedClients) -> bool { |
| cache.get(&mac).is_some() |
| } |
| |
| fn build_nak(req: Message, config: &ServerConfig) -> Message { |
| let mut nak = req; |
| nak.op = OpCode::BOOTREPLY; |
| nak.secs = 0; |
| nak.ciaddr = Ipv4Addr::new(0, 0, 0, 0); |
| nak.yiaddr = Ipv4Addr::new(0, 0, 0, 0); |
| nak.siaddr = Ipv4Addr::new(0, 0, 0, 0); |
| nak.options.clear(); |
| let mut lease = vec![0; 4]; |
| BigEndian::write_u32(&mut lease, config.default_lease_time); |
| nak.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPNAK.into()], |
| }); |
| nak.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: config.server_ip.octets().to_vec(), |
| }); |
| |
| nak |
| } |
| |
| fn get_client_state(msg: &Message) -> ClientState { |
| let maybe_server_id = get_server_id_from(&msg); |
| let maybe_requested_ip = get_requested_ip_addr(&msg); |
| let zero_ciaddr = Ipv4Addr::new(0, 0, 0, 0); |
| |
| if maybe_server_id.is_some() && maybe_requested_ip.is_none() && msg.ciaddr != zero_ciaddr { |
| return ClientState::Selecting; |
| } else if maybe_requested_ip.is_some() && msg.ciaddr == zero_ciaddr { |
| return ClientState::InitReboot; |
| } else if msg.ciaddr != zero_ciaddr { |
| return ClientState::Renewing; |
| } else { |
| return ClientState::Unknown; |
| } |
| } |
| |
| fn get_requested_ip_addr(req: &Message) -> Option<Ipv4Addr> { |
| let req_ip_opt = req |
| .options |
| .iter() |
| .find(|opt| opt.code == OptionCode::RequestedIpAddr)?; |
| let raw_ip = BigEndian::read_u32(&req_ip_opt.value); |
| Some(Ipv4Addr::from(raw_ip)) |
| } |
| |
| fn get_server_id_from(req: &Message) -> Option<Ipv4Addr> { |
| let server_id_opt = req |
| .options |
| .iter() |
| .find(|opt| opt.code == OptionCode::ServerId)?; |
| let raw_server_id = BigEndian::read_u32(&server_id_opt.value); |
| Some(Ipv4Addr::from(raw_server_id)) |
| } |
| |
| fn apply_subnet_mask_to(prefix_len: u8, ip_addr: Ipv4Addr) -> Ipv4Addr { |
| assert!(prefix_len < 32); |
| let subnet_mask_bits = ::std::u32::MAX << (32 - prefix_len); |
| let ip_addr_bits = BigEndian::read_u32(&ip_addr.octets()); |
| Ipv4Addr::from(ip_addr_bits & subnet_mask_bits) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| |
| use super::*; |
| use crate::protocol::{ConfigOption, Message, MessageType, OpCode, OptionCode}; |
| use std::net::Ipv4Addr; |
| |
| fn new_test_server<F>(time_provider: F) -> Server<F> |
| where |
| F: Fn() -> i64, |
| { |
| let mut server = Server::new(time_provider); |
| server.config.server_ip = Ipv4Addr::new(192, 168, 1, 1); |
| server.config.default_lease_time = 100; |
| server.config.routers.push(Ipv4Addr::new(192, 168, 1, 1)); |
| server |
| .config |
| .name_servers |
| .extend_from_slice(&vec![Ipv4Addr::new(8, 8, 8, 8), Ipv4Addr::new(8, 8, 4, 4)]); |
| server |
| .pool |
| .available_addrs |
| .insert(Ipv4Addr::from([192, 168, 1, 2])); |
| server |
| } |
| |
| fn new_test_discover() -> Message { |
| let mut disc = Message::new(); |
| disc.xid = 42; |
| disc.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| disc.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPDISCOVER.into()], |
| }); |
| disc |
| } |
| |
| fn new_test_offer() -> Message { |
| let mut offer = Message::new(); |
| offer.op = OpCode::BOOTREPLY; |
| offer.xid = 42; |
| offer.yiaddr = Ipv4Addr::new(192, 168, 1, 2); |
| offer.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| offer.options.push(ConfigOption { |
| code: OptionCode::IpAddrLeaseTime, |
| value: vec![0, 0, 0, 100], |
| }); |
| offer.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPOFFER.into()], |
| }); |
| offer.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: vec![192, 168, 1, 1], |
| }); |
| offer.options.push(ConfigOption { |
| code: OptionCode::Router, |
| value: vec![192, 168, 1, 1], |
| }); |
| offer.options.push(ConfigOption { |
| code: OptionCode::NameServer, |
| value: vec![8, 8, 8, 8, 8, 8, 4, 4], |
| }); |
| offer.options.push(ConfigOption { |
| code: OptionCode::RenewalTime, |
| value: vec![0, 0, 0, 50], |
| }); |
| offer.options.push(ConfigOption { |
| code: OptionCode::RebindingTime, |
| value: vec![0, 0, 0, 25], |
| }); |
| offer |
| } |
| |
| fn new_test_request() -> Message { |
| let mut req = Message::new(); |
| req.xid = 42; |
| req.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| req.options.push(ConfigOption { |
| code: OptionCode::RequestedIpAddr, |
| value: vec![192, 168, 1, 2], |
| }); |
| req.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPREQUEST.into()], |
| }); |
| req.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: vec![192, 168, 1, 1], |
| }); |
| req |
| } |
| |
| fn new_test_ack() -> Message { |
| let mut ack = Message::new(); |
| ack.op = OpCode::BOOTREPLY; |
| ack.xid = 42; |
| ack.yiaddr = Ipv4Addr::new(192, 168, 1, 2); |
| ack.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| ack.options.push(ConfigOption { |
| code: OptionCode::IpAddrLeaseTime, |
| value: vec![0, 0, 0, 100], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPACK.into()], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: vec![192, 168, 1, 1], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::Router, |
| value: vec![192, 168, 1, 1], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::NameServer, |
| value: vec![8, 8, 8, 8, 8, 8, 4, 4], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::RenewalTime, |
| value: vec![0, 0, 0, 50], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::RebindingTime, |
| value: vec![0, 0, 0, 25], |
| }); |
| ack |
| } |
| |
| fn new_test_nak() -> Message { |
| let mut nak = Message::new(); |
| nak.op = OpCode::BOOTREPLY; |
| nak.xid = 42; |
| nak.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| nak.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPNAK.into()], |
| }); |
| nak.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: vec![192, 168, 1, 1], |
| }); |
| nak |
| } |
| |
| fn new_test_release() -> Message { |
| let mut release = Message::new(); |
| release.xid = 42; |
| release.ciaddr = Ipv4Addr::new(192, 168, 1, 2); |
| release.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| release.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPRELEASE.into()], |
| }); |
| release |
| } |
| |
| fn new_test_inform() -> Message { |
| let mut inform = Message::new(); |
| inform.xid = 42; |
| inform.ciaddr = Ipv4Addr::new(192, 168, 1, 2); |
| inform.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| inform.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPINFORM.into()], |
| }); |
| inform |
| } |
| |
| fn new_test_inform_ack() -> Message { |
| let mut ack = Message::new(); |
| ack.op = OpCode::BOOTREPLY; |
| ack.xid = 42; |
| ack.ciaddr = Ipv4Addr::new(192, 168, 1, 2); |
| ack.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| ack.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPINFORM.into()], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: vec![192, 168, 1, 1], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::Router, |
| value: vec![192, 168, 1, 1], |
| }); |
| ack.options.push(ConfigOption { |
| code: OptionCode::NameServer, |
| value: vec![8, 8, 8, 8, 8, 8, 4, 4], |
| }); |
| ack |
| } |
| |
| fn new_test_decline() -> Message { |
| let mut decline = Message::new(); |
| decline.xid = 42; |
| decline.chaddr = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| decline.options.push(ConfigOption { |
| code: OptionCode::DhcpMessageType, |
| value: vec![MessageType::DHCPDECLINE.into()], |
| }); |
| decline.options.push(ConfigOption { |
| code: OptionCode::RequestedIpAddr, |
| value: vec![192, 168, 1, 2], |
| }); |
| decline.options.push(ConfigOption { |
| code: OptionCode::ServerId, |
| value: vec![192, 168, 1, 1], |
| }); |
| decline |
| } |
| |
| #[test] |
| fn test_dispatch_with_discover_returns_correct_response() { |
| let disc = new_test_discover(); |
| |
| let mut server = new_test_server(|| 42); |
| let got = server.dispatch(disc).unwrap(); |
| |
| let want = new_test_offer(); |
| |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_discover_updates_server_state() { |
| let disc = new_test_discover(); |
| let mac_addr = disc.chaddr; |
| let mut server = new_test_server(|| 42); |
| let _ = server.dispatch(disc).unwrap(); |
| |
| assert_eq!(server.pool.available_addrs.len(), 0); |
| assert_eq!(server.pool.allocated_addrs.len(), 1); |
| assert_eq!(server.cache.len(), 1); |
| let want_config = server.cache.get(&mac_addr).unwrap(); |
| assert_eq!(want_config.client_addr, Ipv4Addr::new(192, 168, 1, 2)); |
| } |
| |
| #[test] |
| fn test_dispatch_with_discover_client_binding_returns_bound_addr() { |
| let disc = new_test_discover(); |
| let mut server = new_test_server(|| 42); |
| let mut client_config = CachedConfig::default(); |
| let client_addr = Ipv4Addr::new(192, 168, 1, 42); |
| client_config.client_addr = client_addr; |
| server.pool.allocated_addrs.insert(client_addr); |
| server.cache.insert(disc.chaddr, client_config); |
| |
| let got = server.dispatch(disc).unwrap(); |
| |
| let mut want = new_test_offer(); |
| want.yiaddr = Ipv4Addr::new(192, 168, 1, 42); |
| |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_discover_expired_client_binding_returns_available_old_addr() { |
| let disc = new_test_discover(); |
| let mut server = new_test_server(|| 42); |
| let mut client_config = CachedConfig::default(); |
| client_config.client_addr = Ipv4Addr::new(192, 168, 1, 42); |
| client_config.expiration = 0; |
| server.cache.insert(disc.chaddr, client_config); |
| server |
| .pool |
| .available_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 42)); |
| |
| let got = server.dispatch(disc).unwrap(); |
| |
| let mut want = new_test_offer(); |
| want.yiaddr = Ipv4Addr::new(192, 168, 1, 42); |
| |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_discover_unavailable_expired_client_binding_returns_new_addr() { |
| let disc = new_test_discover(); |
| let mut server = new_test_server(|| 42); |
| let mut client_config = CachedConfig::default(); |
| client_config.client_addr = Ipv4Addr::new(192, 168, 1, 42); |
| client_config.expiration = 0; |
| server.cache.insert(disc.chaddr, client_config); |
| server |
| .pool |
| .available_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 2)); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 42)); |
| |
| let got = server.dispatch(disc).unwrap(); |
| |
| let mut want = new_test_offer(); |
| want.yiaddr = Ipv4Addr::new(192, 168, 1, 2); |
| |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_discover_available_requested_addr_returns_requested_addr() { |
| let mut disc = new_test_discover(); |
| disc.options.push(ConfigOption { |
| code: OptionCode::RequestedIpAddr, |
| value: vec![192, 168, 1, 3], |
| }); |
| |
| let mut server = new_test_server(|| 42); |
| server |
| .pool |
| .available_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 2)); |
| server |
| .pool |
| .available_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 3)); |
| let got = server.dispatch(disc).unwrap(); |
| |
| let mut want = new_test_offer(); |
| want.yiaddr = Ipv4Addr::new(192, 168, 1, 3); |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_discover_unavailable_requested_addr_returns_next_addr() { |
| let mut disc = new_test_discover(); |
| disc.options.push(ConfigOption { |
| code: OptionCode::RequestedIpAddr, |
| value: vec![192, 168, 1, 42], |
| }); |
| |
| let mut server = new_test_server(|| 42); |
| server |
| .pool |
| .available_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 2)); |
| server |
| .pool |
| .available_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 3)); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 42)); |
| let got = server.dispatch(disc).unwrap(); |
| |
| let mut want = new_test_offer(); |
| want.yiaddr = Ipv4Addr::new(192, 168, 1, 2); |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_selecting_request_valid_selecting_request_returns_ack() { |
| let mut req = new_test_request(); |
| let requested_ip_addr = Ipv4Addr::new(192, 168, 1, 2); |
| req.ciaddr = requested_ip_addr; |
| req.options.remove(0); |
| |
| let mut server = new_test_server(|| 42); |
| server.cache.insert( |
| req.chaddr, |
| CachedConfig { |
| client_addr: requested_ip_addr, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server.pool.allocate_addr(requested_ip_addr); |
| let got = server.dispatch(req).unwrap(); |
| |
| let mut want = new_test_ack(); |
| want.ciaddr = requested_ip_addr; |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_selecting_request_no_address_allocation_to_client_returns_none() { |
| let mut req = new_test_request(); |
| req.ciaddr = Ipv4Addr::new(192, 168, 1, 2); |
| req.options.remove(0); |
| |
| let mut server = new_test_server(|| 42); |
| let got = server.dispatch(req); |
| |
| assert!(got.is_none()); |
| } |
| |
| #[test] |
| fn test_dispatch_with_selecting_request_wrong_server_id_returns_none() { |
| let mut req = new_test_request(); |
| let requested_ip_addr = Ipv4Addr::new(192, 168, 1, 2); |
| req.ciaddr = requested_ip_addr; |
| req.options.remove(0); |
| |
| let mut server = new_test_server(|| 42); |
| server.cache.insert( |
| req.chaddr, |
| CachedConfig { |
| client_addr: requested_ip_addr, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server.pool.allocate_addr(requested_ip_addr); |
| server.config.server_ip = Ipv4Addr::new(1, 2, 3, 4); |
| let got = server.dispatch(req); |
| |
| assert!(got.is_none()); |
| } |
| |
| #[test] |
| fn test_dispatch_with_selecting_request_valid_selecting_request_maintains_server_invariants() { |
| let requested_ip_addr = Ipv4Addr::new(192, 168, 1, 2); |
| let mut req = new_test_request(); |
| req.ciaddr = requested_ip_addr; |
| req.options.remove(0); |
| |
| let mut server = new_test_server(|| 42); |
| server.cache.insert( |
| req.chaddr, |
| CachedConfig { |
| client_addr: requested_ip_addr, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server.pool.allocate_addr(requested_ip_addr); |
| let _ = server.dispatch(req.clone()).unwrap(); |
| |
| assert!(server.cache.contains_key(&req.chaddr)); |
| assert!(server.pool.addr_is_allocated(requested_ip_addr)); |
| } |
| |
| #[test] |
| fn test_dispatch_with_selecting_request_no_address_allocation_maintains_server_invariants() { |
| let requested_ip_addr = Ipv4Addr::new(192, 168, 1, 2); |
| let mut req = new_test_request(); |
| req.ciaddr = requested_ip_addr; |
| req.options.remove(0); |
| |
| let mut server = new_test_server(|| 42); |
| let _ = server.dispatch(req.clone()); |
| |
| assert!(!server.cache.contains_key(&req.chaddr)); |
| assert!(!server.pool.addr_is_allocated(Ipv4Addr::new(192, 168, 1, 2))); |
| } |
| |
| #[test] |
| fn test_dispatch_with_init_boot_request_correct_address_returns_ack() { |
| let mut req = new_test_request(); |
| req.options.remove(2); |
| let requested_ip_addr = get_requested_ip_addr(&req).unwrap(); |
| |
| let mut server = new_test_server(|| 42); |
| server.cache.insert( |
| req.chaddr, |
| CachedConfig { |
| client_addr: requested_ip_addr, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server.pool.allocate_addr(requested_ip_addr); |
| let got = server.dispatch(req).unwrap(); |
| |
| let want = new_test_ack(); |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_init_boot_request_incorrect_address_returns_nak() { |
| let mut req = new_test_request(); |
| req.options.remove(0); |
| req.options.remove(1); |
| req.options.push(ConfigOption { |
| code: OptionCode::RequestedIpAddr, |
| value: vec![192, 168, 1, 42], |
| }); |
| |
| let mut server = new_test_server(|| 42); |
| let assigned_ip = Ipv4Addr::new(192, 168, 1, 2); |
| server.cache.insert( |
| req.chaddr, |
| CachedConfig { |
| client_addr: assigned_ip, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server.pool.allocate_addr(assigned_ip); |
| let got = server.dispatch(req).unwrap(); |
| |
| let want = new_test_nak(); |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_init_boot_request_unknown_client_returns_none() { |
| let mut req = new_test_request(); |
| req.options.remove(2); |
| |
| let mut server = new_test_server(|| 42); |
| let got = server.dispatch(req); |
| |
| assert!(got.is_none()); |
| } |
| |
| #[test] |
| fn test_dispatch_with_init_boot_request_client_on_wrong_subnet_returns_nak() { |
| let mut req = new_test_request(); |
| req.options.remove(0); |
| req.options.remove(1); |
| req.options.push(ConfigOption { |
| code: OptionCode::RequestedIpAddr, |
| value: vec![10, 0, 0, 1], |
| }); |
| |
| let mut server = new_test_server(|| 42); |
| let got = server.dispatch(req).unwrap(); |
| |
| let want = new_test_nak(); |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_renewing_request_valid_request_returns_ack() { |
| let mut req = new_test_request(); |
| req.options.remove(0); |
| req.options.remove(1); |
| let client_ip = Ipv4Addr::new(192, 168, 1, 2); |
| req.ciaddr = client_ip; |
| |
| let mut server = new_test_server(|| 42); |
| server.cache.insert( |
| req.chaddr, |
| CachedConfig { |
| client_addr: client_ip, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server.pool.allocate_addr(client_ip); |
| let got = server.dispatch(req).unwrap(); |
| |
| let mut want = new_test_ack(); |
| want.ciaddr = client_ip; |
| assert_eq!(got, want); |
| } |
| |
| #[test] |
| fn test_dispatch_with_renewing_request_unknown_client_returns_none() { |
| let mut req = new_test_request(); |
| req.options.remove(0); |
| req.options.remove(1); |
| let client_ip = Ipv4Addr::new(192, 168, 1, 2); |
| req.ciaddr = client_ip; |
| |
| let mut server = new_test_server(|| 42); |
| let got = server.dispatch(req); |
| |
| assert!(got.is_none()); |
| } |
| |
| #[test] |
| fn test_get_client_state_with_selecting_returns_selecting() { |
| let mut msg = new_test_request(); |
| msg.ciaddr = Ipv4Addr::new(192, 168, 1, 2); |
| msg.options.remove(0); |
| |
| let got = get_client_state(&msg); |
| |
| assert_eq!(got, ClientState::Selecting); |
| } |
| |
| #[test] |
| fn test_get_client_state_with_initreboot_returns_initreboot() { |
| let mut msg = new_test_request(); |
| msg.options.remove(2); |
| |
| let got = get_client_state(&msg); |
| |
| assert_eq!(got, ClientState::InitReboot); |
| } |
| |
| #[test] |
| fn test_get_client_state_with_renewing_returns_renewing() { |
| let mut msg = new_test_request(); |
| msg.options.remove(0); |
| msg.options.remove(1); |
| msg.ciaddr = Ipv4Addr::new(1, 2, 3, 4); |
| |
| let got = get_client_state(&msg); |
| |
| assert_eq!(got, ClientState::Renewing); |
| } |
| |
| #[test] |
| fn test_get_client_state_with_unknown_returns_unknown() { |
| let mut msg = new_test_request(); |
| msg.options.clear(); |
| |
| let got = get_client_state(&msg); |
| |
| assert_eq!(got, ClientState::Unknown); |
| } |
| |
| #[test] |
| fn test_release_expired_leases_with_none_expired_releases_none() { |
| let mut server = new_test_server(|| 42); |
| server.pool.available_addrs.clear(); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 2), |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 2)); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 3), |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 3)); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 4), |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 4)); |
| |
| server.release_expired_leases(); |
| |
| assert_eq!(server.cache.len(), 3); |
| assert_eq!(server.pool.available_addrs.len(), 0); |
| assert_eq!(server.pool.allocated_addrs.len(), 3); |
| } |
| |
| #[test] |
| fn test_release_expired_leases_with_all_expired_releases_all() { |
| let mut server = new_test_server(|| 42); |
| server.pool.available_addrs.clear(); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 2), |
| options: vec![], |
| expiration: 0, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 2)); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 3), |
| options: vec![], |
| expiration: 0, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 3)); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 4), |
| options: vec![], |
| expiration: 0, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 4)); |
| |
| server.release_expired_leases(); |
| |
| assert_eq!(server.cache.len(), 0); |
| assert_eq!(server.pool.available_addrs.len(), 3); |
| assert_eq!(server.pool.allocated_addrs.len(), 0); |
| } |
| |
| #[test] |
| fn test_release_expired_leases_with_some_expired_releases_expired() { |
| let mut server = new_test_server(|| 42); |
| server.pool.available_addrs.clear(); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 2), |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 2)); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 3), |
| options: vec![], |
| expiration: 0, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 3)); |
| server.cache.insert( |
| MacAddr { |
| octets: [0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC], |
| }, |
| CachedConfig { |
| client_addr: Ipv4Addr::new(192, 168, 1, 4), |
| options: vec![], |
| expiration: std::i64::MAX, |
| }, |
| ); |
| server |
| .pool |
| .allocated_addrs |
| .insert(Ipv4Addr::new(192, 168, 1, 4)); |
| |
| server.release_expired_leases(); |
| |
| assert_eq!(server.cache.len(), 2); |
| assert!(!server.cache.contains_key(&MacAddr { |
| octets: [0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB] |
| })); |
| assert_eq!(server.pool.available_addrs.len(), 1); |
| assert_eq!(server.pool.allocated_addrs.len(), 2); |
| } |
| |
| #[test] |
| fn test_dispatch_with_known_release() { |
| let release = new_test_release(); |
| let mut server = new_test_server(|| 42); |
| let client_ip = Ipv4Addr::new(192, 168, 1, 2); |
| server.pool.allocate_addr(client_ip); |
| let client_mac = MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }; |
| let client_config = CachedConfig { |
| client_addr: client_ip, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }; |
| server.cache.insert(client_mac, client_config.clone()); |
| |
| let got = server.dispatch(release); |
| |
| assert!(got.is_none(), "server returned a Message value"); |
| assert!( |
| !server.pool.addr_is_allocated(client_ip), |
| "server did not free client address" |
| ); |
| assert!( |
| server.pool.addr_is_available(client_ip), |
| "server did not free client address" |
| ); |
| assert!( |
| server.cache.contains_key(&client_mac), |
| "server did not retain cached client settings" |
| ); |
| assert_eq!( |
| server.cache.get(&client_mac).unwrap(), |
| &client_config, |
| "server did not retain cached client settings" |
| ); |
| } |
| |
| #[test] |
| fn test_dispatch_with_unknown_release() { |
| let release = new_test_release(); |
| let mut server = new_test_server(|| 42); |
| let client_ip = Ipv4Addr::new(192, 168, 1, 2); |
| server.pool.allocate_addr(client_ip); |
| let cached_mac = MacAddr { |
| octets: [0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA], |
| }; |
| let client_config = CachedConfig { |
| client_addr: client_ip, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }; |
| server.cache.insert(cached_mac, client_config.clone()); |
| |
| let got = server.dispatch(release); |
| |
| assert!(got.is_none(), "server returned a Message value"); |
| assert!( |
| server.pool.addr_is_allocated(client_ip), |
| "server did not free client address" |
| ); |
| assert!( |
| !server.pool.addr_is_available(client_ip), |
| "server did not free client address" |
| ); |
| assert!( |
| server.cache.contains_key(&cached_mac), |
| "server did not retain cached client settings" |
| ); |
| assert_eq!( |
| server.cache.get(&cached_mac).unwrap(), |
| &client_config, |
| "server did not retain cached client settings" |
| ); |
| } |
| |
| #[test] |
| fn test_dispatch_with_inform_returns_ack() { |
| let inform = new_test_inform(); |
| let mut server = new_test_server(|| 42); |
| |
| let got = server.dispatch(inform).unwrap(); |
| |
| let want = new_test_inform_ack(); |
| |
| assert_eq!(got, want, "expected: {:?}\ngot: {:?}", want, got); |
| } |
| |
| #[test] |
| fn test_dispatch_with_decline_marks_addr_allocated() { |
| let decline = new_test_decline(); |
| let mut server = new_test_server(|| 42); |
| let already_used_ip = Ipv4Addr::new(192, 168, 1, 2); |
| server.config.managed_addrs.push(already_used_ip); |
| let client_config = CachedConfig { |
| client_addr: already_used_ip, |
| options: vec![], |
| expiration: std::i64::MAX, |
| }; |
| server.cache.insert(decline.chaddr, client_config); |
| |
| let got = server.dispatch(decline); |
| |
| assert!(got.is_none(), "server returned a Message value"); |
| assert!( |
| !server.pool.addr_is_available(already_used_ip), |
| "addr still marked available" |
| ); |
| assert!( |
| server.pool.addr_is_allocated(already_used_ip), |
| "addr not marked allocated" |
| ); |
| assert!( |
| !server.cache.contains_key(&MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF] |
| }), |
| "client config retained" |
| ); |
| } |
| |
| #[test] |
| fn test_client_requested_lease_time() { |
| let mut disc = new_test_discover(); |
| disc.options.push(ConfigOption { |
| code: OptionCode::IpAddrLeaseTime, |
| value: vec![0, 0, 0, 20], |
| }); |
| |
| let mut server = new_test_server(|| 42); |
| let result = server.dispatch(disc).unwrap(); |
| assert_eq!( |
| BigEndian::read_u32( |
| &result |
| .get_config_option(OptionCode::IpAddrLeaseTime) |
| .unwrap() |
| .value |
| ), |
| 20 |
| ); |
| |
| let cached_config = server |
| .cache |
| .get(&MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }) |
| .unwrap(); |
| assert_eq!(cached_config.expiration, 42 + 20); |
| } |
| |
| #[test] |
| fn test_client_requested_lease_time_greater_than_max() { |
| let mut disc = new_test_discover(); |
| disc.options.push(ConfigOption { |
| code: OptionCode::IpAddrLeaseTime, |
| value: vec![0, 0, 0, 20], |
| }); |
| |
| let mut server = new_test_server(|| 42); |
| server.config.max_lease_time_s = 10; |
| let result = server.dispatch(disc).unwrap(); |
| assert_eq!( |
| BigEndian::read_u32( |
| &result |
| .get_config_option(OptionCode::IpAddrLeaseTime) |
| .unwrap() |
| .value |
| ), |
| 10 |
| ); |
| |
| let cached_config = server |
| .cache |
| .get(&MacAddr { |
| octets: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF], |
| }) |
| .unwrap(); |
| assert_eq!(cached_config.expiration, 42 + 10); |
| } |
| } |