mirror of
https://github.com/pj1234678/RustyDHCP.git
synced 2026-10-15 00:06:42 +08:00
686 lines
24 KiB
Rust
686 lines
24 KiB
Rust
//! Tests for DHCP Rapid Commit (RFC 4039 option 80 for IPv4, RFC 8415
|
|
//! option 14 for IPv6), gated by `enable_rapid_commit` in `dhcp.conf`.
|
|
//!
|
|
//! Design under test (mirrors `examples/server.rs`): with the flag off, or
|
|
//! without the option on the wire, behavior is exactly the legacy 4-message
|
|
//! (Discover->Offer) / Solicit->Advertise flow. With both present, the
|
|
//! server commits immediately (Ack/Reply: 2-message exchange). Both options
|
|
//! decode as `Unrecognized` — there is deliberately no typed variant, so the
|
|
//! branch-matrix test's typed set is unchanged.
|
|
|
|
use dhcp4r::{dhcpv6, options, packet, server};
|
|
use std::collections::HashMap;
|
|
use std::net::{Ipv4Addr, Ipv6Addr, UdpSocket};
|
|
use std::ops::Add;
|
|
use std::time::{Duration, Instant};
|
|
|
|
fn unwrap_packet(r: Result<packet::Packet, packet::CustomErr<&[u8]>>) -> packet::Packet {
|
|
match r {
|
|
Ok(p) => p,
|
|
Err(_) => panic!("expected Ok Packet"),
|
|
}
|
|
}
|
|
|
|
fn test_packet(xid: u32, chaddr: [u8; 6], opts: Vec<options::DhcpOption>) -> packet::Packet {
|
|
packet::Packet {
|
|
reply: false,
|
|
hops: 0,
|
|
xid,
|
|
secs: 0,
|
|
broadcast: false,
|
|
ciaddr: Ipv4Addr::UNSPECIFIED,
|
|
yiaddr: Ipv4Addr::UNSPECIFIED,
|
|
siaddr: Ipv4Addr::UNSPECIFIED,
|
|
giaddr: Ipv4Addr::UNSPECIFIED,
|
|
chaddr,
|
|
options: opts,
|
|
}
|
|
}
|
|
|
|
fn recv_with_timeout(sock: &UdpSocket, buf: &mut [u8]) -> (usize, std::net::SocketAddr) {
|
|
sock.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
|
|
sock.recv_from(buf).expect("timed out waiting for reply")
|
|
}
|
|
|
|
fn rapid_opt() -> options::DhcpOption {
|
|
options::DhcpOption::Unrecognized(options::RawDhcpOption {
|
|
code: options::RAPID_COMMIT,
|
|
data: vec![],
|
|
})
|
|
}
|
|
|
|
fn make_raw(
|
|
op: u8,
|
|
hlen: u8,
|
|
hops: u8,
|
|
xid: u32,
|
|
secs: u16,
|
|
flags: [u8; 2],
|
|
ci: [u8; 4],
|
|
yi: [u8; 4],
|
|
si: [u8; 4],
|
|
gi: [u8; 4],
|
|
chaddr: [u8; 6],
|
|
opts_after_cookie: Vec<u8>,
|
|
) -> Vec<u8> {
|
|
let mut v = vec![0u8; 236];
|
|
v[0] = op;
|
|
v[1] = 1;
|
|
v[2] = hlen;
|
|
v[3] = hops;
|
|
v[4..8].copy_from_slice(&xid.to_be_bytes());
|
|
v[8..10].copy_from_slice(&secs.to_be_bytes());
|
|
v[10] = flags[0];
|
|
v[11] = flags[1];
|
|
v[12..16].copy_from_slice(&ci);
|
|
v[16..20].copy_from_slice(&yi);
|
|
v[20..24].copy_from_slice(&si);
|
|
v[24..28].copy_from_slice(&gi);
|
|
v[28..34].copy_from_slice(&chaddr);
|
|
v.extend_from_slice(&[99, 130, 83, 99]);
|
|
v.extend_from_slice(&opts_after_cookie);
|
|
v
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// v4 wire: option 80 is Unrecognized, no typed variant by design
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn rapid_commit_v4_const_and_title() {
|
|
assert_eq!(options::RAPID_COMMIT, 80);
|
|
assert_eq!(options::title(80), Some("Rapid Commit"));
|
|
}
|
|
|
|
#[test]
|
|
fn rapid_commit_v4_decodes_as_unrecognized() {
|
|
// empty (the RFC 4039 shape) and non-empty payloads alike passthrough
|
|
for data in [vec![], vec![1, 2, 3]] {
|
|
let mut wire = vec![80, data.len() as u8];
|
|
wire.extend_from_slice(&data);
|
|
match packet::decode_option(&wire) {
|
|
Ok((rest, options::DhcpOption::Unrecognized(r))) => {
|
|
assert_eq!(r.code, 80);
|
|
assert_eq!(r.data, data);
|
|
assert!(rest.is_empty());
|
|
}
|
|
_ => panic!("option 80 must decode as Unrecognized"),
|
|
}
|
|
}
|
|
// round-trips through a full packet like any Unrecognized option
|
|
let p = test_packet(
|
|
1,
|
|
[0; 6],
|
|
vec![
|
|
options::DhcpOption::DhcpMessageType(options::MessageType::Discover),
|
|
rapid_opt(),
|
|
],
|
|
);
|
|
let mut buf = [0u8; 1500];
|
|
let q = unwrap_packet(packet::Packet::from(&p.encode(&mut buf).to_vec()));
|
|
assert_eq!(q.option(80), Some(&rapid_opt()));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// v4 live: rapid-aware handler replica mirroring the example Discover arm
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const IP_START: [u8; 4] = [192, 168, 2, 2];
|
|
const IP_START_NUM: u32 = u32::from_be_bytes(IP_START);
|
|
const LEASE_NUM: u32 = 252;
|
|
const LEASE_SECS: u32 = 86400;
|
|
|
|
struct RapidV4 {
|
|
leases: HashMap<Ipv4Addr, ([u8; 6], Option<Instant>)>,
|
|
last_lease: u32,
|
|
lease_duration: Duration,
|
|
enable_rapid_commit: bool,
|
|
}
|
|
|
|
impl RapidV4 {
|
|
fn new(enabled: bool) -> Self {
|
|
Self {
|
|
leases: HashMap::new(),
|
|
last_lease: 0,
|
|
lease_duration: Duration::from_secs(LEASE_SECS as u64),
|
|
enable_rapid_commit: enabled,
|
|
}
|
|
}
|
|
fn available(&self, chaddr: &[u8; 6], addr: &Ipv4Addr) -> bool {
|
|
let pos: u32 = (*addr).into();
|
|
pos >= IP_START_NUM
|
|
&& pos < IP_START_NUM + LEASE_NUM
|
|
&& match self.leases.get(addr) {
|
|
Some((mac, expiry)) => {
|
|
*mac == *chaddr || expiry.map_or(true, |exp| Instant::now().gt(&exp))
|
|
}
|
|
None => true,
|
|
}
|
|
}
|
|
fn current_lease(&self, chaddr: &[u8; 6]) -> Option<Ipv4Addr> {
|
|
for (i, v) in &self.leases {
|
|
if v.0 == *chaddr {
|
|
return Some(*i);
|
|
}
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
impl server::Handler for RapidV4 {
|
|
fn handle_request(&mut self, server: &server::Server, in_packet: packet::Packet) {
|
|
match in_packet.message_type() {
|
|
Ok(options::MessageType::Discover) => {
|
|
// Rapid Commit (RFC 4039): commit immediately with an Ack.
|
|
if self.enable_rapid_commit
|
|
&& in_packet.option(options::RAPID_COMMIT).is_some()
|
|
{
|
|
if let Some(ip) = self.current_lease(&in_packet.chaddr) {
|
|
let _ = server.reply(
|
|
options::MessageType::Ack,
|
|
vec![options::DhcpOption::IpAddressLeaseTime(LEASE_SECS)],
|
|
ip,
|
|
in_packet,
|
|
);
|
|
return;
|
|
}
|
|
for _ in 0..LEASE_NUM {
|
|
self.last_lease = (self.last_lease + 1) % LEASE_NUM;
|
|
let cand: Ipv4Addr = (IP_START_NUM + self.last_lease).into();
|
|
if self.available(&in_packet.chaddr, &cand) {
|
|
self.leases.insert(
|
|
cand,
|
|
(
|
|
in_packet.chaddr,
|
|
Some(Instant::now().add(self.lease_duration)),
|
|
),
|
|
);
|
|
let _ = server.reply(
|
|
options::MessageType::Ack,
|
|
vec![options::DhcpOption::IpAddressLeaseTime(LEASE_SECS)],
|
|
cand,
|
|
in_packet,
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
// Legacy path: prefer existing, else round-robin Offer.
|
|
if let Some(ip) = self.current_lease(&in_packet.chaddr) {
|
|
let _ = server.reply(
|
|
options::MessageType::Offer,
|
|
vec![options::DhcpOption::IpAddressLeaseTime(LEASE_SECS)],
|
|
ip,
|
|
in_packet,
|
|
);
|
|
return;
|
|
}
|
|
for _ in 0..LEASE_NUM {
|
|
self.last_lease = (self.last_lease + 1) % LEASE_NUM;
|
|
let cand: Ipv4Addr = (IP_START_NUM + self.last_lease).into();
|
|
if self.available(&in_packet.chaddr, &cand) {
|
|
let _ = server.reply(
|
|
options::MessageType::Offer,
|
|
vec![options::DhcpOption::IpAddressLeaseTime(LEASE_SECS)],
|
|
cand,
|
|
in_packet,
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn serve_rapid(enabled: bool) -> std::net::SocketAddr {
|
|
serve_rapid_with_leases(enabled, HashMap::new())
|
|
}
|
|
|
|
fn serve_rapid_with_leases(
|
|
enabled: bool,
|
|
leases: HashMap<Ipv4Addr, ([u8; 6], Option<Instant>)>,
|
|
) -> std::net::SocketAddr {
|
|
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let srv_addr = srv_sock.local_addr().unwrap();
|
|
let mut handler = RapidV4::new(enabled);
|
|
handler.leases = leases;
|
|
std::thread::spawn(move || {
|
|
let _ = server::Server::serve(
|
|
srv_sock,
|
|
Ipv4Addr::new(10, 0, 0, 1),
|
|
Ipv4Addr::new(10, 0, 0, 255),
|
|
handler,
|
|
);
|
|
});
|
|
std::thread::sleep(Duration::from_millis(100));
|
|
srv_addr
|
|
}
|
|
|
|
fn rapid_discover(xid: u32, chaddr: [u8; 6]) -> packet::Packet {
|
|
test_packet(
|
|
xid,
|
|
chaddr,
|
|
vec![
|
|
options::DhcpOption::DhcpMessageType(options::MessageType::Discover),
|
|
rapid_opt(),
|
|
],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn rapid_discover_acks_and_inserts_live() {
|
|
let srv_addr = serve_rapid(true);
|
|
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let mut buf = [0u8; 1500];
|
|
let mut rbuf = [0u8; 1500];
|
|
client
|
|
.send_to(&rapid_discover(0xB001, [1; 6]).encode(&mut buf).to_vec(), srv_addr)
|
|
.unwrap();
|
|
let (n, _) = recv_with_timeout(&client, &mut rbuf);
|
|
let rep = unwrap_packet(packet::Packet::from(&rbuf[..n]));
|
|
// 2-message exchange: Ack (not Offer) straight away...
|
|
assert_eq!(rep.message_type(), Ok(options::MessageType::Ack));
|
|
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 2, 3));
|
|
// ...and the lease was committed: a plain Discover is offered the same IP
|
|
// via the current-lease path instead of rotating to .4.
|
|
let plain = test_packet(
|
|
0xB002,
|
|
[1; 6],
|
|
vec![options::DhcpOption::DhcpMessageType(options::MessageType::Discover)],
|
|
);
|
|
client.send_to(&plain.encode(&mut buf).to_vec(), srv_addr).unwrap();
|
|
let (n, _) = recv_with_timeout(&client, &mut rbuf);
|
|
assert_eq!(
|
|
unwrap_packet(packet::Packet::from(&rbuf[..n])).yiaddr,
|
|
Ipv4Addr::new(192, 168, 2, 3)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rapid_disabled_ignores_option_live() {
|
|
let srv_addr = serve_rapid(false);
|
|
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let mut buf = [0u8; 1500];
|
|
let mut rbuf = [0u8; 1500];
|
|
// flag off: rapid-looking Discover is a plain Discover (Offer + no insert)
|
|
client
|
|
.send_to(&rapid_discover(0xB010, [2; 6]).encode(&mut buf).to_vec(), srv_addr)
|
|
.unwrap();
|
|
let (n, _) = recv_with_timeout(&client, &mut rbuf);
|
|
let rep = unwrap_packet(packet::Packet::from(&rbuf[..n]));
|
|
assert_eq!(rep.message_type(), Ok(options::MessageType::Offer));
|
|
// no insert happened: next Discover rotates instead of sticking
|
|
let plain = test_packet(
|
|
0xB011,
|
|
[2; 6],
|
|
vec![options::DhcpOption::DhcpMessageType(options::MessageType::Discover)],
|
|
);
|
|
client.send_to(&plain.encode(&mut buf).to_vec(), srv_addr).unwrap();
|
|
let (n, _) = recv_with_timeout(&client, &mut rbuf);
|
|
assert_eq!(
|
|
unwrap_packet(packet::Packet::from(&rbuf[..n])).yiaddr,
|
|
Ipv4Addr::new(192, 168, 2, 4)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rapid_without_option_is_plain_discover_live() {
|
|
let srv_addr = serve_rapid(true);
|
|
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let mut buf = [0u8; 1500];
|
|
let mut rbuf = [0u8; 1500];
|
|
// flag on but no option 80 on the wire: legacy Offer path
|
|
let plain = test_packet(
|
|
0xB020,
|
|
[3; 6],
|
|
vec![options::DhcpOption::DhcpMessageType(options::MessageType::Discover)],
|
|
);
|
|
client.send_to(&plain.encode(&mut buf).to_vec(), srv_addr).unwrap();
|
|
let (n, _) = recv_with_timeout(&client, &mut rbuf);
|
|
assert_eq!(
|
|
unwrap_packet(packet::Packet::from(&rbuf[..n])).message_type(),
|
|
Ok(options::MessageType::Offer)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rapid_pool_exhausted_silent_live() {
|
|
// fill every address with other MACs so nothing is available
|
|
let mut leases = HashMap::new();
|
|
for i in 0..LEASE_NUM {
|
|
let ip: Ipv4Addr = (IP_START_NUM + i).into();
|
|
leases.insert(
|
|
ip,
|
|
(
|
|
[0xCC, 0xDD, (i >> 8) as u8, i as u8, 0xEE, 0xFF],
|
|
Some(Instant::now() + Duration::from_secs(3600)),
|
|
),
|
|
);
|
|
}
|
|
let srv_addr = serve_rapid_with_leases(true, leases);
|
|
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
client
|
|
.set_read_timeout(Some(Duration::from_millis(400)))
|
|
.unwrap();
|
|
let mut buf = [0u8; 1500];
|
|
let mut rbuf = [0u8; 1500];
|
|
client
|
|
.send_to(&rapid_discover(0xB030, [4; 6]).encode(&mut buf).to_vec(), srv_addr)
|
|
.unwrap();
|
|
assert!(
|
|
client.recv_from(&mut rbuf).is_err(),
|
|
"exhausted rapid Discover must get no reply, like Discover"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rapid_existing_lease_acked_without_moving_counter() {
|
|
let srv_addr = serve_rapid(true);
|
|
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let mut buf = [0u8; 1500];
|
|
let mut rbuf = [0u8; 1500];
|
|
// take .3 via rapid commit first
|
|
client
|
|
.send_to(&rapid_discover(0xB040, [5; 6]).encode(&mut buf).to_vec(), srv_addr)
|
|
.unwrap();
|
|
let (n, _) = recv_with_timeout(&client, &mut rbuf);
|
|
assert_eq!(
|
|
unwrap_packet(packet::Packet::from(&rbuf[..n])).yiaddr,
|
|
Ipv4Addr::new(192, 168, 2, 3)
|
|
);
|
|
// rapid Discover again (even naming another IP): Ack for the held lease.
|
|
// Requested IP is ignored on the rapid path by design (Discover-style
|
|
// selection), so the stray option must not steer the answer.
|
|
let mut opts = vec![
|
|
options::DhcpOption::DhcpMessageType(options::MessageType::Discover),
|
|
rapid_opt(),
|
|
];
|
|
opts.push(options::DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 168, 2, 99)));
|
|
let retry = test_packet(0xB041, [5; 6], opts);
|
|
client.send_to(&retry.encode(&mut buf).to_vec(), srv_addr).unwrap();
|
|
let (n, _) = recv_with_timeout(&client, &mut rbuf);
|
|
let rep = unwrap_packet(packet::Packet::from(&rbuf[..n]));
|
|
assert_eq!(rep.message_type(), Ok(options::MessageType::Ack));
|
|
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 2, 3), "held lease wins");
|
|
}
|
|
|
|
#[test]
|
|
fn rapid_truncated_option_falls_back_to_offer_live() {
|
|
let srv_addr = serve_rapid(true);
|
|
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
|
let mut rbuf = [0u8; 1500];
|
|
// option 80 declares len 3 but only 1 byte follows: InvalidHlen swallows
|
|
// it, so option(80) is absent and the normal Offer path runs, not Ack.
|
|
let raw = make_raw(
|
|
1, 6, 0, 0xB050, 0, [0, 0],
|
|
[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0],
|
|
[6; 6],
|
|
vec![53, 1, 1, 80, 3, 0xAA, 255],
|
|
);
|
|
client.send_to(&raw, srv_addr).unwrap();
|
|
client
|
|
.set_read_timeout(Some(Duration::from_secs(2)))
|
|
.unwrap();
|
|
let (n, _) = client.recv_from(&mut rbuf).expect("reply");
|
|
let rep = unwrap_packet(packet::Packet::from(&rbuf[..n]));
|
|
assert_eq!(rep.message_type(), Ok(options::MessageType::Offer));
|
|
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 2, 3));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// v6: option 14 handling + rapid TestV6Server replica
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const V6_SERVER_DUID: [u8; 10] =
|
|
[0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x5e, 0xaa, 0xbb, 0xcc];
|
|
const V6_LEASE_SECS: u32 = 86400;
|
|
|
|
fn v6(a: [u16; 8]) -> Ipv6Addr {
|
|
Ipv6Addr::new(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])
|
|
}
|
|
|
|
fn v6pool() -> dhcpv6::V6Pool {
|
|
dhcpv6::V6Pool::new(
|
|
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x100),
|
|
1000,
|
|
Duration::from_secs(V6_LEASE_SECS as u64),
|
|
)
|
|
}
|
|
|
|
fn v6ip(n: u16) -> Ipv6Addr {
|
|
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, n)
|
|
}
|
|
|
|
fn rapid6_opt() -> dhcpv6::Dhcpv6Option {
|
|
dhcpv6::Dhcpv6Option::Unrecognized(dhcpv6::RawDhcpv6Option {
|
|
code: dhcpv6::OPT_RAPID_COMMIT,
|
|
data: vec![],
|
|
})
|
|
}
|
|
|
|
struct RapidV6Server {
|
|
pool: dhcpv6::V6Pool,
|
|
server_duid: Vec<u8>,
|
|
lease_secs: u32,
|
|
dns: Vec<Ipv6Addr>,
|
|
enable_rapid_commit: bool,
|
|
}
|
|
|
|
impl RapidV6Server {
|
|
fn new(enabled: bool) -> Self {
|
|
Self {
|
|
pool: v6pool(),
|
|
server_duid: V6_SERVER_DUID.to_vec(),
|
|
lease_secs: V6_LEASE_SECS,
|
|
dns: vec![v6([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888])],
|
|
enable_rapid_commit: enabled,
|
|
}
|
|
}
|
|
|
|
fn success(
|
|
&self,
|
|
req: &dhcpv6::Packet,
|
|
msg_type: dhcpv6::MsgType,
|
|
ip: Ipv6Addr,
|
|
) -> dhcpv6::Packet {
|
|
let iaid = req.first_iana().map(|ia| ia.iaid).unwrap_or(0);
|
|
let (t1, t2) = dhcpv6::default_t1_t2(self.lease_secs);
|
|
let duid = match req.option(dhcpv6::OPT_CLIENTID) {
|
|
Some(dhcpv6::Dhcpv6Option::ClientId(d)) => d.clone(),
|
|
_ => vec![],
|
|
};
|
|
let mut options = vec![
|
|
dhcpv6::Dhcpv6Option::ServerId(self.server_duid.clone()),
|
|
dhcpv6::Dhcpv6Option::ClientId(duid),
|
|
dhcpv6::Dhcpv6Option::IaNa(dhcpv6::IaNa {
|
|
iaid,
|
|
t1,
|
|
t2,
|
|
addrs: vec![dhcpv6::IaAddr {
|
|
addr: ip,
|
|
preferred: self.lease_secs,
|
|
valid: self.lease_secs,
|
|
}],
|
|
}),
|
|
];
|
|
if !self.dns.is_empty() {
|
|
options.push(dhcpv6::Dhcpv6Option::DnsServers(self.dns.clone()));
|
|
}
|
|
dhcpv6::Packet { msg_type, transaction_id: req.transaction_id, options }
|
|
}
|
|
|
|
fn nodata(&self, req: &dhcpv6::Packet, msg_type: dhcpv6::MsgType) -> dhcpv6::Packet {
|
|
let iaid = req.first_iana().map(|ia| ia.iaid).unwrap_or(0);
|
|
let duid = match req.option(dhcpv6::OPT_CLIENTID) {
|
|
Some(dhcpv6::Dhcpv6Option::ClientId(d)) => d.clone(),
|
|
_ => vec![],
|
|
};
|
|
dhcpv6::Packet {
|
|
msg_type,
|
|
transaction_id: req.transaction_id,
|
|
options: vec![
|
|
dhcpv6::Dhcpv6Option::ServerId(self.server_duid.clone()),
|
|
dhcpv6::Dhcpv6Option::ClientId(duid),
|
|
dhcpv6::Dhcpv6Option::IaNa(dhcpv6::IaNa {
|
|
iaid,
|
|
t1: 0,
|
|
t2: 0,
|
|
addrs: vec![],
|
|
}),
|
|
dhcpv6::Dhcpv6Option::StatusCode(
|
|
dhcpv6::STATUS_NO_ADDRS_AVAIL,
|
|
"NoAddrsAvail".to_string(),
|
|
),
|
|
],
|
|
}
|
|
}
|
|
|
|
// Mirrors the updated examples/server.rs serve_v6 Solicit arm, pure return.
|
|
fn handle_solicit(&mut self, req: &dhcpv6::Packet) -> Option<dhcpv6::Packet> {
|
|
let duid = match req.option(dhcpv6::OPT_CLIENTID) {
|
|
Some(dhcpv6::Dhcpv6Option::ClientId(d)) => d.clone(),
|
|
_ => return None,
|
|
};
|
|
debug_assert_eq!(req.msg_type, dhcpv6::MsgType::Solicit);
|
|
if self.enable_rapid_commit && req.option(dhcpv6::OPT_RAPID_COMMIT).is_some() {
|
|
return match self.pool.discover(&duid) {
|
|
Some(offered) => match self.pool.request(&duid, offered) {
|
|
Ok(ip) => Some(self.success(req, dhcpv6::MsgType::Reply, ip)),
|
|
Err(_) => Some(self.nodata(req, dhcpv6::MsgType::Reply)),
|
|
},
|
|
None => Some(self.nodata(req, dhcpv6::MsgType::Reply)),
|
|
};
|
|
}
|
|
match self.pool.discover(&duid) {
|
|
Some(ip) => Some(self.success(req, dhcpv6::MsgType::Advertise, ip)),
|
|
None => Some(self.nodata(req, dhcpv6::MsgType::Advertise)),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn solicit6(duid_bytes: &[u8], iaid: u32, rapid: bool) -> dhcpv6::Packet {
|
|
let mut options = vec![
|
|
dhcpv6::Dhcpv6Option::ClientId(duid_bytes.to_vec()),
|
|
dhcpv6::Dhcpv6Option::IaNa(dhcpv6::IaNa { iaid, t1: 0, t2: 0, addrs: vec![] }),
|
|
];
|
|
if rapid {
|
|
options.push(rapid6_opt());
|
|
}
|
|
dhcpv6::Packet { msg_type: dhcpv6::MsgType::Solicit, transaction_id: 0x112233, options }
|
|
}
|
|
|
|
fn v6codes(p: &dhcpv6::Packet) -> Vec<u16> {
|
|
p.options.iter().map(|o| o.code()).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn v6_rapid_title_present() {
|
|
assert_eq!(dhcpv6::title(dhcpv6::OPT_RAPID_COMMIT), Some("Rapid Commit"));
|
|
}
|
|
|
|
#[test]
|
|
fn v6_rapid_solicit_replies_committed() {
|
|
let mut s = RapidV6Server::new(true);
|
|
let rep = s.handle_solicit(&solicit6(&[1], 0x01020304, true)).unwrap();
|
|
assert_eq!(rep.msg_type, dhcpv6::MsgType::Reply, "rapid commit skips Advertise");
|
|
assert_eq!(rep.transaction_id, 0x112233);
|
|
assert_eq!(v6codes(&rep), vec![2, 1, 3, 23]);
|
|
match rep.option(dhcpv6::OPT_IA_NA) {
|
|
Some(dhcpv6::Dhcpv6Option::IaNa(ia)) => {
|
|
assert_eq!(ia.iaid, 0x01020304, "request IAID echoed");
|
|
assert_eq!(ia.addrs.len(), 1);
|
|
assert_eq!(ia.addrs[0].addr, v6ip(0x101), "first free past start");
|
|
assert_eq!((ia.addrs[0].preferred, ia.addrs[0].valid), (86400, 86400));
|
|
assert_eq!((ia.t1, ia.t2), (43200, 75600));
|
|
}
|
|
_ => panic!("Reply must carry IA_NA"),
|
|
}
|
|
assert!(rep.option(dhcpv6::OPT_STATUS_CODE).is_none());
|
|
// committed server-side: pool holds it for this DUID
|
|
assert_eq!(s.pool.current_lease(&[1]), Some(v6ip(0x101)));
|
|
}
|
|
|
|
#[test]
|
|
fn v6_solicit_without_rapid_advertises() {
|
|
let mut s = RapidV6Server::new(true);
|
|
let rep = s.handle_solicit(&solicit6(&[2], 0x01020304, false)).unwrap();
|
|
assert_eq!(rep.msg_type, dhcpv6::MsgType::Advertise, "no option 14, no rapid path");
|
|
assert!(s.pool.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn v6_rapid_disabled_ignores_option() {
|
|
let mut s = RapidV6Server::new(false);
|
|
let rep = s.handle_solicit(&solicit6(&[3], 0x01020304, true)).unwrap();
|
|
assert_eq!(rep.msg_type, dhcpv6::MsgType::Advertise, "flag off: legacy flow");
|
|
assert!(s.pool.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn v6_rapid_exhausted_nodata_reply() {
|
|
let mut s = RapidV6Server::new(true);
|
|
// fill first 10 of the pool directly (fast, no network)
|
|
for i in 0u8..10 {
|
|
let ip = v6ip(0x100 + i as u16);
|
|
s.pool.request(&[0xA0 + i], ip).unwrap();
|
|
}
|
|
// exhaust the rest through discover+request pairs is slow; instead fill
|
|
// everything except one chunk is overkill — fill via direct inserts.
|
|
for i in 10u16..1000 {
|
|
s.pool.insert(
|
|
v6ip(0x100 + i),
|
|
vec![0xB0, (i & 0xFF) as u8],
|
|
Some(Instant::now() + Duration::from_secs(3600)),
|
|
);
|
|
}
|
|
assert_eq!(s.pool.len(), 1000);
|
|
let rep = s.handle_solicit(&solicit6(&[9], 0x09090909, true)).unwrap();
|
|
assert_eq!(rep.msg_type, dhcpv6::MsgType::Reply, "rapid failure is still a Reply");
|
|
assert_eq!(v6codes(&rep), vec![2, 1, 3, 13]);
|
|
match rep.option(dhcpv6::OPT_IA_NA) {
|
|
Some(dhcpv6::Dhcpv6Option::IaNa(ia)) => {
|
|
assert_eq!(ia.iaid, 0x09090909);
|
|
assert!(ia.addrs.is_empty());
|
|
}
|
|
_ => panic!("nodata Reply keeps an empty IA_NA"),
|
|
}
|
|
assert_eq!(
|
|
rep.option(dhcpv6::OPT_STATUS_CODE),
|
|
Some(&dhcpv6::Dhcpv6Option::StatusCode(
|
|
dhcpv6::STATUS_NO_ADDRS_AVAIL,
|
|
"NoAddrsAvail".to_string()
|
|
))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn v6_rapid_without_iana_acks_iaid_zero() {
|
|
let mut s = RapidV6Server::new(true);
|
|
// ClientId + rapid option but no IA_NA: discover by DUID still works,
|
|
// reply IAID defaults to 0 like the normal path.
|
|
let req = dhcpv6::Packet {
|
|
msg_type: dhcpv6::MsgType::Solicit,
|
|
transaction_id: 0x0A0B0C,
|
|
options: vec![dhcpv6::Dhcpv6Option::ClientId(vec![5]), rapid6_opt()],
|
|
};
|
|
let rep = s.handle_solicit(&req).unwrap();
|
|
assert_eq!(rep.msg_type, dhcpv6::MsgType::Reply);
|
|
match rep.option(dhcpv6::OPT_IA_NA) {
|
|
Some(dhcpv6::Dhcpv6Option::IaNa(ia)) => {
|
|
assert_eq!(ia.iaid, 0);
|
|
assert_eq!(ia.addrs.len(), 1);
|
|
}
|
|
_ => panic!("must still commit"),
|
|
}
|
|
assert_eq!(
|
|
s.pool.current_lease(&[5]),
|
|
rep.option(dhcpv6::OPT_IA_NA).and_then(|ia| match ia {
|
|
dhcpv6::Dhcpv6Option::IaNa(ia) => Some(ia.addrs[0].addr),
|
|
_ => None,
|
|
})
|
|
);
|
|
}
|