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 #[inline]
34 fn new(ip: Ipv4Addr, status: LeaseStatus) -> Self {
35 Self {
36 ip,
37 status,
38 when: SystemTime::now(),
39 }
40 }
41
42 #[inline]
44 fn extend(&mut self) {
45 self.when = SystemTime::now();
46 }
47
48 fn is_expired(&self, lease_time: u32) -> Result<bool> {
49 match self.when.elapsed() {
50 Ok(elapsed) => Ok(elapsed >= Duration::from_secs(lease_time as u64)),
51 Err(_) => Err("Problem getting elapsed system time"),
52 }
53 }
54}
55
56#[derive(Deserialize, Serialize)]
59pub struct IpPool {
60 #[serde(skip)]
61 available: HashSet<Ipv4Addr>,
62 leased: HashMap<MacAddress, Lease>,
63 static_leases: HashMap<MacAddress, Ipv4Addr>,
66 network: Ipv4Network,
68 lease_time: u32,
70 #[serde(skip)]
71 file_path: PathBuf,
73}
74
75impl IpPool {
76 pub const FILE_NAME: &'static str = "toe-beans-leases.toml";
78
79 pub fn new(config: &Config) -> Self {
81 let network = config.network_cidr;
82 let size = network.size() as usize;
83
84 let mut available = HashSet::with_capacity(size);
85 available.extend(network.iter());
86 available.remove(&network.broadcast());
88 available.remove(&network.ip());
89
90 let leased = HashMap::with_capacity(size);
91 let static_leases = HashMap::new();
92
93 Self {
94 file_path: config.path.clone(),
95 available,
96 leased,
97 static_leases,
98 network,
99 lease_time: config.lease_time.as_server(),
100 }
101 }
102
103 pub fn restore(config: &Config) -> Result<Self> {
107 let network = config.network_cidr;
108 let read_result = read_to_string(config.path.join(Self::FILE_NAME));
109
110 let mut ip_pool: IpPool = match read_result {
111 Ok(toml_string) => {
112 let toml_result = toml::from_str(&toml_string);
113 match toml_result {
114 Ok(ip_pool) => ip_pool,
115 Err(_) => return Err("Problem parsing leases file"),
116 }
117 }
118 Err(_) => {
119 return Err("Problem reading leases file");
120 }
121 };
122
123 if network != ip_pool.network {
124 return Err(
125 "The restored and configured IP address pool's network range/capacity do not match",
126 );
127 }
128
129 ip_pool.static_leases.values().try_for_each(|static_ip| {
132 if !network.contains(*static_ip) {
133 return Err("The configured network range does not include the static ip");
134 }
135
136 Ok(())
137 })?;
138
139 ip_pool.available.extend(network.iter());
141
142 let broadcast_address = network.broadcast();
143 let network_address = network.ip();
144
145 ip_pool.available.remove(&broadcast_address);
147 ip_pool.available.remove(&network_address);
148
149 ip_pool.leased.values().try_for_each(|lease| {
150 let ip = lease.ip;
151
152 if !network.contains(ip) {
153 return Err("The configured network range does not include the leased ip");
154 }
155
156 if ip == broadcast_address {
157 return Err("The network's broadcast address can't be leased");
158 }
159
160 if ip == network_address {
161 return Err("The network's network address can't be leased");
162 }
163
164 ip_pool.available.remove(&ip);
165
166 Ok(())
167 })?;
168
169 info!("Restored IP address leases");
170 Ok(ip_pool)
171 }
172
173 pub fn restore_or_new(config: &Config) -> Self {
176 IpPool::restore(config).unwrap_or_else(|_| IpPool::new(config))
177 }
178
179 fn commit(&self) -> Result<()> {
181 debug!("Writing {}", Self::FILE_NAME);
182
183 let file_content = match toml::to_string_pretty(&self) {
184 Ok(content) => content,
185 Err(_) => return Err("Failed to generate toml data"),
186 };
187
188 let open_result = OpenOptions::new()
189 .read(false)
190 .write(true)
191 .create(true)
192 .truncate(true)
193 .mode(0o644) .open(self.file_path.join(Self::FILE_NAME));
195
196 let mut file = match open_result {
197 Ok(file) => file,
198 Err(_) => return Err("Failed to open file for writing"),
199 };
200
201 match file.write_all(file_content.as_bytes()) {
202 Ok(_) => Ok(()),
203 Err(_) => Err("Failed to write to file"),
204 }
205 }
206
207 pub fn offer(&mut self, owner: MacAddress, requested_ip: Option<Ipv4Addr>) -> Result<Ipv4Addr> {
218 if let Some(lease) = self.leased.get(&owner) {
219 match lease.status {
220 LeaseStatus::Offered => {
221 warn!(
222 "{} will be offered an IP address that it has already been offered",
223 owner
224 );
225 return Ok(lease.ip);
226 }
227 LeaseStatus::Acked => {
228 if !lease.is_expired(self.lease_time)? {
229 return Err("This device has already been leased a non-expired IP address");
230 }
231 }
232 }
233 }
234
235 if let Some(requested_ip) = requested_ip {
236 let ip_in_pool = self.network.contains(requested_ip);
237 let not_available = !self.available.contains(&requested_ip);
238 let has_static_lease = self.static_leases.contains_key(&owner);
239
240 if !ip_in_pool {
241 debug!("Requested IP Address is not in network range");
242 } else if not_available {
243 debug!("Requested IP Address is not available");
244 } else if has_static_lease {
245 debug!("Requested IP Address ignored because owner has static lease");
246 } else {
247 self.available.remove(&requested_ip);
248 self.leased
249 .insert(owner, Lease::new(requested_ip, LeaseStatus::Offered));
250 return Ok(requested_ip);
251 }
252 }
253
254 let ip = self.find_available_ip(&owner)?;
256 self.leased
257 .insert(owner, Lease::new(ip, LeaseStatus::Offered));
258 Ok(ip)
259 }
260
261 pub fn ack(&mut self, owner: MacAddress) -> Result<Ipv4Addr> {
264 let maybe_leased = self.leased.get_mut(&owner);
265 let ip = match maybe_leased {
266 Some(leased) => {
267 match leased.status {
268 LeaseStatus::Offered => {
269 leased.status = LeaseStatus::Acked;
270 leased.extend();
271 leased.ip
272 }
273 LeaseStatus::Acked => {
274 if !leased.is_expired(self.lease_time)? {
275 return Err(
276 "This device has already been leased a non-expired IP address",
277 );
278 }
279
280 leased.ip
283 }
284 }
285 }
286 None => {
287 let ip = self.find_available_ip(&owner)?;
289 self.leased
290 .insert(owner, Lease::new(ip, LeaseStatus::Acked));
291 ip
292 }
293 };
294
295 #[cfg(not(feature = "benchmark"))]
296 self.commit()?;
297
298 Ok(ip)
299 }
300
301 pub fn extend(&mut self, owner: MacAddress) -> Result<()> {
304 match self.leased.get_mut(&owner) {
305 Some(lease) => {
306 lease.extend();
307 Ok(())
308 }
309 None => Err("No IP address lease found with that owner"),
310 }
311 }
312
313 pub fn release(&mut self, owner: MacAddress) -> Result<()> {
315 let maybe_leased = self.leased.remove(&owner);
316
317 match maybe_leased {
318 Some(lease) => {
319 self.available.insert(lease.ip);
320 Ok(())
321 }
322 None => Err("No IP address lease found with that owner"),
323 }
324 }
325
326 fn find_available_ip(&mut self, owner: &MacAddress) -> Result<Ipv4Addr> {
331 if let Some(ip) = self.static_leases.get(owner) {
332 return match self.available.take(ip) {
333 Some(ip) => Ok(ip),
334 None => Err("A static lease was found but is not available"),
335 };
336 }
337
338 if let Some(any) = self.available.iter().next().cloned() {
339 debug!("Chose available IP address {any}");
340 return Ok(self.available.take(&any).unwrap());
342 }
343
344 let offer_timeout: u32 = 300;
346
347 let maybe_expired_lease = self
348 .leased
349 .clone() .into_iter() .find(|(_owner, lease)| match lease.status {
352 LeaseStatus::Offered => lease.is_expired(offer_timeout).unwrap_or(false),
356 LeaseStatus::Acked => lease.is_expired(self.lease_time).unwrap_or(false),
357 });
358
359 if let Some(expired_lease) = maybe_expired_lease {
360 debug!("Reusing expired lease's IP address");
361 return Ok(self.leased.remove(&expired_lease.0).unwrap().ip);
363 }
364
365 Err("No more IP addresses available")
366 }
367
368 pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
370 self.available.contains(ip)
371 }
372
373 pub fn verify_lease(&self, owner: MacAddress, ip: &Ipv4Addr) -> Result<()> {
376 let maybe_leased = self.leased.get(&owner);
377
378 match maybe_leased {
379 Some(lease) => {
380 if let LeaseStatus::Offered = lease.status {
381 return Err("Lease not acked");
382 }
383
384 if &lease.ip != ip {
385 return Err("Client's notion of ip address is wrong");
386 }
387
388 if lease.is_expired(self.lease_time)? {
389 return Err("Lease has expired");
390 }
391
392 Ok(())
393 }
394 None => Err("No IP address lease found with that owner"),
395 }
396 }
397}