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)]
19struct Lease {
20 ip: Ipv4Addr,
21 when: SystemTime,
23}
24
25#[derive(Deserialize, Serialize)]
28pub struct IpPool {
29 #[serde(skip)]
30 available: HashSet<Ipv4Addr>,
31 #[serde(skip)]
32 offered: HashMap<MacAddress, Lease>,
33 acked: HashMap<MacAddress, Lease>,
34 static_leases: HashMap<MacAddress, Ipv4Addr>,
37 network: Ipv4Network,
39 lease_time: u32,
41 #[serde(skip)]
42 file_path: PathBuf,
44}
45
46impl IpPool {
47 pub const FILE_NAME: &'static str = "toe-beans-leases.toml";
49
50 pub fn new(config: &Config) -> Self {
52 let network = config.network_cidr;
53 let size = network.size() as usize;
54 let mut available = HashSet::with_capacity(size);
55 available.extend(network.iter());
56 let offered = HashMap::with_capacity(size);
57 let acked = HashMap::with_capacity(size);
58 let static_leases = HashMap::new();
59
60 Self {
61 file_path: config.path.clone(),
62 available,
63 offered,
64 acked,
65 static_leases,
66 network,
67 lease_time: config.lease_time.as_server(),
68 }
69 }
70
71 pub fn restore(config: &Config) -> Result<Self> {
77 let network = config.network_cidr;
78 let read_result = read_to_string(config.path.join(Self::FILE_NAME));
79
80 let mut ip_pool: IpPool = match read_result {
81 Ok(toml_string) => {
82 let toml_result = toml::from_str(&toml_string);
83 match toml_result {
84 Ok(ip_pool) => ip_pool,
85 Err(_) => return Err("Problem parsing leases file"),
86 }
87 }
88 Err(_) => {
89 return Err("Problem reading leases file");
90 }
91 };
92
93 if network != ip_pool.network {
94 return Err(
95 "The restored and configured IP address pool's network range/capacity do not match",
96 );
97 }
98
99 ip_pool.static_leases.values().try_for_each(|static_ip| {
100 if !network.contains(*static_ip) {
101 return Err("The configured network range does not include the static ip");
102 }
103
104 Ok(())
105 })?;
106
107 ip_pool.available.extend(network.iter());
108
109 info!("Restored IP address leases");
112 Ok(ip_pool)
113 }
114
115 pub fn restore_or_new(config: &Config) -> Self {
118 IpPool::restore(config).unwrap_or_else(|_| IpPool::new(config))
119 }
120
121 fn commit(&self) -> Result<()> {
123 debug!("Writing {}", Self::FILE_NAME);
124
125 let file_content = match toml::to_string_pretty(&self) {
126 Ok(content) => content,
127 Err(_) => return Err("Failed to generate toml data"),
128 };
129
130 let open_result = OpenOptions::new()
131 .read(false)
132 .write(true)
133 .create(true)
134 .truncate(true)
135 .mode(0o644) .open(self.file_path.join(Self::FILE_NAME));
137
138 let mut file = match open_result {
139 Ok(file) => file,
140 Err(_) => return Err("Failed to open file for writing"),
141 };
142
143 match file.write_all(file_content.as_bytes()) {
144 Ok(_) => Ok(()),
145 Err(_) => Err("Failed to write to file"),
146 }
147 }
148
149 pub fn offer(&mut self, owner: MacAddress, requested_ip: Option<Ipv4Addr>) -> Result<Ipv4Addr> {
159 if self.acked.contains_key(&owner) && !self.is_expired(&owner)? {
160 return Err("This device already has already been leased a non-expired IP address");
161 }
162
163 if let Some(offered) = self.offered.get(&owner) {
164 warn!(
165 "{} will be offered an IP address that it has already been offered",
166 owner
167 );
168 return Ok(offered.ip);
169 }
170
171 if let Some(requested_ip) = requested_ip {
172 let ip_in_pool = self.network.contains(requested_ip);
173 let not_available = !self.available.contains(&requested_ip);
174 let has_static_lease = self.static_leases.contains_key(&owner);
175
176 if !ip_in_pool {
177 debug!("Requested IP Address is not in network range");
178 } else if not_available {
179 debug!("Requested IP Address is not available");
180 } else if has_static_lease {
181 debug!("Requested IP Address ignored because owner has static lease");
182 } else {
183 self.available.remove(&requested_ip);
184 self.offered.insert(
185 owner,
186 Lease {
187 ip: requested_ip,
188 when: SystemTime::now(),
189 },
190 );
191 return Ok(requested_ip);
192 }
193 }
194
195 let ip = self.find_available_ip(&owner)?;
197 self.offered.insert(
198 owner,
199 Lease {
200 ip,
201 when: SystemTime::now(),
202 },
203 );
204 Ok(ip)
205 }
206
207 pub fn ack(&mut self, owner: MacAddress) -> Result<Ipv4Addr> {
212 if self.acked.contains_key(&owner) && !self.is_expired(&owner)? {
213 return Err("This device already has already been leased a non-expired IP address");
214 }
215
216 let maybe_offered_ip = self.offered.remove(&owner);
217 let ip = match maybe_offered_ip {
218 Some(lease) => lease.ip,
219 None => {
220 self.find_available_ip(&owner)?
222 }
223 };
224
225 self.acked.insert(
226 owner,
227 Lease {
228 ip,
229 when: SystemTime::now(),
230 },
231 );
232
233 self.commit()?;
234
235 Ok(ip)
236 }
237
238 pub fn extend(&mut self, owner: MacAddress, ip: Ipv4Addr) -> Result<()> {
240 match self.acked.insert(
241 owner,
242 Lease {
243 ip,
244 when: SystemTime::now(),
245 },
246 ) {
247 Some(_) => Ok(()),
248 None => Err("No IP address lease found with that owner"),
249 }
250 }
251
252 pub fn unoffer(&mut self, owner: MacAddress) -> Result<()> {
254 let maybe_offered = self.offered.remove(&owner);
255
256 match maybe_offered {
257 Some(lease) => {
258 self.available.insert(lease.ip);
259 Ok(())
260 }
261 None => Err("No IP address offer found to unoffer"),
262 }
263 }
264
265 pub fn release(&mut self, owner: MacAddress) -> Result<()> {
268 let maybe_leased = self.acked.remove(&owner);
269
270 match maybe_leased {
271 Some(lease) => {
272 self.available.insert(lease.ip);
273 Ok(())
274 }
275 None => Err("No IP address lease found with that owner"),
276 }
277 }
278
279 fn find_available_ip(&mut self, owner: &MacAddress) -> Result<Ipv4Addr> {
284 if let Some(ip) = self.static_leases.get(owner) {
285 return match self.available.take(ip) {
286 Some(ip) => Ok(ip),
287 None => Err("A static lease was found but is not available"),
288 };
289 }
290
291 if let Some(any) = self.available.iter().next().cloned() {
292 debug!("Chose available IP address {any}");
293 return Ok(self.available.take(&any).unwrap());
295 }
296
297 let maybe_expired_offer = self
303 .offered
304 .clone() .into_iter() .find(|offer| {
307 let elapsed_result = offer.1.when.elapsed();
308 match elapsed_result {
309 Ok(elapsed) => elapsed > Duration::from_secs(300),
310 Err(_) => false,
311 }
312 });
313
314 if let Some(expired_offer) = maybe_expired_offer {
315 debug!("Found an expired offer's IP address");
316 return Ok(self.offered.remove(&expired_offer.0).unwrap().ip);
318 }
319
320 let maybe_expired_ack = self
322 .acked
323 .clone() .into_iter() .find(|ack| {
326 let elapsed_result = ack.1.when.elapsed();
327 match elapsed_result {
328 Ok(elapsed) => elapsed > Duration::from_secs(300),
329 Err(_) => false,
330 }
331 });
332
333 if let Some(expired_ack) = maybe_expired_ack {
334 debug!("Found an expired ack's IP address");
335 return Ok(self.acked.remove(&expired_ack.0).unwrap().ip);
337 }
338
339 Err("No more IP addresses available")
340 }
341
342 pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
344 self.available.contains(ip)
345 }
346
347 pub fn verify_lease(&self, owner: MacAddress, ip: &Ipv4Addr) -> Result<()> {
350 let maybe_leased = self.acked.get(&owner);
351
352 match maybe_leased {
353 Some(lease) => {
354 if &lease.ip != ip {
355 return Err("Client's notion of ip address is wrong");
356 }
357
358 if self.is_expired(&owner)? {
359 return Err("Lease has expired");
360 }
361
362 Ok(())
363 }
364 None => Err("No IP address lease found with that owner"),
365 }
366 }
367
368 pub fn is_expired(&self, owner: &MacAddress) -> Result<bool> {
370 let maybe_leased = self.acked.get(owner);
371
372 match maybe_leased {
373 Some(lease) => match lease.when.elapsed() {
374 Ok(elapsed) => Ok(elapsed >= Duration::from_secs(self.lease_time as u64)),
375 Err(_) => Err("Problem getting elapsed system time"),
376 },
377 None => Err("No IP address lease found with that owner"),
378 }
379 }
380}