toe_beans/v4/server/leases/
mod.rs1mod config;
2mod lease_time;
3
4pub use config::*;
5pub use lease_time::*;
6
7use crate::v4::error::Result;
8use ip_network::Ipv4Network;
9use jiff::Zoned;
10use log::{debug, error, info, warn};
11use mac_address::MacAddress;
12use std::fmt::Display;
13use std::fs::{File, OpenOptions};
14use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
15use std::str::FromStr;
16use std::{
17 collections::{HashMap, HashSet},
18 net::Ipv4Addr,
19};
20
21const PAGE_SIZE: usize = 200;
23
24#[derive(Clone, Debug)]
25enum LeaseStatus {
26 Offered,
27 Acked,
28}
29
30impl Display for LeaseStatus {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32 match self {
33 LeaseStatus::Offered => write!(f, "Offered"),
34 LeaseStatus::Acked => write!(f, "Acked"),
35 }
36 }
37}
38
39impl From<&str> for LeaseStatus {
40 fn from(value: &str) -> Self {
41 match value {
42 "Offered" => Self::Offered,
43 "Acked" => Self::Acked,
44 _ => panic!("Unknown value used for LeaseStatus"),
45 }
46 }
47}
48
49#[derive(Clone, Debug)]
50struct Lease {
51 line: u32,
54 ip: Ipv4Addr,
56 status: LeaseStatus,
57 is_static: bool,
58 when: Zoned,
60}
61
62impl Lease {
63 #[inline]
64 fn new(ip: Ipv4Addr, status: LeaseStatus, is_static: bool) -> Self {
65 Self {
66 line: 0,
67 ip,
68 status,
69 is_static,
70 when: Zoned::now(),
71 }
72 }
73
74 fn from_string(string: String, line: u32) -> Result<(MacAddress, Lease)> {
77 let mut parts = string.split(',');
78 let owner = MacAddress::from_str(parts.next().unwrap()).unwrap();
80 let ip = Ipv4Addr::from_str(parts.next().unwrap()).unwrap();
81 let status = LeaseStatus::from(parts.next().unwrap());
82 let is_static = parts.next().unwrap().parse().unwrap();
83
84 let when = Zoned::from_str(parts.next().unwrap().trim_end()).unwrap();
86
87 let lease = Self {
88 line,
89 ip,
90 status,
91 is_static,
92 when,
93 };
94 Ok((owner, lease))
95 }
96
97 #[inline]
99 fn extend(&mut self) {
100 self.when = Zoned::now();
101 }
102
103 fn is_expired(&self, lease_time: u32) -> bool {
106 let elapsed = self.when.duration_until(&Zoned::now());
107 elapsed.as_secs() >= lease_time as i64
108 }
109
110 fn to_string(&self, owner: MacAddress) -> String {
111 let mut string = String::with_capacity(PAGE_SIZE);
113
114 string.push_str(&owner.to_string());
115 string.push(',');
116 string.push_str(&self.ip.to_string()); string.push(',');
118 string.push_str(&self.status.to_string()); string.push(',');
120 string.push_str(&self.is_static.to_string()); string.push(',');
122 string.push_str(&self.when.to_string()); string
125 }
126}
127
128#[derive(Debug)]
134pub struct Leases {
135 available: HashSet<Ipv4Addr>,
136 leased: HashMap<MacAddress, Lease>,
137 lease_time: u32,
139 lines: u32,
141 config: LeaseConfig,
143 file: Option<File>,
148}
149
150impl Leases {
151 pub const FILE_NAME: &'static str = "toe-beans.leases";
153
154 const OFFER_TIMEOUT: u32 = 300;
161
162 #[inline]
165 pub fn get_config(&self) -> &LeaseConfig {
166 &self.config
167 }
168
169 fn empty(config: &LeaseConfig) -> Self {
170 let size = config.network_cidr.hosts().len();
171 Self {
172 available: HashSet::with_capacity(size),
173 leased: HashMap::with_capacity(size),
174 lease_time: config.lease_time.as_server(),
175 config: LeaseConfig::default(),
176 lines: 0,
177 file: None,
178 }
179 }
180
181 pub fn new(config: LeaseConfig) -> Self {
184 let file_path = config.leases_file_path.join(Self::FILE_NAME);
185
186 let mut leases = if config.use_leases_file {
187 let mut leases = Self::read_leases(&config).unwrap_or_else(|_| Self::empty(&config));
188
189 let file = OpenOptions::new()
190 .write(true)
191 .create(true)
192 .truncate(false)
193 .open(file_path)
194 .expect("Problem opening leases file for writing");
195 file.lock().expect("Failed to acquire lock on leases file");
196 leases.file = Some(file);
197
198 leases
199 } else {
200 Self::empty(&config)
201 };
202
203 leases.config = config;
204 leases.fill_available();
205
206 leases
207 }
208
209 fn read_leases(config: &LeaseConfig) -> Result<Leases> {
210 info!("Trying to read, parse, and validate leases file");
211
212 let full_leases_file_path = config.leases_file_path.join(Self::FILE_NAME);
213 let open_result = File::open(full_leases_file_path);
214 let leased: HashMap<MacAddress, Lease> = match open_result {
215 Ok(file) => {
216 let mut line_num = 0;
217 BufReader::new(file)
218 .lines()
219 .map(|line| {
220 let lease_string = line.expect("Problem reading leases file");
221 line_num += 1;
222 Lease::from_string(lease_string, line_num)
223 .expect("Problem parsing leases file")
224 })
225 .collect()
226 }
227 Err(message) => {
228 error!("Problem opening leases file: {}", message);
229 return Err("Problem opening leases file");
230 }
231 };
232
233 let mut leases = Leases::empty(config);
234 leases.leased = leased;
235
236 leases.validate(&config.network_cidr)?;
237
238 info!("Restored and validated leases file");
239
240 Ok(leases)
241 }
242
243 #[inline]
247 fn validate(&self, network: &Ipv4Network) -> Result<()> {
248 let broadcast_address = network.broadcast_address();
249 let network_address = network.network_address();
250 let static_lease_ips = self.config.static_leases.values();
251 let leased_ips = self.leased.values().map(|lease| &lease.ip);
252
253 static_lease_ips.chain(leased_ips).try_for_each(|ip| {
254 if !network.contains(*ip) {
255 return Err("The configured network range does not include the restored ip. Did the network range change?");
256 }
257
258 if ip == &broadcast_address {
259 return Err("The network's broadcast address can't be leased");
260 }
261
262 if ip == &network_address {
263 return Err("The network's network address can't be leased");
264 }
265
266 Ok(())
267 })?;
268
269 Ok(())
270 }
271
272 fn fill_available(&mut self) {
274 let network = self.config.network_cidr;
275 let hosts = network.hosts();
276
277 self.available.extend(hosts);
278
279 self.available.remove(&network.broadcast_address());
281 self.available.remove(&network.network_address());
282
283 self.leased.values().for_each(|lease| {
285 self.available.remove(&lease.ip);
286 });
287
288 self.config.static_leases.values().for_each(|ip| {
290 self.available.remove(ip);
291 });
292 }
293
294 fn commit(&mut self, mut lease: Lease, owner: MacAddress) -> Result<()> {
296 if !self.config.use_leases_file {
297 return Ok(());
298 }
299
300 if lease.line == 0 {
302 lease.line = self.lines + 1
303 }
304
305 let mut file_content: [u8; PAGE_SIZE] = [32; PAGE_SIZE];
308 lease
309 .to_string(owner)
310 .as_bytes()
311 .iter()
312 .enumerate()
313 .for_each(|(i, byte)| file_content[i] = *byte);
314 file_content[PAGE_SIZE - 1] = 10;
316
317 if self
318 .file
319 .as_mut()
320 .expect("This should always be Some here")
321 .seek(SeekFrom::Start((PAGE_SIZE * lease.line as usize) as u64))
322 .is_err()
323 {
324 return Err("Problem writing to leases file");
325 };
326
327 if self
328 .file
329 .as_mut()
330 .expect("This should always be Some here")
331 .write_all(&file_content)
332 .is_err()
333 {
334 return Err("Problem writing to leases file");
335 };
336
337 self.lines += 1;
338
339 Ok(())
340 }
341
342 pub fn offer(&mut self, owner: MacAddress, requested_ip: Option<Ipv4Addr>) -> Result<Ipv4Addr> {
352 if let Some(lease) = self.leased.get_mut(&owner) {
353 match lease.status {
354 LeaseStatus::Offered => {
355 warn!(
356 "{} will be offered an IP address that it has already been offered",
357 owner
358 );
359 lease.extend();
360 return Ok(lease.ip);
361 }
362 LeaseStatus::Acked => {
363 if !lease.is_expired(self.lease_time) {
364 warn!(
365 "{} will be offered its non-expired, acked IP address again",
366 owner
367 );
368 lease.status = LeaseStatus::Offered;
369 lease.extend();
370 return Ok(lease.ip);
371 }
372 }
374 }
375 }
376
377 if let Some(requested_ip) = requested_ip {
378 let not_available = !self.available.contains(&requested_ip);
379 let has_static_lease = self.config.static_leases.contains_key(&owner);
380
381 if not_available {
382 debug!("Requested IP Address is not available (or maybe not in network range)");
384 } else if has_static_lease {
385 debug!("Requested IP Address ignored because owner has static lease");
386 } else {
387 self.available.remove(&requested_ip);
388 self.leased
389 .insert(owner, Lease::new(requested_ip, LeaseStatus::Offered, false));
390 return Ok(requested_ip);
391 }
392 }
393
394 let lease = self.get_lease(&owner, LeaseStatus::Offered)?;
396 let ip = lease.ip;
397 self.leased.insert(owner, lease);
398 Ok(ip)
399 }
400
401 pub fn ack(&mut self, owner: MacAddress) -> Result<Ipv4Addr> {
404 let maybe_lease = self.leased.get_mut(&owner);
405 let lease = match maybe_lease {
406 Some(lease) => {
407 match lease.status {
408 LeaseStatus::Offered => {
409 lease.status = LeaseStatus::Acked;
410 }
411 LeaseStatus::Acked => {
412 warn!(
413 "{} will be leased an IP address it was already leased",
414 owner
415 );
416 }
417 };
418
419 lease.extend();
420 lease.to_owned()
421 }
422 None => {
423 let lease = self.get_lease(&owner, LeaseStatus::Acked)?;
425 self.leased.insert(owner, lease.clone());
426 lease
427 }
428 };
429
430 let ip = lease.ip;
431 self.commit(lease, owner)?;
432
433 Ok(ip)
434 }
435
436 pub fn extend(&mut self, owner: MacAddress) -> Result<()> {
439 match self.leased.get_mut(&owner) {
440 Some(lease) => {
441 lease.extend();
442 Ok(())
443 }
444 None => Err("No IP address lease found with that owner"),
445 }
446 }
447
448 pub fn release(&mut self, owner: MacAddress) -> Result<()> {
450 let maybe_leased = self.leased.remove(&owner);
451
452 match maybe_leased {
453 Some(lease) => {
454 if !lease.is_static {
455 self.available.insert(lease.ip);
456 }
457 Ok(())
458 }
459 None => Err("No IP address lease found with that owner"),
460 }
461 }
462
463 fn get_lease(&mut self, owner: &MacAddress, status: LeaseStatus) -> Result<Lease> {
465 let lease = match self.config.static_leases.get(owner) {
466 Some(ip) => Lease::new(*ip, status, true),
467 None => Lease::new(self.get_ip()?, status, false),
468 };
469 Ok(lease)
470 }
471
472 fn get_ip(&mut self) -> Result<Ipv4Addr> {
477 if let Some(any) = self.available.iter().next().cloned() {
478 debug!("Chose available IP address {any}");
479 return Ok(self.available.take(&any).unwrap());
481 }
482
483 let maybe_expired_lease = self
484 .leased
485 .clone() .into_iter() .find(|(_owner, lease)| {
488 let expiration = match lease.status {
489 LeaseStatus::Offered => Self::OFFER_TIMEOUT,
490 LeaseStatus::Acked => self.lease_time,
491 };
492 lease.is_expired(expiration)
493 });
494
495 if let Some(expired_lease) = maybe_expired_lease {
496 debug!("Reusing expired lease's IP address");
497 return Ok(self.leased.remove(&expired_lease.0).unwrap().ip);
499 }
500
501 Err("No more IP addresses available")
502 }
503
504 pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
506 self.available.contains(ip)
507 }
508
509 pub fn verify_lease(&self, owner: MacAddress, ip: &Ipv4Addr) -> Result<()> {
512 let maybe_leased = self.leased.get(&owner);
513
514 match maybe_leased {
515 Some(lease) => {
516 if let LeaseStatus::Offered = lease.status {
517 return Err("Lease offered but not previously acked");
518 }
519
520 if &lease.ip != ip {
521 return Err("Client's notion of ip address is wrong");
522 }
523
524 if lease.is_expired(self.lease_time) {
525 return Err("Lease has expired");
526 }
527
528 Ok(())
529 }
530 None => Err("No IP address lease found with that owner"),
531 }
532 }
533}