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
// SPDX-License-Identifier: Apache-2.0
use super::env::{
init_log, set_client_ip, with_dhcp_env, with_udhcpd_env, FOO1_HOSTNAME,
FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID, TEST_CLS_DST, TEST_CLS_DST_LEN,
TEST_CLS_RT_ADDR, TEST_DHCP_SRV_ADDR, TEST_NIC_CLI,
};
use crate::{
DhcpV4ClasslessRoute, DhcpV4Client, DhcpV4Config, DhcpV4Lease, DhcpV4State,
};
use std::net::Ipv4Addr;
use tokio::time::{timeout, Duration};
const FOO2_HOSTNAME: &str = "foo2";
#[test]
fn test_dhcpv4() {
init_log();
with_dhcp_env(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.enable_io()
.build()
.unwrap();
let lease = rt.block_on(get_lease());
assert!(lease.is_some());
if let Some(lease) = lease {
// We should get FOO2_HOSTNAME as the hostname since that's what we
// sent in option 12 in the DHCP request.
assert_eq!(
lease.host_name.as_ref(),
Some(&FOO2_HOSTNAME.to_string())
);
// If the client id was set correctly to FOO1_HOSTNAME via the
// call to use_host_name_as_client_id(), then the server should
// return FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID.
assert_eq!(lease.yiaddr, FOO1_STATIC_IP_HOSTNAME_AS_CLIENT_ID,);
assert_eq!(
lease.classless_routes.as_deref().unwrap(),
&[DhcpV4ClasslessRoute {
destination: TEST_CLS_DST,
prefix_length: TEST_CLS_DST_LEN,
router: TEST_CLS_RT_ADDR,
}]
);
assert_eq!(
lease.get_option_raw(249).unwrap(),
&[249, 8, 24, 203, 0, 113, 192, 0, 2, 40]
);
}
})
}
#[test]
fn test_dhcpv4_unicast_renew_uses_srv_id() {
// test with udhcpd from busybox. Its a quite old server implementation
// but simple and reliable. It does not set siaddr automatically like
// dnsmasq which makes it a good candidate for a renew test so see that
// srv_id is used for the unicast renew and not siaddr.
init_log();
with_udhcpd_env(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_time()
.enable_io()
.build()
.unwrap();
rt.block_on(async {
let cfg = DhcpV4Config::new(TEST_NIC_CLI);
let mut cli = DhcpV4Client::init(cfg, None).await.unwrap();
let lease = loop {
if let DhcpV4State::Done(l) = cli.run().await.unwrap() {
break l;
}
};
assert_eq!(lease.srv_id, TEST_DHCP_SRV_ADDR);
assert_eq!(lease.yiaddr, Ipv4Addr::new(192, 0, 2, 100));
set_client_ip(lease.yiaddr);
// Wait until we are safely past T1 (50% lease time)
tokio::time::sleep(Duration::from_secs(6)).await;
// Renew phase
let state = cli.run().await.unwrap();
assert_eq!(state, DhcpV4State::Renewing);
// Observe outcome, timeout is fine, but we should never get so Rebinding
// (rebinding happens when renew fails, rebinding will use broadcast again like
// the first discovery)
let _ = timeout(Duration::from_secs(4), async {
loop {
let state = cli.run().await.unwrap();
match state {
// Rebinding would happen on T2 = 85% lease time
DhcpV4State::Rebinding => {
panic!("entered Rebinding state – Renew via srv_id failed");
}
DhcpV4State::Renewing => {
// still fine, keep polling
}
other => {
log::debug!("Received unused dhcp state: {other:?}")
}
}
}
})
.await;
});
});
}
async fn get_lease() -> Option<DhcpV4Lease> {
let mut config = DhcpV4Config::new(TEST_NIC_CLI);
// Since hostname hasn't been set yet, client_id should be empty.
config.use_host_name_as_client_id();
assert_eq!(config.client_id.len(), 0);
config.set_host_name(FOO1_HOSTNAME);
config.use_host_name_as_client_id();
// Now client id should be set to 0 + hostname.
let mut client_id = vec![0];
client_id.extend_from_slice(FOO1_HOSTNAME.as_bytes());
assert_eq!(config.client_id, client_id);
// config.use_host_name_as_client_id() copies the current hostname to
// client_id at the time it was called. We should now change the
// hostname to something dnsmasq doesn't know about so we're sure we get
// the correct ip address based on the client id (original hostname) and
// not the hostname we're now sending in option 12.
config.set_host_name(FOO2_HOSTNAME);
let mut cli = DhcpV4Client::init(config, None).await.unwrap();
while let Ok(state) = cli.run().await {
if let DhcpV4State::Done(lease) = state {
cli.release(&lease).await.unwrap();
return Some(*lease);
} else {
println!("DHCP state {state}");
}
}
None
}