toe_beans/v4/server/
ip_pool.rs

1use 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    // TODO this time measurement is not monotonic.
29    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/// Used to manage the dynamic collection of IP addresses that the server leases.
42// TODO Use a CIDR Trie for this?
43#[derive(Deserialize, Serialize)]
44pub struct IpPool {
45    #[serde(skip)]
46    available: HashSet<Ipv4Addr>,
47    leased: HashMap<MacAddress, Lease>,
48    /// The server will always assign these ip adddresses to these mac addresses.
49    /// All static leases must be within the network_cidr range.
50    static_leases: HashMap<MacAddress, Ipv4Addr>,
51    /// Passed to IpPool from Config.network_cidr.
52    network: Ipv4Network,
53    /// Passed to IpPool from Config.lease_time, so IpPool can check for lease expiration.
54    lease_time: u32,
55    #[serde(skip)]
56    /// Passed to IpPool from Config.path because they share a single directory.
57    file_path: PathBuf,
58}
59
60impl IpPool {
61    /// The file name that the IpPool is read from and written to.
62    pub const FILE_NAME: &'static str = "toe-beans-leases.toml";
63
64    /// Create a pool with the capacity of the configured network's size.
65    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    /// Restores the state of an IpPool from a toe-beans-leases.toml
84    /// as long as the IpPool's network (range/capacity) has not changed.
85    /// Static leases are verified to be within network range.
86    ///
87    /// This does not restore dhcp offers.
88    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        // TODO remove restored "acked" from "available"
122
123        info!("Restored IP address leases");
124        Ok(ip_pool)
125    }
126
127    /// Use the toe-beans-leases.toml to restore past leases
128    /// or return a new IpPool if that was not successful.
129    pub fn restore_or_new(config: &Config) -> Self {
130        IpPool::restore(config).unwrap_or_else(|_| IpPool::new(config))
131    }
132
133    /// Writes leases to persistent storage as toe-beans-leases.toml
134    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) // ensure that file permissions are set before creating file
148            .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    /// Takes an available IP address, marks it as offered, and returns it.
162    ///
163    /// Returns one of:
164    /// - The previously offered address, if one has been offered.
165    /// - The requested address:
166    ///   - If one was requested,
167    ///   - _and_ it is in the pool's range,
168    ///   - _and_ it is available,
169    ///   - _and_ there is not a static lease
170    /// - Otherwise any available address (unless none are available).
171    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        // else give an available ip address below...
215        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    /// Takes a chaddr's offered ip address and marks it as reserved
228    /// then commits it to persistent storage and returns the address.
229    ///
230    /// Currently only one IP is allowed per chaddr.
231    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                        // TODO should leased.when be refreshed here?
249
250                        leased.ip
251                    }
252                }
253            }
254            None => {
255                // nothing was offered, but this might be a rapid commit
256                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    /// Resets the time of an offered or acked lease. Used in the lease renew/rebind process.
276    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    /// Makes an ip address that was offered or acked available again.
287    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    /// Returns a static ip address or gets the next available address.
300    /// If there are no more available addresses, then it will:
301    /// 1. Search for offered addresses older than 1 minute
302    /// 2. Search for acked addresses that have expired.
303    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            // UNWRAP is okay because we've found this element above
314            return Ok(self.available.take(&any).unwrap());
315        }
316
317        // Offers only valid for about 5 minutes
318        let offer_timeout: u32 = 300;
319
320        let maybe_expired_lease = self
321            .leased
322            .clone() // TODO slow
323            .into_iter() // TODO slow
324            .find(|(_owner, lease)| match lease.status {
325                // "Because the servers have not committed any network address assignments"
326                // "on the basis of a DHCPOFFER, server are free to reuse offered network addresses in response to subsequent requests."
327                // "Servers SHOULD NOT reuse offered addresses and may use an implementation-specific timeout mechanism to decide when to reuse an offered address."
328                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            // UNWRAP is okay because we've found this element above
335            return Ok(self.leased.remove(&expired_lease.0).unwrap().ip);
336        }
337
338        Err("No more IP addresses available")
339    }
340
341    /// Checks whether the passed IP address is available in the pool.
342    pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
343        self.available.contains(ip)
344    }
345
346    /// Checks if the passed IP address matches the committed, leased IP address,
347    /// and that the lease is not expired.
348    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}