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
66 changes: 66 additions & 0 deletions bin/tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -833,3 +833,69 @@ fn test_cache_threshold() -> Result<()> {
drop(_srv);
Ok(())
}

/// send a discover to an ip that is not configured in the `interfaces` list
#[test]
#[traced_test]
fn test_interface_no_match() -> Result<()> {
let _srv = DhcpServerEnv::start(
"interface.yaml",
"interface.db",
"dora_test",
"dhcpcli",
"dhcpsrv",
"192.168.2.1",
);
// use veth_cli created in start()
let settings = ClientSettingsBuilder::default()
.iface_name("dhcpcli")
.target("192.168.2.2".parse::<std::net::IpAddr>().unwrap())
.port(9900_u16)
.build()?;
// create a client that sends dhcpv4 messages
let mut client = Client::<v4::Message>::new(settings);
// create DISCOVER msg & send
let msg_args = DiscoverBuilder::default()
.giaddr([192, 168, 2, 1])
.build()?;
let resp = client.run(MsgType::Discover(msg_args));

assert!(resp.is_err());

// pedantic drop
drop(_srv);
Ok(())
}

/// send a discover to an ip that is configured in the `interfaces` list
#[test]
#[traced_test]
fn test_interface_match() -> Result<()> {
let _srv = DhcpServerEnv::start(
"interface.yaml",
"interface.db",
"dora_test",
"dhcpcli",
"dhcpsrv",
"192.168.2.1",
);
// use veth_cli created in start()
let settings = ClientSettingsBuilder::default()
.iface_name("dhcpcli")
.target("192.168.2.1".parse::<std::net::IpAddr>().unwrap())
.port(9900_u16)
.build()?;
// create a client that sends dhcpv4 messages
let mut client = Client::<v4::Message>::new(settings);
// create DISCOVER msg & send
let msg_args = DiscoverBuilder::default()
.giaddr([192, 168, 2, 1])
.build()?;
let resp = client.run(MsgType::Discover(msg_args))?;

assert_eq!(resp.opts().msg_type().unwrap(), v4::MessageType::Offer);

// pedantic drop
drop(_srv);
Ok(())
}
10 changes: 10 additions & 0 deletions bin/tests/common/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ impl DhcpServerEnv {
veth_srv: &str,
srv_ip: &str,
) -> Self {
// Clean up any leftover resources from previous failed tests
// This is necessary because if start() panics before returning Self,
// Drop won't run and resources won't be cleaned up
remove_test_veth_nics(veth_cli);
remove_test_net_namespace(netns);

create_test_net_namespace(netns);
create_test_veth_nics(netns, srv_ip, veth_cli, veth_srv);
Self {
Expand Down Expand Up @@ -57,6 +63,8 @@ impl Drop for DhcpServerEnv {
const SUDO: &str = "sudo";

fn create_test_net_namespace(netns: &str) {
// Clean up any existing namespace first (from failed test runs)
run_cmd_ignore_failure(&format!("{SUDO} ip netns del {netns}"));
run_cmd(&format!("{SUDO} ip netns add {netns}"));
}

Expand All @@ -65,6 +73,8 @@ fn remove_test_net_namespace(netns: &str) {
}

fn create_test_veth_nics(netns: &str, srv_ip: &str, veth_cli: &str, veth_srv: &str) {
// Clean up any existing veth interfaces first (from failed test runs)
run_cmd_ignore_failure(&format!("{SUDO} ip link del {veth_cli}"));
run_cmd(&format!(
"{SUDO} ip link add {veth_cli} type veth peer name {veth_srv}",
));
Expand Down
22 changes: 22 additions & 0 deletions bin/tests/test_configs/interface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
interfaces:
- dhcpsrv@192.168.2.1
networks:
192.168.2.0/24:
ranges:
-
start: 192.168.2.100
end: 192.168.2.150
config:
lease_time:
default: 3600
min: 1200
max: 4800
options:
values:
1:
type: ip
value: 192.168.2.1
3:
type: ip
value:
- 192.168.2.1
175 changes: 167 additions & 8 deletions libs/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ pub fn backup_ivp4_interface(interface: Option<&str>) -> Result<Ipv4Network> {
/// Returns:
/// - interfaces matching the list supplied that are 'up' and have an IPv4
/// - OR any 'up' interfaces that also have an IPv4
pub fn v4_find_interfaces(interfaces: Option<Vec<String>>) -> Result<Vec<NetworkInterface>> {
pub fn v4_find_interfaces(interfaces: Option<&[wire::Interface]>) -> Result<Vec<NetworkInterface>> {
let found_interfaces = pnet::datalink::interfaces()
.into_iter()
.filter(|e| e.is_up() && !e.ips.is_empty() && e.ips.iter().any(|i| i.is_ipv4()))
Expand All @@ -131,7 +131,7 @@ pub fn v4_find_interfaces(interfaces: Option<Vec<String>>) -> Result<Vec<Network
/// Returns:
/// - interfaces matching the list supplied that are 'up' and have an IPv6
/// - OR any 'up' interfaces that also have an IPv6
pub fn v6_find_interfaces(interfaces: Option<Vec<String>>) -> Result<Vec<NetworkInterface>> {
pub fn v6_find_interfaces(interfaces: Option<&[wire::Interface]>) -> Result<Vec<NetworkInterface>> {
let found_interfaces = pnet::datalink::interfaces()
.into_iter()
.filter(|e| e.is_up() && !e.ips.is_empty() && e.ips.iter().any(|i| i.is_ipv6()))
Expand All @@ -141,17 +141,27 @@ pub fn v6_find_interfaces(interfaces: Option<Vec<String>>) -> Result<Vec<Network

fn found_or_default(
found_interfaces: Vec<NetworkInterface>,
interfaces: Option<Vec<String>>,
interfaces: Option<&[wire::Interface]>,
) -> Result<Vec<NetworkInterface>> {
Ok(match interfaces {
Some(interfaces) => interfaces
.iter()
.map(
|interface| match found_interfaces.iter().find(|i| &i.name == interface) {
.map(|interface| {
match found_interfaces.iter().find(|i| {
i.name == interface.name
&& interface
.addr
.map(|addr| i.ips.iter().any(|ip| ip.contains(addr)))
.unwrap_or(true)
}) {
Some(i) => Ok(i.clone()),
None => bail!("unable to find interface {}", interface),
},
)
None => bail!(
"unable to find interface {} with ip {:#?}",
interface.name,
interface.addr
),
}
})
.collect::<Result<Vec<_>, _>>()?,
None => found_interfaces,
})
Expand Down Expand Up @@ -229,3 +239,152 @@ impl PersistIdentifier {
Ok(Duid::from(duid_bytes))
}
}

#[cfg(test)]
mod test {
use std::net::IpAddr;

use dora_core::{pnet::ipnetwork::IpNetwork, prelude::NetworkInterface};

use crate::wire;

fn mock_interface(name: &str, ip_str: &str, prefix: u8) -> NetworkInterface {
let ip = ip_str.parse::<IpAddr>().unwrap();
NetworkInterface {
name: name.to_string(),
description: String::new(),
index: 0,
mac: None,
ips: vec![IpNetwork::new(ip, prefix).unwrap()],
flags: 0,
}
}

#[test]
fn test_found_or_default() {
let found = vec![mock_interface("eth0", "192.168.1.10", 24)];
let result = crate::found_or_default(found.clone(), None).unwrap();
assert!(!result.is_empty());

// no IP
let found = vec![mock_interface("eth0", "192.168.1.10", 24)];
let config = vec![wire::Interface {
name: "eth0".to_string(),
addr: None,
}];
let result = crate::found_or_default(found, Some(&config)).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "eth0");

// matching ip
let found = vec![mock_interface("eth0", "192.168.1.10", 24)];
let config = vec![wire::Interface {
name: "eth0".to_string(),
addr: Some("192.168.1.10".parse().unwrap()),
}];
let result = crate::found_or_default(found, Some(&config)).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "eth0");

// System interface has 192.168.1.1/24, config asks for 192.168.1.50
let found = vec![mock_interface("eth0", "192.168.1.10", 24)];
let config = vec![wire::Interface {
name: "eth0".to_string(),
addr: Some("192.168.1.50".parse().unwrap()),
}];
let result = crate::found_or_default(found, Some(&config)).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "eth0");

// System interface has 192.168.1.10, config asks for 10.0.0.1
let found = vec![mock_interface("eth0", "192.168.1.10", 24)];
let config = vec![wire::Interface {
name: "eth0".to_string(),
addr: Some("10.0.0.1".parse().unwrap()),
}];
let result = crate::found_or_default(found, Some(&config));
assert!(result.is_err());
}

#[test]
fn test_not_found_interface() {
let found = vec![mock_interface("eth0", "192.168.1.10", 24)];
let config = vec![wire::Interface {
name: "eth0".to_string(),
addr: Some([192, 168, 0, 10].into()),
}];
let result = crate::found_or_default(found, Some(&config));
assert!(result.is_err());

let found = vec![mock_interface("eth0", "192.168.1.10", 24)];
let config = vec![wire::Interface {
name: "eth1".to_string(), // Wrong name
addr: None,
}];
let result = crate::found_or_default(found, Some(&config));
assert!(result.is_err());
}

#[test]
fn test_find_by_name_and_ipv6_in_subnet() {
// System interface has 2001:db8::1/64, config asks for 2001:db8::dead:beef
let found = vec![mock_interface("eth1", "2001:db8::1", 64)];
let config = vec![wire::Interface {
name: "eth1".to_string(),
addr: Some("2001:db8::dead:beef".parse().unwrap()),
}];
let result = crate::found_or_default(found, Some(&config)).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "eth1");
}

#[test]
fn test_fail_on_ipv6_mismatch() {
// System interface has 2001:db8::1, config asks for fd00::1
let found = vec![mock_interface("eth1", "2001:db8::1", 64)];
let config = vec![wire::Interface {
name: "eth1".to_string(),
addr: Some("fd00::1".parse().unwrap()),
}];
let result = crate::found_or_default(found, Some(&config));
assert!(result.is_err());
}

#[test]
fn test_multiple_interfaces_find_by_ip() {
let found = vec![
mock_interface("eth0", "192.168.1.10", 24),
mock_interface("eth1", "10.0.0.5", 8),
];
let config = vec![wire::Interface {
name: "eth1".to_string(),
addr: Some("10.0.0.5".parse().unwrap()),
}];
let result = crate::found_or_default(found, Some(&config)).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "eth1");
}

#[test]
fn test_multiple_config_interfaces_selects_all() {
let found = vec![
mock_interface("eth0", "192.168.1.10", 24),
mock_interface("eth1", "10.0.0.5", 8),
mock_interface("lo", "127.0.0.1", 8),
];
let config = vec![
wire::Interface {
name: "eth0".to_string(),
addr: None,
},
wire::Interface {
name: "eth1".to_string(),
addr: Some("10.0.0.5".parse().unwrap()),
},
];
let result = crate::found_or_default(found, Some(&config)).unwrap();
assert_eq!(result.len(), 2);
assert!(result.iter().any(|i| i.name == "eth0"));
assert!(result.iter().any(|i| i.name == "eth1"));
}
}
Loading
Loading