toe_beans/v4/server/
ip_pool.rs1use super::Config;
2use crate::v4::error::Result;
3use ipnetwork::Ipv4Network;
4use log::{debug, info, warn};
5use mac_address::MacAddress;
6use serde::{Deserialize, Serialize};
7use std::fs::read_to_string;
8use std::io::Write;
9use std::os::unix::fs::OpenOptionsExt;
10use std::path::PathBuf;
11use std::time::{Duration, SystemTime};
12use std::{
13 collections::{HashMap, HashSet},
14 fs::OpenOptions,
15 net::Ipv4Addr,
16};
17
18#[derive(Deserialize, Serialize, Clone)]
19enum LeaseStatus {
20 Offered,
21 Acked,
22}
23
24#[derive(Deserialize, Serialize, Clone)]
25struct Lease {
26 ip: Ipv4Addr,
27 status: LeaseStatus,
28 when: SystemTime,
30}
31
32impl Lease {
33 fn is_expired(&self, lease_time: u32) -> Result<bool> {
34 match self.when.elapsed() {
35 Ok(elapsed) => Ok(elapsed >= Duration::from_secs(lease_time as u64)),
36 Err(_) => Err("Problem getting elapsed system time"),
37 }
38 }
39}
40
41#[derive(Deserialize, Serialize)]
44pub struct IpPool {
45 #[serde(skip)]
46 available: HashSet<Ipv4Addr>,
47 leased: HashMap<MacAddress, Lease>,
48 static_leases: HashMap<MacAddress, Ipv4Addr>,
51 network: Ipv4Network,
53 lease_time: u32,
55 #[serde(skip)]
56 file_path: PathBuf,
58}
59
60impl IpPool {
61 pub const FILE_NAME: &'static str = "toe-beans-leases.toml";
63
64 pub fn new(config: &Config) -> Self {
66 let network = config.network_cidr;
67 let size = network.size() as usize;
68 let mut available = HashSet::with_capacity(size);
69 available.extend(network.iter());
70 let leased = HashMap::with_capacity(size);
71 let static_leases = HashMap::new();
72
73 Self {
74 file_path: config.path.clone(),
75 available,
76 leased,
77 static_leases,
78 network,
79 lease_time: config.lease_time.as_server(),
80 }
81 }
82
83 pub fn restore(config: &Config) -> Result<Self> {
89 let network = config.network_cidr;
90 let read_result = read_to_string(config.path.join(Self::FILE_NAME));
91
92 let mut ip_pool: IpPool = match read_result {
93 Ok(toml_string) => {
94 let toml_result = toml::from_str(&toml_string);
95 match toml_result {
96 Ok(ip_pool) => ip_pool,
97 Err(_) => return Err("Problem parsing leases file"),
98 }
99 }
100 Err(_) => {
101 return Err("Problem reading leases file");
102 }
103 };
104
105 if network != ip_pool.network {
106 return Err(
107 "The restored and configured IP address pool's network range/capacity do not match",
108 );
109 }
110
111 ip_pool.static_leases.values().try_for_each(|static_ip| {
112 if !network.contains(*static_ip) {
113 return Err("The configured network range does not include the static ip");
114 }
115
116 Ok(())
117 })?;
118
119 ip_pool.available.extend(network.iter());
120
121 info!("Restored IP address leases");
124 Ok(ip_pool)
125 }
126
127 pub fn restore_or_new(config: &Config) -> Self {
130 IpPool::restore(config).unwrap_or_else(|_| IpPool::new(config))
131 }
132
133 fn commit(&self) -> Result<()> {
135 debug!("Writing {}", Self::FILE_NAME);
136
137 let file_content = match toml::to_string_pretty(&self) {
138 Ok(content) => content,
139 Err(_) => return Err("Failed to generate toml data"),
140 };
141
142 let open_result = OpenOptions::new()
143 .read(false)
144 .write(true)
145 .create(true)
146 .truncate(true)
147 .mode(0o644) .open(self.file_path.join(Self::FILE_NAME));
149
150 let mut file = match open_result {
151 Ok(file) => file,
152 Err(_) => return Err("Failed to open file for writing"),
153 };
154
155 match file.write_all(file_content.as_bytes()) {
156 Ok(_) => Ok(()),
157 Err(_) => Err("Failed to write to file"),
158 }
159 }
160
161 pub fn offer(&mut self, owner: MacAddress, requested_ip: Option<Ipv4Addr>) -> Result<Ipv4Addr> {
172 if let Some(lease) = self.leased.get(&owner) {
173 match lease.status {
174 LeaseStatus::Offered => {
175 warn!(
176 "{} will be offered an IP address that it has already been offered",
177 owner
178 );
179 return Ok(lease.ip);
180 }
181 LeaseStatus::Acked => {
182 if !lease.is_expired(self.lease_time)? {
183 return Err("This device has already been leased a non-expired IP address");
184 }
185 }
186 }
187 }
188
189 if let Some(requested_ip) = requested_ip {
190 let ip_in_pool = self.network.contains(requested_ip);
191 let not_available = !self.available.contains(&requested_ip);
192 let has_static_lease = self.static_leases.contains_key(&owner);
193
194 if !ip_in_pool {
195 debug!("Requested IP Address is not in network range");
196 } else if not_available {
197 debug!("Requested IP Address is not available");
198 } else if has_static_lease {
199 debug!("Requested IP Address ignored because owner has static lease");
200 } else {
201 self.available.remove(&requested_ip);
202 self.leased.insert(
203 owner,
204 Lease {
205 ip: requested_ip,
206 status: LeaseStatus::Offered,
207 when: SystemTime::now(),
208 },
209 );
210 return Ok(requested_ip);
211 }
212 }
213
214 let ip = self.find_available_ip(&owner)?;
216 self.leased.insert(
217 owner,
218 Lease {
219 ip,
220 status: LeaseStatus::Offered,
221 when: SystemTime::now(),
222 },
223 );
224 Ok(ip)
225 }
226
227 pub fn ack(&mut self, owner: MacAddress) -> Result<Ipv4Addr> {
232 let maybe_leased = self.leased.get_mut(&owner);
233 let ip = match maybe_leased {
234 Some(leased) => {
235 match leased.status {
236 LeaseStatus::Offered => {
237 leased.status = LeaseStatus::Acked;
238 leased.when = SystemTime::now();
239 leased.ip
240 }
241 LeaseStatus::Acked => {
242 if !leased.is_expired(self.lease_time)? {
243 return Err(
244 "This device has already been leased a non-expired IP address",
245 );
246 }
247
248 leased.ip
251 }
252 }
253 }
254 None => {
255 let ip = self.find_available_ip(&owner)?;
257 self.leased.insert(
258 owner,
259 Lease {
260 ip,
261 status: LeaseStatus::Acked,
262 when: SystemTime::now(),
263 },
264 );
265 ip
266 }
267 };
268
269 #[cfg(not(feature = "benchmark"))]
270 self.commit()?;
271
272 Ok(ip)
273 }
274
275 pub fn extend(&mut self, owner: MacAddress) -> Result<()> {
277 match self.leased.get_mut(&owner) {
278 Some(lease) => {
279 lease.when = SystemTime::now();
280 Ok(())
281 }
282 None => Err("No IP address lease found with that owner"),
283 }
284 }
285
286 pub fn release(&mut self, owner: MacAddress) -> Result<()> {
288 let maybe_leased = self.leased.remove(&owner);
289
290 match maybe_leased {
291 Some(lease) => {
292 self.available.insert(lease.ip);
293 Ok(())
294 }
295 None => Err("No IP address lease found with that owner"),
296 }
297 }
298
299 fn find_available_ip(&mut self, owner: &MacAddress) -> Result<Ipv4Addr> {
304 if let Some(ip) = self.static_leases.get(owner) {
305 return match self.available.take(ip) {
306 Some(ip) => Ok(ip),
307 None => Err("A static lease was found but is not available"),
308 };
309 }
310
311 if let Some(any) = self.available.iter().next().cloned() {
312 debug!("Chose available IP address {any}");
313 return Ok(self.available.take(&any).unwrap());
315 }
316
317 let offer_timeout: u32 = 300;
319
320 let maybe_expired_lease = self
321 .leased
322 .clone() .into_iter() .find(|(_owner, lease)| match lease.status {
325 LeaseStatus::Offered => lease.is_expired(offer_timeout).unwrap_or(false),
329 LeaseStatus::Acked => lease.is_expired(self.lease_time).unwrap_or(false),
330 });
331
332 if let Some(expired_lease) = maybe_expired_lease {
333 debug!("Reusing expired lease's IP address");
334 return Ok(self.leased.remove(&expired_lease.0).unwrap().ip);
336 }
337
338 Err("No more IP addresses available")
339 }
340
341 pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
343 self.available.contains(ip)
344 }
345
346 pub fn verify_lease(&self, owner: MacAddress, ip: &Ipv4Addr) -> Result<()> {
349 let maybe_leased = self.leased.get(&owner);
350
351 match maybe_leased {
352 Some(lease) => {
353 if let LeaseStatus::Offered = lease.status {
354 return Err("Lease not acked");
355 }
356
357 if &lease.ip != ip {
358 return Err("Client's notion of ip address is wrong");
359 }
360
361 if lease.is_expired(self.lease_time)? {
362 return Err("Lease has expired");
363 }
364
365 Ok(())
366 }
367 None => Err("No IP address lease found with that owner"),
368 }
369 }
370}