toe_beans/v4/server/
leases.rs

1use super::Config;
2use crate::v4::error::Result;
3use log::{debug, info, warn};
4use mac_address::MacAddress;
5use serde::{Deserialize, Serialize};
6use std::fs::{read_to_string, File};
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    is_static: bool,
28    // TODO this time measurement is not monotonic.
29    when: SystemTime,
30}
31
32impl Lease {
33    #[inline]
34    fn new(ip: Ipv4Addr, status: LeaseStatus, is_static: bool) -> Self {
35        Self {
36            ip,
37            status,
38            is_static,
39            when: SystemTime::now(),
40        }
41    }
42
43    /// Resets the time that this was considered leased.
44    #[inline]
45    fn extend(&mut self) {
46        self.when = SystemTime::now();
47    }
48
49    fn is_expired(&self, lease_time: u32) -> Result<bool> {
50        match self.when.elapsed() {
51            Ok(elapsed) => Ok(elapsed >= Duration::from_secs(lease_time as u64)),
52            Err(_) => Err("Problem getting elapsed system time"),
53        }
54    }
55}
56
57/// Used to manage the dynamic collection of IP addresses that the server leases.
58#[derive(Deserialize, Serialize)]
59pub struct Leases {
60    #[serde(skip)]
61    available: HashSet<Ipv4Addr>,
62    leased: HashMap<MacAddress, Lease>,
63    /// The server will always assign these ip adddresses to these mac addresses.
64    /// All static leases must be within the network_cidr range.
65    #[serde(skip)]
66    static_leases: HashMap<MacAddress, Ipv4Addr>,
67    /// Passed to Leases from Config.lease_time, so Leases can check for lease expiration.
68    lease_time: u32,
69    /// An open descriptor to the leases file that is written to for each commit.
70    ///
71    /// This will be None until partway through a `restore`,
72    /// and will always be Some after `new` or `restore`.
73    #[serde(skip)]
74    leases_file: Option<File>,
75}
76
77impl Leases {
78    /// The file name that the Leases is read from and written to.
79    pub const FILE_NAME: &'static str = "toe-beans-leases.toml";
80
81    /// Offers only valid for about 5 minutes (300 seconds)
82    /// at which point the offer's IP address may be reused.
83    ///
84    /// "Because the servers have not committed any network address assignments"
85    /// "on the basis of a DHCPOFFER, server are free to reuse offered network addresses in response to subsequent requests."
86    /// "Servers SHOULD NOT reuse offered addresses and may use an implementation-specific timeout mechanism to decide when to reuse an offered address."
87    const OFFER_TIMEOUT: u32 = 300;
88
89    fn open_leases_file(path: &PathBuf) -> File {
90        OpenOptions::new()
91            .read(false)
92            .write(true)
93            .create(true)
94            .truncate(true)
95            .mode(0o644) // ensure that file permissions are set before creating file
96            .open(path.join(Self::FILE_NAME))
97            .expect("Failed to open leases file")
98    }
99
100    /// Only call this _after_ `leased` and `static_leases` is filled.
101    fn fill_available(&mut self, config: &Config) {
102        let network = config.network_cidr;
103        let hosts = network.hosts();
104
105        self.available.extend(hosts);
106
107        // Dont assign the broadcast or network addresses
108        self.available.remove(&network.broadcast_address());
109        self.available.remove(&network.network_address());
110
111        // Dont assign the already assigned addresses
112        self.leased.values().for_each(|lease| {
113            self.available.remove(&lease.ip);
114        });
115
116        // Dont assign the statically reserved addresses
117        self.static_leases.values().for_each(|ip| {
118            self.available.remove(ip);
119        });
120    }
121
122    /// Create a pool with the capacity of the configured network's size.
123    pub fn new(config: &Config) -> Self {
124        #[cfg(not(feature = "benchmark"))]
125        let leases_file = Some(Self::open_leases_file(&config.path));
126        #[cfg(feature = "benchmark")]
127        let leases_file = None;
128
129        let size = config.network_cidr.hosts().len();
130
131        let mut leases = Self {
132            available: HashSet::with_capacity(size),
133            leased: HashMap::with_capacity(size),
134            static_leases: config.static_leases.clone(),
135            lease_time: config.lease_time.as_server(),
136            leases_file,
137        };
138
139        leases.fill_available(config);
140
141        leases
142    }
143
144    /// Checks that both `static_leases` and `leased` are:
145    /// 1. Only contain ip addresses in the configured network range
146    /// 2. Don't contain the broadcast or network address (which are unassignable)
147    #[inline]
148    fn validate(&self, config: &Config) -> Result<()> {
149        let network = config.network_cidr;
150        let broadcast_address = network.broadcast_address();
151        let network_address = network.network_address();
152
153        self.static_leases.values().try_for_each(|static_ip| {
154            if !network.contains(*static_ip) {
155                return Err("The configured network range does not include the restored static ip. Did the network range change?");
156            }
157
158            if static_ip == &broadcast_address {
159                return Err("The network's broadcast address can't be statically leased");
160            }
161
162            if static_ip == &network_address {
163                return Err("The network's network address can't be statically leased");
164            }
165
166            Ok(())
167        })?;
168
169        self.leased.values().try_for_each(|lease| {
170            let ip = lease.ip;
171
172            if !network.contains(ip) {
173                return Err("The configured network range does not include the restored leased ip. Did the network range change?");
174            }
175
176            if ip == broadcast_address {
177                return Err("The network's broadcast address can't be leased");
178            }
179
180            if ip == network_address {
181                return Err("The network's network address can't be leased");
182            }
183
184            Ok(())
185        })?;
186
187        Ok(())
188    }
189
190    /// Restores the state of a `Leases` from a `toe-beans-leases.toml`.
191    /// This involves validating that a user has not edited the file in a way that is invalid.
192    pub fn restore(config: &Config) -> Result<Self> {
193        let read_result = read_to_string(config.path.join(Self::FILE_NAME));
194
195        let mut leases: Leases = match read_result {
196            Ok(toml_string) => {
197                let toml_result = toml::from_str(&toml_string);
198                match toml_result {
199                    Ok(leases) => leases,
200                    Err(_) => return Err("Problem parsing leases file"),
201                }
202            }
203            Err(_) => {
204                return Err("Problem reading leases file");
205            }
206        };
207
208        leases.validate(config)?;
209
210        leases.leases_file = Some(Self::open_leases_file(&config.path));
211        leases.static_leases = config.static_leases.clone();
212        leases.fill_available(config);
213
214        info!("Restored IP address leases");
215        Ok(leases)
216    }
217
218    /// Use the toe-beans-leases.toml to restore past leases
219    /// or return a new Leases if that was not successful.
220    pub fn restore_or_new(config: &Config) -> Self {
221        Leases::restore(config).unwrap_or_else(|_| Leases::new(config))
222    }
223
224    /// Writes leases to persistent storage as toe-beans-leases.toml
225    fn commit(&mut self) -> Result<()> {
226        debug!("Writing {}", Self::FILE_NAME);
227
228        let file_content = match toml::to_string_pretty(&self) {
229            Ok(content) => content,
230            Err(_) => return Err("Failed to generate toml content for leases file"),
231        };
232
233        match self
234            .leases_file
235            .as_mut()
236            .expect("A leases file should always be Some at this point")
237            .write_all(file_content.as_bytes())
238        {
239            Ok(_) => Ok(()),
240            Err(_) => Err("Failed to write to leases file"),
241        }
242    }
243
244    /// Takes an available IP address, marks it as offered, and returns it.
245    ///
246    /// Returns one of:
247    /// - The previously offered address, if one has been offered.
248    /// - The requested address:
249    ///   - If one was requested,
250    ///   - _and_ it is available,
251    ///   - _and_ there is not a static lease
252    /// - Otherwise any available address (unless none are available).
253    pub fn offer(&mut self, owner: MacAddress, requested_ip: Option<Ipv4Addr>) -> Result<Ipv4Addr> {
254        if let Some(lease) = self.leased.get_mut(&owner) {
255            match lease.status {
256                LeaseStatus::Offered => {
257                    warn!(
258                        "{} will be offered an IP address that it has already been offered",
259                        owner
260                    );
261                    lease.extend();
262                    return Ok(lease.ip);
263                }
264                LeaseStatus::Acked => {
265                    if !lease.is_expired(self.lease_time)? {
266                        warn!(
267                            "{} will be offered its non-expired, acked IP address again",
268                            owner
269                        );
270                        lease.status = LeaseStatus::Offered;
271                        lease.extend();
272                        return Ok(lease.ip);
273                    }
274                    // else if it is expired then continue to offer new address below...
275                }
276            }
277        }
278
279        if let Some(requested_ip) = requested_ip {
280            let not_available = !self.available.contains(&requested_ip);
281            let has_static_lease = self.static_leases.contains_key(&owner);
282
283            if not_available {
284                // we validated that available and leased were in range in `restore`
285                debug!("Requested IP Address is not available (or maybe not in network range)");
286            } else if has_static_lease {
287                debug!("Requested IP Address ignored because owner has static lease");
288            } else {
289                self.available.remove(&requested_ip);
290                self.leased
291                    .insert(owner, Lease::new(requested_ip, LeaseStatus::Offered, false));
292                return Ok(requested_ip);
293            }
294        }
295
296        // else give an available ip address below...
297        let lease = self.get_lease(&owner, LeaseStatus::Offered)?;
298        let ip = lease.ip;
299        self.leased.insert(owner, lease);
300        Ok(ip)
301    }
302
303    /// Takes a chaddr's offered ip address and marks it as reserved
304    /// then commits it to persistent storage and returns the address.
305    pub fn ack(&mut self, owner: MacAddress) -> Result<Ipv4Addr> {
306        let maybe_leased = self.leased.get_mut(&owner);
307        let ip = match maybe_leased {
308            Some(leased) => {
309                match leased.status {
310                    LeaseStatus::Offered => {
311                        leased.status = LeaseStatus::Acked;
312                    }
313                    LeaseStatus::Acked => {
314                        warn!(
315                            "{} will be leased an IP address it was already leased",
316                            owner
317                        );
318                    }
319                };
320
321                leased.extend();
322                leased.ip
323            }
324            None => {
325                // nothing was offered, but this might be a rapid commit
326                let lease = self.get_lease(&owner, LeaseStatus::Acked)?;
327                let ip = lease.ip;
328                self.leased.insert(owner, lease);
329                ip
330            }
331        };
332
333        #[cfg(not(feature = "benchmark"))]
334        self.commit()?;
335
336        Ok(ip)
337    }
338
339    /// Resets the time of an offered or acked lease.
340    /// Used by the server in the lease renew/rebind process.
341    pub fn extend(&mut self, owner: MacAddress) -> Result<()> {
342        match self.leased.get_mut(&owner) {
343            Some(lease) => {
344                lease.extend();
345                Ok(())
346            }
347            None => Err("No IP address lease found with that owner"),
348        }
349    }
350
351    /// Makes an ip address that was offered or acked available again.
352    pub fn release(&mut self, owner: MacAddress) -> Result<()> {
353        let maybe_leased = self.leased.remove(&owner);
354
355        match maybe_leased {
356            Some(lease) => {
357                if !lease.is_static {
358                    self.available.insert(lease.ip);
359                }
360                Ok(())
361            }
362            None => Err("No IP address lease found with that owner"),
363        }
364    }
365
366    /// Returns either a lease with a static address or one with the next available address
367    fn get_lease(&mut self, owner: &MacAddress, status: LeaseStatus) -> Result<Lease> {
368        let lease = match self.static_leases.get(owner) {
369            Some(ip) => Lease::new(*ip, status, true),
370            None => Lease::new(self.get_ip()?, status, false),
371        };
372        Ok(lease)
373    }
374
375    /// Returns a static ip address or gets the next available address.
376    /// If there are no more available addresses, then it will:
377    /// 1. Search for offered addresses older than 1 minute
378    /// 2. Search for acked addresses that have expired.
379    fn get_ip(&mut self) -> Result<Ipv4Addr> {
380        if let Some(any) = self.available.iter().next().cloned() {
381            debug!("Chose available IP address {any}");
382            // UNWRAP is okay because we've found this element above
383            return Ok(self.available.take(&any).unwrap());
384        }
385
386        let maybe_expired_lease = self
387            .leased
388            .clone() // TODO slow
389            .into_iter() // TODO slow
390            .find(|(_owner, lease)| match lease.status {
391                LeaseStatus::Offered => lease.is_expired(Self::OFFER_TIMEOUT).unwrap_or(false),
392                LeaseStatus::Acked => lease.is_expired(self.lease_time).unwrap_or(false),
393            });
394
395        if let Some(expired_lease) = maybe_expired_lease {
396            debug!("Reusing expired lease's IP address");
397            // UNWRAP is okay because we've found this element above
398            return Ok(self.leased.remove(&expired_lease.0).unwrap().ip);
399        }
400
401        Err("No more IP addresses available")
402    }
403
404    /// Checks whether the passed IP address is available in the pool.
405    pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
406        self.available.contains(ip)
407    }
408
409    /// Checks if the passed IP address matches the committed, leased IP address,
410    /// and that the lease is not expired.
411    pub fn verify_lease(&self, owner: MacAddress, ip: &Ipv4Addr) -> Result<()> {
412        let maybe_leased = self.leased.get(&owner);
413
414        match maybe_leased {
415            Some(lease) => {
416                if let LeaseStatus::Offered = lease.status {
417                    return Err("Lease offered but not previously acked");
418                }
419
420                if &lease.ip != ip {
421                    return Err("Client's notion of ip address is wrong");
422                }
423
424                if lease.is_expired(self.lease_time)? {
425                    return Err("Lease has expired");
426                }
427
428                Ok(())
429            }
430            None => Err("No IP address lease found with that owner"),
431        }
432    }
433}