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