toe_beans/v4/server/
leases.rs1use super::Config;
2use crate::v4::error::Result;
3use log::{debug, info, warn};
4use mac_address::MacAddress;
5use serde::{Deserialize, Serialize};
6use std::fs::{File, read_to_string};
7use std::io::Write;
8use std::os::unix::fs::OpenOptionsExt;
9use std::path::PathBuf;
10use std::time::{Duration, SystemTime};
11use std::{
12 collections::{HashMap, HashSet},
13 fs::OpenOptions,
14 net::Ipv4Addr,
15};
16
17#[derive(Deserialize, Serialize, Clone)]
18enum LeaseStatus {
19 Offered,
20 Acked,
21}
22
23#[derive(Deserialize, Serialize, Clone)]
24struct Lease {
25 ip: Ipv4Addr,
26 status: LeaseStatus,
27 when: SystemTime,
29}
30
31impl Lease {
32 #[inline]
33 fn new(ip: Ipv4Addr, status: LeaseStatus) -> Self {
34 Self {
35 ip,
36 status,
37 when: SystemTime::now(),
38 }
39 }
40
41 #[inline]
43 fn extend(&mut self) {
44 self.when = SystemTime::now();
45 }
46
47 fn is_expired(&self, lease_time: u32) -> Result<bool> {
48 match self.when.elapsed() {
49 Ok(elapsed) => Ok(elapsed >= Duration::from_secs(lease_time as u64)),
50 Err(_) => Err("Problem getting elapsed system time"),
51 }
52 }
53}
54
55#[derive(Deserialize, Serialize)]
57pub struct Leases {
58 #[serde(skip)]
59 available: HashSet<Ipv4Addr>,
60 leased: HashMap<MacAddress, Lease>,
61 static_leases: HashMap<MacAddress, Ipv4Addr>,
64 lease_time: u32,
66 #[serde(skip)]
71 leases_file: Option<File>,
72}
73
74impl Leases {
75 pub const FILE_NAME: &'static str = "toe-beans-leases.toml";
77
78 const OFFER_TIMEOUT: u32 = 300;
85
86 fn open_leases_file(path: PathBuf) -> File {
87 OpenOptions::new()
88 .read(false)
89 .write(true)
90 .create(true)
91 .truncate(true)
92 .mode(0o644) .open(path.join(Self::FILE_NAME))
94 .expect("Failed to open leases file")
95 }
96
97 pub fn new(config: &Config) -> Self {
99 let network = config.network_cidr;
100 let hosts = network.hosts();
101 let size = hosts.len();
102
103 let mut available = HashSet::with_capacity(size);
104 available.extend(hosts);
105 available.remove(&network.broadcast_address());
107 available.remove(&network.network_address());
108
109 let leased = HashMap::with_capacity(size);
110 let static_leases = HashMap::new();
111 #[cfg(not(feature = "benchmark"))]
112 let leases_file = Some(Self::open_leases_file(config.path.clone()));
113 #[cfg(feature = "benchmark")]
114 let leases_file = None;
115
116 Self {
117 available,
118 leased,
119 static_leases,
120 lease_time: config.lease_time.as_server(),
121 leases_file,
122 }
123 }
124
125 pub fn restore(config: &Config) -> Result<Self> {
129 let network = config.network_cidr;
130 let read_result = read_to_string(config.path.join(Self::FILE_NAME));
131
132 let mut leases: Leases = match read_result {
133 Ok(toml_string) => {
134 let toml_result = toml::from_str(&toml_string);
135 match toml_result {
136 Ok(leases) => leases,
137 Err(_) => return Err("Problem parsing leases file"),
138 }
139 }
140 Err(_) => {
141 return Err("Problem reading leases file");
142 }
143 };
144
145 leases.leases_file = Some(Self::open_leases_file(config.path.clone()));
146
147 leases.static_leases.values().try_for_each(|static_ip| {
150 if !network.contains(*static_ip) {
151 return Err("The configured network range does not include the restored static ip. Did the network range change?");
152 }
153
154 Ok(())
155 })?;
156
157 leases.available.extend(network.hosts());
159
160 let broadcast_address = network.broadcast_address();
161 let network_address = network.network_address();
162
163 leases.available.remove(&broadcast_address);
165 leases.available.remove(&network_address);
166
167 leases.leased.values().try_for_each(|lease| {
168 let ip = lease.ip;
169
170 if !network.contains(ip) {
171 return Err("The configured network range does not include the restored leased ip. Did the network range change?");
172 }
173
174 if ip == broadcast_address {
175 return Err("The network's broadcast address can't be leased");
176 }
177
178 if ip == network_address {
179 return Err("The network's network address can't be leased");
180 }
181
182 leases.available.remove(&ip);
183
184 Ok(())
185 })?;
186
187 info!("Restored IP address leases");
188 Ok(leases)
189 }
190
191 pub fn restore_or_new(config: &Config) -> Self {
194 Leases::restore(config).unwrap_or_else(|_| Leases::new(config))
195 }
196
197 fn commit(&mut self) -> Result<()> {
199 debug!("Writing {}", Self::FILE_NAME);
200
201 let file_content = match toml::to_string_pretty(&self) {
202 Ok(content) => content,
203 Err(_) => return Err("Failed to generate toml content for leases file"),
204 };
205
206 match self
207 .leases_file
208 .as_mut()
209 .expect("A leases file should always be Some at this point")
210 .write_all(file_content.as_bytes())
211 {
212 Ok(_) => Ok(()),
213 Err(_) => Err("Failed to write to leases file"),
214 }
215 }
216
217 pub fn offer(&mut self, owner: MacAddress, requested_ip: Option<Ipv4Addr>) -> Result<Ipv4Addr> {
227 if let Some(lease) = self.leased.get_mut(&owner) {
228 match lease.status {
229 LeaseStatus::Offered => {
230 warn!(
231 "{} will be offered an IP address that it has already been offered",
232 owner
233 );
234 lease.extend();
235 return Ok(lease.ip);
236 }
237 LeaseStatus::Acked => {
238 if !lease.is_expired(self.lease_time)? {
239 warn!(
240 "{} will be offered its non-expired, acked IP address again",
241 owner
242 );
243 lease.status = LeaseStatus::Offered;
244 lease.extend();
245 return Ok(lease.ip);
246 }
247 }
249 }
250 }
251
252 if let Some(requested_ip) = requested_ip {
253 let not_available = !self.available.contains(&requested_ip);
254 let has_static_lease = self.static_leases.contains_key(&owner);
255
256 if not_available {
257 debug!("Requested IP Address is not available (or maybe not in network range)");
259 } else if has_static_lease {
260 debug!("Requested IP Address ignored because owner has static lease");
261 } else {
262 self.available.remove(&requested_ip);
263 self.leased
264 .insert(owner, Lease::new(requested_ip, LeaseStatus::Offered));
265 return Ok(requested_ip);
266 }
267 }
268
269 let ip = self.find_available_ip(&owner)?;
271 self.leased
272 .insert(owner, Lease::new(ip, LeaseStatus::Offered));
273 Ok(ip)
274 }
275
276 pub fn ack(&mut self, owner: MacAddress) -> Result<Ipv4Addr> {
279 let maybe_leased = self.leased.get_mut(&owner);
280 let ip = match maybe_leased {
281 Some(leased) => {
282 match leased.status {
283 LeaseStatus::Offered => {
284 leased.status = LeaseStatus::Acked;
285 }
286 LeaseStatus::Acked => {
287 warn!(
288 "{} will be leased an IP address it was already leased",
289 owner
290 );
291 }
292 };
293
294 leased.extend();
295 leased.ip
296 }
297 None => {
298 let ip = self.find_available_ip(&owner)?;
300 self.leased
301 .insert(owner, Lease::new(ip, LeaseStatus::Acked));
302 ip
303 }
304 };
305
306 #[cfg(not(feature = "benchmark"))]
307 self.commit()?;
308
309 Ok(ip)
310 }
311
312 pub fn extend(&mut self, owner: MacAddress) -> Result<()> {
315 match self.leased.get_mut(&owner) {
316 Some(lease) => {
317 lease.extend();
318 Ok(())
319 }
320 None => Err("No IP address lease found with that owner"),
321 }
322 }
323
324 pub fn release(&mut self, owner: MacAddress) -> Result<()> {
326 let maybe_leased = self.leased.remove(&owner);
327
328 match maybe_leased {
329 Some(lease) => {
330 self.available.insert(lease.ip);
331 Ok(())
332 }
333 None => Err("No IP address lease found with that owner"),
334 }
335 }
336
337 fn find_available_ip(&mut self, owner: &MacAddress) -> Result<Ipv4Addr> {
342 if let Some(ip) = self.static_leases.get(owner) {
343 return match self.available.take(ip) {
344 Some(ip) => Ok(ip),
345 None => Err("A static lease was found but is not available"),
346 };
347 }
348
349 if let Some(any) = self.available.iter().next().cloned() {
350 debug!("Chose available IP address {any}");
351 return Ok(self.available.take(&any).unwrap());
353 }
354
355 let maybe_expired_lease = self
356 .leased
357 .clone() .into_iter() .find(|(_owner, lease)| match lease.status {
360 LeaseStatus::Offered => lease.is_expired(Self::OFFER_TIMEOUT).unwrap_or(false),
361 LeaseStatus::Acked => lease.is_expired(self.lease_time).unwrap_or(false),
362 });
363
364 if let Some(expired_lease) = maybe_expired_lease {
365 debug!("Reusing expired lease's IP address");
366 return Ok(self.leased.remove(&expired_lease.0).unwrap().ip);
368 }
369
370 Err("No more IP addresses available")
371 }
372
373 pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
375 self.available.contains(ip)
376 }
377
378 pub fn verify_lease(&self, owner: MacAddress, ip: &Ipv4Addr) -> Result<()> {
381 let maybe_leased = self.leased.get(&owner);
382
383 match maybe_leased {
384 Some(lease) => {
385 if let LeaseStatus::Offered = lease.status {
386 return Err("Lease offered but not previously acked");
387 }
388
389 if &lease.ip != ip {
390 return Err("Client's notion of ip address is wrong");
391 }
392
393 if lease.is_expired(self.lease_time)? {
394 return Err("Lease has expired");
395 }
396
397 Ok(())
398 }
399 None => Err("No IP address lease found with that owner"),
400 }
401 }
402}