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