Skip to main content

toe_beans/v4/server/
leases.rs

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