Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,17 @@ networks:
start: 192.168.5.2
# end of your range
end: 192.168.5.250
# configured lease time (only `default` is required)
# configured lease time:
# Values can be specified as:
# - Plain numbers (assumed to be seconds): 3600, 86400
# - Strings with time units: 1h, 60m, 3600s, 24h
# Supported units: h (hours), m (minutes), s (seconds)
# Defaults: default=24h (86400s), min=20m (1200s), max=7d (604800s)
config:
lease_time:
default: 3600
min: 1200
max: 4800
default: 3600 # or 1h
min: 1200 # or 20m
max: 4800 # or 80m
# Both reservations & ranges can include an options map, if an incoming dhcp msg gets
# an IP from that reservation or range, it will also use the corresponding `options`
# to respond to any parameter request list values.
Expand Down
122 changes: 121 additions & 1 deletion libs/config/src/wire/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use std::{collections::HashMap, num::NonZeroU32, time::Duration};

use anyhow::{Context, Result};
use ipnet::Ipv4Net;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, de};

use crate::{LeaseTime, wire::client_classes::ClientClasses};

Expand Down Expand Up @@ -37,8 +38,11 @@ pub struct FloodThreshold {

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub struct MinMax {
#[serde(deserialize_with = "deserialize_duration")]
pub default: NonZeroU32,
#[serde(default, deserialize_with = "deserialize_optional_duration")]
pub min: Option<NonZeroU32>,
#[serde(default, deserialize_with = "deserialize_optional_duration")]
pub max: Option<NonZeroU32>,
}

Expand Down Expand Up @@ -70,6 +74,16 @@ pub fn default_cache_threshold() -> u32 {
0
}

impl Default for MinMax {
fn default() -> Self {
Self {
default: NonZeroU32::new(86400).unwrap(), // 24 hours
min: Some(NonZeroU32::new(1200).unwrap()), // 20 minutes
max: Some(NonZeroU32::new(604800).unwrap()), // 7 days
}
}
}

impl From<MinMax> for LeaseTime {
fn from(lease_time: MinMax) -> Self {
let default = Duration::from_secs(lease_time.default.get() as u64);
Expand All @@ -85,6 +99,70 @@ impl From<MinMax> for LeaseTime {
}
}

/// Parse a duration string with optional time units
/// Accepts: "3600", "3600s", "60m", "24h"
/// If no unit is specified, assumes seconds
fn parse_duration(s: &str) -> Result<u32> {
let s = s.trim();
if s.is_empty() {
return Err(anyhow::Error::msg("empty duration string"));
}

let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
// split units
let (num, unit) = s.split_at(end);
let num = num.parse::<u32>().context("invalid number")?;

let num_seconds = match unit.trim() {
"" | "s" => 1,
"m" => 60,
"h" => 3600,
other => anyhow::bail!(
"unknown time unit '{}', only 'h', 'm', or 's' are supported",
other
),
};

num.checked_mul(num_seconds)
.context("duration value overflow")
}

#[derive(Deserialize)]
#[serde(untagged)]
enum LeaseDuration {
Seconds(u64),
String(String),
}

impl LeaseDuration {
fn into_nonzero<E: de::Error>(self) -> Result<NonZeroU32, E> {
match self {
LeaseDuration::Seconds(val) => NonZeroU32::new(
u32::try_from(val).map_err(|_| E::custom("duration value too large"))?,
)
.ok_or_else(|| E::custom("duration cannot be zero")),
LeaseDuration::String(s) => NonZeroU32::new(parse_duration(&s).map_err(E::custom)?)
.ok_or_else(|| E::custom("duration cannot be zero")),
}
}
}

fn deserialize_duration<'de, D>(de: D) -> Result<NonZeroU32, D::Error>
where
D: Deserializer<'de>,
{
LeaseDuration::deserialize(de)?.into_nonzero()
}

fn deserialize_optional_duration<'de, D>(de: D) -> Result<Option<NonZeroU32>, D::Error>
where
D: Deserializer<'de>,
{
Option::<LeaseDuration>::deserialize(de)?
.map(LeaseDuration::into_nonzero)
.transpose()
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub(crate) enum MaybeList<T> {
Expand All @@ -94,6 +172,7 @@ pub(crate) enum MaybeList<T> {

#[cfg(test)]
mod tests {
use super::*;

pub static EXAMPLE: &str = include_str!("../../../../example.yaml");

Expand All @@ -106,4 +185,45 @@ mod tests {
let s = serde_yaml::to_string(&cfg).unwrap();
println!("{s}");
}

#[test]
fn test_parse_duration() {
assert_eq!(parse_duration("3600s").unwrap(), 3600);
assert_eq!(parse_duration("60s").unwrap(), 60);
assert_eq!(parse_duration("1s").unwrap(), 1);

assert_eq!(parse_duration("60m").unwrap(), 3600);
assert_eq!(parse_duration("1m").unwrap(), 60);
assert_eq!(parse_duration("90m").unwrap(), 5400);

assert_eq!(parse_duration("24h").unwrap(), 86400);
assert_eq!(parse_duration("1h").unwrap(), 3600);
assert_eq!(parse_duration("48h").unwrap(), 172800);
}

#[test]
fn test_parse_duration_invalid_unit() {
assert!(parse_duration("60d").is_err());
assert!(parse_duration("60w").is_err());
assert!(parse_duration("60x").is_err());
assert!(parse_duration("60mins").is_err());
}

#[test]
fn test_minmax() {
let json = r#"{"default": 3600, "min": 1200, "max": 7200}"#;
let minmax: MinMax = serde_json::from_str(json).unwrap();
assert_eq!(minmax.default.get(), 3600);
assert_eq!(minmax.min.unwrap().get(), 1200);
assert_eq!(minmax.max.unwrap().get(), 7200);
}

#[test]
fn test_minmax_strings() {
let json = r#"{"default": "1h", "min": "20m", "max": "2h"}"#;
let minmax: MinMax = serde_json::from_str(json).unwrap();
assert_eq!(minmax.default.get(), 3600);
assert_eq!(minmax.min.unwrap().get(), 1200);
assert_eq!(minmax.max.unwrap().get(), 7200);
}
}
5 changes: 4 additions & 1 deletion libs/config/src/wire/v4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,16 @@ pub struct IpRange {
#[serde(flatten)]
pub range: RangeInclusive<Ipv4Addr>,
pub options: Options,
#[serde(default)]
pub config: NetworkConfig,
#[serde(default)]
pub except: Vec<Ipv4Addr>,
pub class: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct NetworkConfig {
#[serde(default)]
pub lease_time: MinMax,
}

Expand Down Expand Up @@ -130,6 +132,7 @@ pub struct ReservedIp {
pub options: Options,
#[serde(rename = "match")]
pub condition: Condition,
#[serde(default)]
pub config: NetworkConfig,
pub class: Option<String>,
}
Expand Down
Loading