blob: 1f736e286cbd496c142fd291c4b9ae25eca3bae9 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::configuration::{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);
}
}