Skip to main content

toe_beans/v4/server/leases/
mod.rs

1mod config;
2mod lease;
3mod lease_time;
4
5pub use config::*;
6pub use lease_time::*;
7
8use crate::v4::error::Result;
9use ip_network::Ipv4Network;
10use lease::{Lease, LeaseStatus, PAGE_SIZE};
11use mac_address::MacAddress;
12use std::fs::{File, OpenOptions};
13use std::io::{BufRead, BufReader, Seek, SeekFrom, Write};
14use std::{
15    collections::{HashMap, HashSet},
16    net::Ipv4Addr,
17};
18use tracing::{debug, error, info, warn};
19
20/// Stores the leases and other information used across leasing functions in memory.
21///
22/// The leases are also written to a file on disk to preserve state in the event that something, such as a power outage, causes the server to stop.
23/// This file is attempted to be read, parsed, and validated during `Leases` creation during `Server` start. After which it is only written to in order to sync it with its in-memory state.
24/// Therefore, changing the file when the server is running will have no effect and may impact syncing to the file correctly.
25#[derive(Debug)]
26pub struct Leases {
27    available: HashSet<Ipv4Addr>,
28    leased: HashMap<MacAddress, Lease>,
29    /// Passed to Leases from LeaseConfig.lease_time, so Leases can check for lease expiration.
30    lease_time: u32,
31    /// Keeps track of the number of lines written to the leases file.
32    lines: u32,
33    /// All configuration for `Leases`.
34    config: LeaseConfig,
35    /// A handle to the leases file.
36    /// Will always be Some after `::new()` if `LeasesConfig.use_leases_file` is true.
37    /// This is kept open for the duration of a `Leases` which is usually the lifetime of the `Server`.
38    /// An exclusive file lock is attempted to reduce the chances that another process (or even another toe-beans server) will write to the same file. If you want to run multiple toe-beans servers then change the `LeasesConfig.leases_file_path` so that each writes to a different location.
39    file: Option<File>,
40}
41
42impl Leases {
43    /// The file name that the Leases is read from and written to.
44    pub const FILE_NAME: &'static str = "toe-beans.leases";
45
46    /// Offers only valid for about 5 minutes (300 seconds)
47    /// at which point the offer's IP address may be reused.
48    ///
49    /// "Because the servers have not committed any network address assignments"
50    /// "on the basis of a DHCPOFFER, server are free to reuse offered network addresses in response to subsequent requests."
51    /// "Servers SHOULD NOT reuse offered addresses and may use an implementation-specific timeout mechanism to decide when to reuse an offered address."
52    const OFFER_TIMEOUT: u32 = 300;
53
54    /// Once you pass a `LeaseConfig` to `Leases`, `Leases` privately owns it,
55    /// but you can use this fn to borrow an immutable reference.
56    #[inline]
57    pub fn get_config(&self) -> &LeaseConfig {
58        &self.config
59    }
60
61    fn empty(config: &LeaseConfig) -> Self {
62        let size = config.network_cidr.hosts().len();
63        Self {
64            available: HashSet::with_capacity(size),
65            leased: HashMap::with_capacity(size),
66            lease_time: config.lease_time.as_server(),
67            config: LeaseConfig::default(),
68            lines: 0,
69            file: None,
70        }
71    }
72
73    /// Depending on whether the passed config enables `use_leases_file`:
74    /// Restore and validate the leases file or create a new, empty `Leases`.
75    pub fn new(config: LeaseConfig) -> Self {
76        let file_path = config.leases_file_path.join(Self::FILE_NAME);
77
78        let mut leases = if config.use_leases_file {
79            let mut leases = Self::read_leases(&config).unwrap_or_else(|_| Self::empty(&config));
80
81            let file = OpenOptions::new()
82                .write(true)
83                .create(true)
84                .truncate(false)
85                .open(file_path)
86                .expect("Problem opening leases file for writing");
87            file.lock().expect("Failed to acquire lock on leases file");
88            leases.file = Some(file);
89
90            leases
91        } else {
92            Self::empty(&config)
93        };
94
95        leases.config = config;
96        leases.fill_available();
97
98        leases
99    }
100
101    fn read_leases(config: &LeaseConfig) -> Result<Leases> {
102        info!("Trying to read, parse, and validate leases file");
103
104        let full_leases_file_path = config.leases_file_path.join(Self::FILE_NAME);
105        let open_result = File::open(full_leases_file_path);
106        let leased: HashMap<MacAddress, Lease> = match open_result {
107            Ok(file) => {
108                let mut line_num = 0;
109                BufReader::new(file)
110                    .lines()
111                    .map(|line| {
112                        let lease_string = line.expect("Problem reading leases file");
113                        line_num += 1;
114                        Lease::from_string(lease_string, line_num)
115                            .expect("Problem parsing leases file")
116                    })
117                    .collect()
118            }
119            Err(message) => {
120                error!("Problem opening leases file: {}", message);
121                return Err("Problem opening leases file");
122            }
123        };
124
125        let mut leases = Leases::empty(config);
126        leases.leased = leased;
127
128        leases.validate(&config.network_cidr)?;
129
130        info!("Restored and validated leases file");
131
132        Ok(leases)
133    }
134
135    /// Checks that both `static_leases` and `leased`:
136    /// 1. Only contain ip addresses in the configured network range
137    /// 2. Don't contain the broadcast or network address (which are unassignable)
138    #[inline]
139    fn validate(&self, network: &Ipv4Network) -> Result<()> {
140        let broadcast_address = network.broadcast_address();
141        let network_address = network.network_address();
142        let static_lease_ips = self.config.static_leases.values();
143        let leased_ips = self.leased.values().map(|lease| &lease.ip);
144
145        static_lease_ips.chain(leased_ips).try_for_each(|ip| {
146            if !network.contains(*ip) {
147                return Err("The configured network range does not include the restored ip. Did the network range change?");
148            }
149
150            if ip == &broadcast_address {
151                return Err("The network's broadcast address can't be leased");
152            }
153
154            if ip == &network_address {
155                return Err("The network's network address can't be leased");
156            }
157
158            Ok(())
159        })?;
160
161        Ok(())
162    }
163
164    /// Only call this _after_ `leased` and `static_leases` are filled.
165    fn fill_available(&mut self) {
166        let network = self.config.network_cidr;
167        let hosts = network.hosts();
168
169        self.available.extend(hosts);
170
171        // Dont assign the broadcast or network addresses
172        self.available.remove(&network.broadcast_address());
173        self.available.remove(&network.network_address());
174
175        // Dont assign the already assigned addresses
176        self.leased.values().for_each(|lease| {
177            self.available.remove(&lease.ip);
178        });
179
180        // Dont assign the statically reserved addresses
181        self.config.static_leases.values().for_each(|ip| {
182            self.available.remove(ip);
183        });
184    }
185
186    /// Writes leases to persistent storage as toe-beans-leases.toml
187    fn commit(&mut self, mut lease: Lease, owner: MacAddress) -> Result<()> {
188        if !self.config.use_leases_file {
189            return Ok(());
190        }
191
192        // if line has not been assigned, assign the next one:
193        if lease.line == 0 {
194            lease.line = self.lines + 1
195        }
196
197        // Convert string to a page of bytes for writing.
198        // If string is less than the page size then remaining characters are spaces.
199        let mut file_content: [u8; PAGE_SIZE] = [32; PAGE_SIZE];
200        lease
201            .to_string(owner)
202            .as_bytes()
203            .iter()
204            .enumerate()
205            .for_each(|(i, byte)| file_content[i] = *byte);
206        // The last character in a page is a line break.
207        file_content[PAGE_SIZE - 1] = 10;
208
209        if self
210            .file
211            .as_mut()
212            .expect("This should always be Some here")
213            .seek(SeekFrom::Start((PAGE_SIZE * lease.line as usize) as u64))
214            .is_err()
215        {
216            return Err("Problem writing to leases file");
217        };
218
219        if self
220            .file
221            .as_mut()
222            .expect("This should always be Some here")
223            .write_all(&file_content)
224            .is_err()
225        {
226            return Err("Problem writing to leases file");
227        };
228
229        self.lines += 1;
230
231        Ok(())
232    }
233
234    /// Takes an available IP address, marks it as offered, and returns it.
235    ///
236    /// Returns one of:
237    /// - The previously offered address, if one has been offered.
238    /// - The requested address:
239    ///   - If one was requested,
240    ///   - _and_ it is available,
241    ///   - _and_ there is not a static lease
242    /// - Otherwise any available address (unless none are available).
243    pub fn offer(&mut self, owner: MacAddress, requested_ip: Option<Ipv4Addr>) -> Result<Ipv4Addr> {
244        if let Some(lease) = self.leased.get_mut(&owner) {
245            match lease.status {
246                LeaseStatus::Offered => {
247                    warn!(
248                        "{} will be offered an IP address that it has already been offered",
249                        owner
250                    );
251                    lease.extend();
252                    return Ok(lease.ip);
253                }
254                LeaseStatus::Acked => {
255                    if !lease.is_expired(self.lease_time) {
256                        warn!(
257                            "{} will be offered its non-expired, acked IP address again",
258                            owner
259                        );
260                        lease.status = LeaseStatus::Offered;
261                        lease.extend();
262                        return Ok(lease.ip);
263                    }
264                    // else if it is expired then continue to offer new address below...
265                }
266            }
267        }
268
269        if let Some(requested_ip) = requested_ip {
270            let not_available = !self.available.contains(&requested_ip);
271            let has_static_lease = self.config.static_leases.contains_key(&owner);
272
273            if not_available {
274                // we validated that available and leased were in range in `restore`
275                debug!("Requested IP Address is not available (or maybe not in network range)");
276            } else if has_static_lease {
277                debug!("Requested IP Address ignored because owner has static lease");
278            } else {
279                self.available.remove(&requested_ip);
280                self.leased
281                    .insert(owner, Lease::new(requested_ip, LeaseStatus::Offered, false));
282                return Ok(requested_ip);
283            }
284        }
285
286        // else give an available ip address below...
287        let lease = self.get_lease(&owner, LeaseStatus::Offered)?;
288        let ip = lease.ip;
289        self.leased.insert(owner, lease);
290        Ok(ip)
291    }
292
293    /// Takes a chaddr's offered ip address and marks it as reserved
294    /// then commits it to persistent storage and returns the address.
295    pub fn ack(&mut self, owner: MacAddress) -> Result<Ipv4Addr> {
296        let maybe_lease = self.leased.get_mut(&owner);
297        let lease = match maybe_lease {
298            Some(lease) => {
299                match lease.status {
300                    LeaseStatus::Offered => {
301                        lease.status = LeaseStatus::Acked;
302                    }
303                    LeaseStatus::Acked => {
304                        warn!(
305                            "{} will be leased an IP address it was already leased",
306                            owner
307                        );
308                    }
309                };
310
311                lease.extend();
312                lease.to_owned()
313            }
314            None => {
315                // nothing was offered, but this might be a rapid commit
316                let lease = self.get_lease(&owner, LeaseStatus::Acked)?;
317                self.leased.insert(owner, lease.clone());
318                lease
319            }
320        };
321
322        let ip = lease.ip;
323        self.commit(lease, owner)?;
324
325        Ok(ip)
326    }
327
328    /// Resets the time of an offered or acked lease.
329    /// Used by the server in the lease renew/rebind process.
330    pub fn extend(&mut self, owner: MacAddress) -> Result<()> {
331        match self.leased.get_mut(&owner) {
332            Some(lease) => {
333                lease.extend();
334                Ok(())
335            }
336            None => Err("No IP address lease found with that owner"),
337        }
338    }
339
340    /// Makes an ip address that was offered or acked available again.
341    pub fn release(&mut self, owner: MacAddress) -> Result<()> {
342        let maybe_leased = self.leased.remove(&owner);
343
344        match maybe_leased {
345            Some(lease) => {
346                if !lease.is_static {
347                    self.available.insert(lease.ip);
348                }
349                Ok(())
350            }
351            None => Err("No IP address lease found with that owner"),
352        }
353    }
354
355    /// Returns either a lease with a static address or one with the next available address
356    fn get_lease(&mut self, owner: &MacAddress, status: LeaseStatus) -> Result<Lease> {
357        let lease = match self.config.static_leases.get(owner) {
358            Some(ip) => Lease::new(*ip, status, true),
359            None => Lease::new(self.get_ip()?, status, false),
360        };
361        Ok(lease)
362    }
363
364    /// Returns the next available address.
365    /// If there are no more available addresses, then it will:
366    /// 1. Search for offered addresses older than 1 minute
367    /// 2. Search for acked addresses that have expired.
368    fn get_ip(&mut self) -> Result<Ipv4Addr> {
369        if let Some(any) = self.available.iter().next().cloned() {
370            debug!("Chose available IP address {any}");
371            // UNWRAP is okay because we've found this element above
372            return Ok(self.available.take(&any).unwrap());
373        }
374
375        let maybe_expired_lease = self
376            .leased
377            .clone() // TODO slow
378            .into_iter() // TODO slow
379            .find(|(_owner, lease)| {
380                let expiration = match lease.status {
381                    LeaseStatus::Offered => Self::OFFER_TIMEOUT,
382                    LeaseStatus::Acked => self.lease_time,
383                };
384                lease.is_expired(expiration)
385            });
386
387        if let Some(expired_lease) = maybe_expired_lease {
388            debug!("Reusing expired lease's IP address");
389            // UNWRAP is okay because we've found this element above
390            return Ok(self.leased.remove(&expired_lease.0).unwrap().ip);
391        }
392
393        Err("No more IP addresses available")
394    }
395
396    /// Checks whether the passed IP address is available in the pool.
397    pub fn is_available(&self, ip: &Ipv4Addr) -> bool {
398        self.available.contains(ip)
399    }
400
401    /// Checks if the passed IP address matches the committed, leased IP address,
402    /// and that the lease is not expired.
403    pub fn verify_lease(&self, owner: MacAddress, ip: &Ipv4Addr) -> Result<()> {
404        let maybe_leased = self.leased.get(&owner);
405
406        match maybe_leased {
407            Some(lease) => {
408                if let LeaseStatus::Offered = lease.status {
409                    return Err("Lease offered but not previously acked");
410                }
411
412                if &lease.ip != ip {
413                    return Err("Client's notion of ip address is wrong");
414                }
415
416                if lease.is_expired(self.lease_time) {
417                    return Err("Lease has expired");
418                }
419
420                Ok(())
421            }
422            None => Err("No IP address lease found with that owner"),
423        }
424    }
425}