Files
RustyDHCP/tests/test_dhcpv6.rs
T

650 lines
23 KiB
Rust

//! Tests for `dhcp4r::dhcpv6` (RFC 8415 codec + managed range allocator).
//!
//! Locks the managed DHCPv6 range behavior: message/option wire format,
//! strict all-or-nothing decoding (no v4-style swallow quirk by design),
//! and pool allocation mirroring the v4 example — except infinite leases
//! are NOT stealable here (deliberate difference, see below).
//!
//! Pure codec + allocator tests only (no sockets: loopback IPv6 availability
//! varies by platform, so nothing here binds).
use dhcp4r::dhcpv6::*;
use std::net::Ipv6Addr;
use std::time::{Duration, Instant};
fn duid(b: &[u8]) -> Vec<u8> {
b.to_vec()
}
fn v6(a: [u16; 8]) -> Ipv6Addr {
Ipv6Addr::new(a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7])
}
// ---------------------------------------------------------------------------
// message types
// ---------------------------------------------------------------------------
#[test]
fn msgtype_values_match_rfc8415() {
assert_eq!(MsgType::Solicit as u8, 1);
assert_eq!(MsgType::Advertise as u8, 2);
assert_eq!(MsgType::Request as u8, 3);
assert_eq!(MsgType::Confirm as u8, 4);
assert_eq!(MsgType::Renew as u8, 5);
assert_eq!(MsgType::Rebind as u8, 6);
assert_eq!(MsgType::Reply as u8, 7);
assert_eq!(MsgType::Release as u8, 8);
assert_eq!(MsgType::Decline as u8, 9);
assert_eq!(MsgType::Reconfigure as u8, 10);
assert_eq!(MsgType::InformationRequest as u8, 11);
assert_eq!(MsgType::RelayForward as u8, 12);
assert_eq!(MsgType::RelayReply as u8, 13);
for v in 1u8..=13 {
assert!(MsgType::from(v).is_ok(), "{}", v);
}
}
#[test]
fn msgtype_from_invalid_is_err() {
for v in [0u8, 14, 255] {
assert_eq!(MsgType::from(v), Err(DecodeError::BadMessageType(v)));
}
}
// ---------------------------------------------------------------------------
// transaction id: 24-bit big-endian, masked on encode
// ---------------------------------------------------------------------------
#[test]
fn transaction_id_three_bytes_be() {
let p = Packet {
msg_type: MsgType::Solicit,
transaction_id: 0x12_34_56,
options: vec![],
};
assert_eq!(&p.encode().unwrap()[..4], &[1, 0x12, 0x34, 0x56]);
assert_eq!(Packet::from(&[7, 0x12, 0x34, 0x56]).unwrap().transaction_id, 0x12_34_56);
}
#[test]
fn transaction_id_top_byte_masked_on_encode() {
let p = Packet {
msg_type: MsgType::Solicit,
transaction_id: 0xFF_12_34_56,
options: vec![],
};
let enc = p.encode().unwrap();
assert_eq!(&enc[1..4], &[0x12, 0x34, 0x56]);
assert_eq!(Packet::from(&enc).unwrap().transaction_id, 0x12_34_56);
}
// ---------------------------------------------------------------------------
// option encode lengths
// ---------------------------------------------------------------------------
#[test]
fn option_ids_match_rfc8415() {
assert_eq!(Dhcpv6Option::ClientId(vec![]).code(), 1);
assert_eq!(Dhcpv6Option::ServerId(vec![]).code(), 2);
assert_eq!(
Dhcpv6Option::IaNa(IaNa { iaid: 0, t1: 0, t2: 0, addrs: vec![] }).code(),
3
);
assert_eq!(Dhcpv6Option::DnsServers(vec![]).code(), 23);
assert_eq!(Dhcpv6Option::StatusCode(0, String::new()).code(), 13);
assert_eq!(
Dhcpv6Option::Unrecognized(RawDhcpv6Option { code: 99, data: vec![] }).code(),
99
);
}
#[test]
fn duid_options_opaque_including_empty() {
let raw = Dhcpv6Option::ClientId(duid(&[1, 2, 3])).to_raw();
assert_eq!((raw.code, raw.data), (1, vec![1, 2, 3]));
let raw = Dhcpv6Option::ServerId(vec![]).to_raw();
assert_eq!((raw.code, raw.data), (2, vec![]));
}
#[test]
fn iana_encodes_iaid_t1_t2_then_iaaddr_suboptions() {
let raw = Dhcpv6Option::IaNa(IaNa {
iaid: 0x01020304,
t1: 100,
t2: 200,
addrs: vec![
IaAddr { addr: v6([0xfd00, 0, 0, 0, 0, 0, 0, 1]), preferred: 300, valid: 400 },
IaAddr { addr: v6([0xfd00, 0, 0, 0, 0, 0, 0, 2]), preferred: 301, valid: 401 },
],
})
.to_raw();
assert_eq!(raw.code, 3);
// 12-byte header + 2 sub-options of 28 data bytes each
assert_eq!(raw.data.len(), 12 + 2 * 28);
assert_eq!(&raw.data[..12], &[1, 2, 3, 4, 0, 0, 0, 100, 0, 0, 0, 200]);
// first sub-option: code 5, len 24, addr, preferred, valid
assert_eq!(&raw.data[12..16], &[0, 5, 0, 24]);
assert_eq!(&raw.data[16..32], &v6([0xfd00, 0, 0, 0, 0, 0, 0, 1]).octets());
assert_eq!(&raw.data[32..36], &300u32.to_be_bytes());
assert_eq!(&raw.data[36..40], &400u32.to_be_bytes());
// second sub-option header sits right after the first 24 data bytes
assert_eq!(&raw.data[40..44], &[0, 5, 0, 24]);
// empty IA_NA is just the 12-byte header
let raw = Dhcpv6Option::IaNa(IaNa { iaid: 7, t1: 0, t2: 0, addrs: vec![] }).to_raw();
assert_eq!(raw.data.len(), 12);
}
#[test]
fn dns_servers_concatenates_16_byte_addrs() {
assert!(Dhcpv6Option::DnsServers(vec![]).to_raw().data.is_empty());
let raw = Dhcpv6Option::DnsServers(vec![
v6([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888]),
v6([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8844]),
])
.to_raw();
assert_eq!(raw.code, 23);
assert_eq!(raw.data.len(), 32);
}
#[test]
fn status_code_is_be_code_plus_text() {
let raw = Dhcpv6Option::StatusCode(2, "NoAddrsAvail".to_string()).to_raw();
assert_eq!(raw.code, 13);
assert_eq!(&raw.data[..2], &[0, 2]);
assert_eq!(&raw.data[2..], b"NoAddrsAvail");
let raw = Dhcpv6Option::StatusCode(0, String::new()).to_raw();
assert_eq!(raw.code, 13);
assert_eq!(raw.data, vec![0, 0]);
}
#[test]
fn unrecognized_round_trips_code_and_data() {
let inner = RawDhcpv6Option { code: 60000, data: vec![9, 8, 7] };
assert_eq!(Dhcpv6Option::Unrecognized(inner.clone()).to_raw(), inner);
}
#[test]
fn title_known_and_unknown() {
assert_eq!(title(1), Some("Client Identifier"));
assert_eq!(title(3), Some("Identity Association for Non-temporary Addresses"));
assert_eq!(title(13), Some("Status Code"));
assert_eq!(title(23), Some("DNS Recursive Name Server"));
assert_eq!(title(0), None);
assert_eq!(title(255), None);
assert_eq!(title(60000), None);
}
// ---------------------------------------------------------------------------
// decode errors: strict, whole-packet rejection
// ---------------------------------------------------------------------------
#[test]
fn decode_rejects_truncated_header() {
for len in 0..4 {
let buf = vec![7u8; len];
assert_eq!(Packet::from(&buf), Err(DecodeError::Truncated), "len {}", len);
}
}
#[test]
fn decode_rejects_bad_msgtype() {
assert_eq!(Packet::from(&[0, 0, 0, 1]), Err(DecodeError::BadMessageType(0)));
assert_eq!(Packet::from(&[14, 0, 0, 1]), Err(DecodeError::BadMessageType(14)));
}
#[test]
fn decode_rejects_truncated_option_header_and_data() {
// 3 trailing bytes: no full option header
assert_eq!(
Packet::from(&[1, 0, 0, 1, 53, 1, 2]),
Err(DecodeError::Truncated)
);
// declared len overruns the datagram
assert_eq!(
Packet::from(&[1, 0, 0, 1, 0, 1, 0, 10, 1, 2]),
Err(DecodeError::Truncated)
);
// direct: header needs 4 bytes
assert_eq!(decode_option(&[1, 2, 3]), Err(DecodeError::Truncated));
assert_eq!(decode_option(&[]), Err(DecodeError::Truncated));
}
#[test]
fn decode_malformed_iana_variants() {
// shorter than IAID+T1+T2
assert_eq!(decode_option(&[0, 3, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), Err(DecodeError::Malformed));
// trailing 1-3 bytes after header are not a sub-option
let mut wire = vec![0, 3, 0, 13];
wire.extend_from_slice(&[0u8; 12]);
wire.push(0xAA);
assert_eq!(decode_option(&wire), Err(DecodeError::Malformed));
// sub-option length overruns
let mut wire = vec![0, 3, 0, 14];
wire.extend_from_slice(&[0u8; 12]);
wire.extend_from_slice(&[0, 5, 0, 28, 1, 2]);
assert_eq!(decode_option(&wire), Err(DecodeError::Malformed));
// IAADDR body shorter than 24
let mut wire = vec![0, 3, 0, 12 + 4 + 10];
wire.extend_from_slice(&[0u8; 12]);
wire.extend_from_slice(&[0, 5, 0, 10]);
wire.extend_from_slice(&[0u8; 10]);
assert_eq!(decode_option(&wire), Err(DecodeError::Malformed));
}
#[test]
fn decode_malformed_dns_and_status() {
// DNS length not a multiple of 16
assert_eq!(decode_option(&[0, 23, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), Err(DecodeError::Malformed));
// status shorter than 2-byte code
assert_eq!(decode_option(&[0, 13, 0, 1, 0]), Err(DecodeError::Malformed));
// status message not UTF-8
assert_eq!(
decode_option(&[0, 13, 0, 4, 0, 2, 0xFF, 0xFE]),
Err(DecodeError::BadUtf8)
);
// empty status message is fine
match decode_option(&[0, 13, 0, 2, 0, 0]) {
Ok((_, Dhcpv6Option::StatusCode(0, m))) => assert_eq!(m, ""),
_ => panic!("empty status message"),
}
}
// ---------------------------------------------------------------------------
// packet round-trips (options run to end of datagram: no END byte)
// ---------------------------------------------------------------------------
#[test]
fn packet_empty_options_roundtrip() {
let p = Packet { msg_type: MsgType::Solicit, transaction_id: 1, options: vec![] };
let enc = p.encode().unwrap();
assert_eq!(enc, vec![1, 0, 0, 1]);
assert_eq!(Packet::from(&enc).unwrap(), p);
}
#[test]
fn packet_full_solicit_roundtrip_without_end_byte() {
let p = Packet {
msg_type: MsgType::Solicit,
transaction_id: 0xABCDEF,
options: vec![
Dhcpv6Option::ClientId(duid(&[0, 3, 0, 1, 2, 0, 0x5e, 0xaa, 0xbb, 0xcc])),
Dhcpv6Option::IaNa(IaNa {
iaid: 0x0A0B0C0D,
t1: 0,
t2: 0,
addrs: vec![],
}),
Dhcpv6Option::DnsServers(vec![v6([0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888])]),
Dhcpv6Option::Unrecognized(RawDhcpv6Option { code: 999, data: vec![1, 2] }),
],
};
let enc = p.encode().unwrap();
// no END marker anywhere: last bytes are the final option's data
assert_eq!(&enc[enc.len() - 2..], &[1, 2]);
assert_eq!(Packet::from(&enc).unwrap(), p);
}
#[test]
fn packet_trailing_garbage_rejects_whole_packet() {
// valid option followed by 2 undecodable bytes: strict Err (by design,
// unlike the v4 swallow-and-desync behavior).
let mut enc = Packet {
msg_type: MsgType::Solicit,
transaction_id: 1,
options: vec![Dhcpv6Option::ClientId(duid(&[1]))],
}
.encode()
.unwrap();
enc.extend_from_slice(&[9, 9]);
assert_eq!(Packet::from(&enc), Err(DecodeError::Truncated));
}
#[test]
fn packet_option_first_wins_and_iana_accessor() {
let p = Packet {
msg_type: MsgType::Request,
transaction_id: 2,
options: vec![
Dhcpv6Option::IaNa(IaNa { iaid: 1, t1: 0, t2: 0, addrs: vec![] }),
Dhcpv6Option::IaNa(IaNa { iaid: 2, t1: 0, t2: 0, addrs: vec![] }),
],
};
assert_eq!(p.first_iana().unwrap().iaid, 1);
assert!(p.option(999).is_none());
let bare = Packet { msg_type: MsgType::Solicit, transaction_id: 0, options: vec![] };
assert!(bare.first_iana().is_none());
}
#[test]
fn encode_rejects_option_over_u16_max() {
let p = Packet {
msg_type: MsgType::Solicit,
transaction_id: 1,
options: vec![Dhcpv6Option::Unrecognized(RawDhcpv6Option {
code: 200,
data: vec![0u8; 65536],
})],
};
assert_eq!(p.encode(), Err(EncodeError::OptionTooLarge));
}
#[test]
fn is_for_server_exact_duid_match_only() {
let ours = duid(&[1, 2, 3]);
let mk = |opts| Packet { msg_type: MsgType::Request, transaction_id: 1, options: opts };
assert!(is_for_server(
&ours,
&mk(vec![Dhcpv6Option::ServerId(duid(&[1, 2, 3]))])
));
assert!(!is_for_server(&ours, &mk(vec![Dhcpv6Option::ServerId(vec![])])));
assert!(!is_for_server(
&ours,
&mk(vec![Dhcpv6Option::ServerId(duid(&[1, 2, 4]))])
));
assert!(!is_for_server(&ours, &mk(vec![])), "missing ServerId");
assert!(
!is_for_server(
&ours,
&mk(vec![Dhcpv6Option::Unrecognized(RawDhcpv6Option {
code: 2,
data: duid(&[1, 2, 3])
})])
),
"Unrecognized(2) is not a ServerId"
);
}
#[test]
fn default_t1_t2_halves_and_seven_eighths() {
assert_eq!(default_t1_t2(86400), (43200, 75600));
assert_eq!(default_t1_t2(3600), (1800, 3150));
assert_eq!(default_t1_t2(0), (0, 0));
assert_eq!(default_t1_t2(u32::MAX), (2147483647, 3758096384));
}
// ---------------------------------------------------------------------------
// V6Pool managed range
// ---------------------------------------------------------------------------
fn pool() -> V6Pool {
V6Pool::new(
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x100),
1000,
Duration::from_secs(86400),
)
}
fn ip(n: u16) -> Ipv6Addr {
Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, n)
}
#[test]
fn pool_accessors() {
let p = pool();
assert_eq!(p.start_addr(), ip(0x100));
assert_eq!(p.count(), 1000);
assert_eq!(p.len(), 0);
assert!(p.is_empty());
}
#[test]
fn pool_boundaries_first_offer_and_last() {
let mut p = pool();
// round-robin starts at start+1 like the v4 example
assert_eq!(p.discover(&duid(&[1])), Some(ip(0x101)));
// last usable address is start+count-1
assert!(p.available(&duid(&[2]), &ip(0x100 + 999)));
assert!(!p.available(&duid(&[2]), &ip(0x100 + 1000)), "exclusive upper bound");
assert!(!p.available(&duid(&[2]), &ip(0xFF)), "below pool");
assert!(!p.available(&duid(&[2]), &v6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 1])), "other prefix");
// unleased in-pool address is available
assert!(p.available(&duid(&[3]), &ip(0x200)));
}
#[test]
fn pool_exhaustion_returns_none_then_recovers() {
let mut p = V6Pool::new(ip(0x100), 4, Duration::from_secs(60));
for i in 0u8..4 {
let d = duid(&[i]);
let got = p.discover(&d).unwrap();
p.request(&d, got).unwrap();
}
assert_eq!(p.len(), 4);
assert_eq!(p.discover(&duid(&[9])), None, "pool exhausted");
assert!(p.request(&duid(&[9]), ip(0x101)).is_err());
assert!(p.release(&duid(&[0])));
// freed .101 is first in scan order (round-robin resumes after last=0)
assert_eq!(p.discover(&duid(&[9])), Some(ip(0x101)));
}
#[test]
fn pool_request_prefers_current_without_refresh_quirk() {
let mut p = pool();
let mac = duid(&[7]);
assert_eq!(p.request(&mac, ip(0x110)), Ok(ip(0x110)));
let before = p.get(&ip(0x110)).unwrap().1;
// asking for someone else's IP still returns our own, untouched
assert_eq!(p.request(&mac, ip(0x120)), Ok(ip(0x110)));
assert_eq!(p.get(&ip(0x110)).unwrap().1, before, "no expiry refresh, like v4");
assert!(p.get(&ip(0x120)).is_none());
}
#[test]
fn pool_infinite_leases_are_not_stealable() {
// deliberate difference from the v4 available() quirk: None expiry here
// means permanently reserved, and strangers get Err, not the address.
let mut p = pool();
let owner = duid(&[0xF4, 0x5C]);
let stranger = duid(&[0xDE, 0xAD]);
p.insert(ip(0x190), owner.clone(), None);
assert!(!p.available(&stranger, &ip(0x190)));
assert!(p.request(&stranger, ip(0x190)).is_err());
assert_eq!(p.request(&owner, ip(0x190)), Ok(ip(0x190)));
assert_eq!(p.discover(&owner), Some(ip(0x190)));
}
#[test]
fn pool_expired_leases_reusable() {
let mut p = pool();
p.insert(ip(0x1A0), duid(&[1]), Some(Instant::now() - Duration::from_secs(1)));
assert!(p.available(&duid(&[2]), &ip(0x1A0)));
assert_eq!(p.request(&duid(&[2]), ip(0x1A0)), Ok(ip(0x1A0)));
}
#[test]
fn pool_release_true_false() {
let mut p = pool();
assert!(!p.release(&duid(&[1])), "nothing held");
assert_eq!(p.request(&duid(&[1]), ip(0x1B0)), Ok(ip(0x1B0)));
assert!(p.release(&duid(&[1])));
assert!(!p.release(&duid(&[1])), "already gone");
assert!(p.is_empty());
}
#[test]
fn pool_duid_identity_is_exact_bytes() {
let mut p = pool();
assert_eq!(p.request(&duid(&[1, 2]), ip(0x1C0)), Ok(ip(0x1C0)));
// prefix-extended DUID is a different client: blocked
assert!(!p.available(&duid(&[1, 2, 3]), &ip(0x1C0)));
assert_eq!(p.current_lease(&duid(&[1, 2, 3])), None);
assert_eq!(p.current_lease(&duid(&[1, 2])), Some(ip(0x1C0)));
}
#[test]
fn pool_empty_count_never_panics() {
let mut p = V6Pool::new(ip(0x100), 0, Duration::from_secs(60));
assert_eq!(p.discover(&duid(&[1])), None);
assert!(p.request(&duid(&[1]), ip(0x100)).is_err());
assert!(!p.available(&duid(&[1]), &ip(0x100)));
}
#[test]
fn pool_max_start_never_panics() {
// start near u128::MAX with a count reaching past it: the exclusive end
// overflows, so every candidate is out of range — but small enough to
// scan quickly (a huge count must never mean a huge loop).
let near_max = Ipv6Addr::from(u128::MAX - 3);
let mut p = V6Pool::new(near_max, 16, Duration::from_secs(60));
assert_eq!(p.discover(&duid(&[1])), None, "overflowed candidates skipped");
assert!(!p.available(&duid(&[1]), &near_max));
assert!(p.request(&duid(&[1]), near_max).is_err());
// sheer count size alone must not hang the scan either: an empty pool
// returns None after scanning, and the loop is bounded by count — keep
// counts under test small by construction (see pool_exhaustion test).
let mut tiny = V6Pool::new(ip(0x100), 0, Duration::from_secs(60));
assert_eq!(tiny.discover(&duid(&[1])), None);
}
// ---------------------------------------------------------------------------
// coverageg gaps: sub-option skipping, identity edges, pool corners
// ---------------------------------------------------------------------------
#[test]
fn iana_skips_unknown_suboptions_keeps_iaaddrs() {
// unknown sub-option (code 99) interleaved between two IAADDRs: skipped
// by length while both addresses still decode.
let mut data = vec![0u8; 12]; // iaid/t1/t2 zeroes
data[0..4].copy_from_slice(&9u32.to_be_bytes());
let addr = |n: u16| {
let mut sub = vec![0u8, 5, 0, 24];
sub.extend_from_slice(&v6([0xfd00, 0, 0, 0, 0, 0, 0, n]).octets());
sub.extend_from_slice(&100u32.to_be_bytes());
sub.extend_from_slice(&200u32.to_be_bytes());
sub
};
data.extend_from_slice(&addr(1));
data.extend_from_slice(&[0, 99, 0, 2, 0xAA, 0xBB]); // unknown, skipped
data.extend_from_slice(&addr(2));
let mut wire = vec![0, 3, (data.len() >> 8) as u8, (data.len() & 0xFF) as u8];
wire.extend_from_slice(&data);
match decode_option(&wire) {
Ok((_, Dhcpv6Option::IaNa(ia))) => {
assert_eq!(ia.iaid, 9);
assert_eq!(
ia.addrs.iter().map(|a| a.addr).collect::<Vec<_>>(),
vec![v6([0xfd00, 0, 0, 0, 0, 0, 0, 1]), v6([0xfd00, 0, 0, 0, 0, 0, 0, 2])]
);
}
_ => panic!("unknown sub-options must be skipped"),
}
}
#[test]
fn iaaddr_trailing_subsuboptions_ignored() {
// IAADDR data longer than 24 bytes: first 24 parse, rest ignored.
let mut body = vec![0u8; 0];
body.extend_from_slice(&v6([0xfd00, 0, 0, 0, 0, 0, 0, 9]).octets());
body.extend_from_slice(&300u32.to_be_bytes());
body.extend_from_slice(&400u32.to_be_bytes());
body.extend_from_slice(&[9, 9, 9, 9, 9, 9]); // sub-sub-options, ignored
let mut data = vec![0u8; 12];
let mut wire = vec![0, 3, 0, 0]; // len patched below
let mut opt = vec![0, 5, 0, body.len() as u8];
opt.extend_from_slice(&body);
data.extend_from_slice(&opt);
wire[2..4].copy_from_slice(&(data.len() as u16).to_be_bytes());
wire.extend_from_slice(&data);
match decode_option(&wire) {
Ok((_, Dhcpv6Option::IaNa(ia))) => {
assert_eq!(ia.addrs.len(), 1);
assert_eq!(ia.addrs[0].addr, v6([0xfd00, 0, 0, 0, 0, 0, 0, 9]));
assert_eq!((ia.addrs[0].preferred, ia.addrs[0].valid), (300, 400));
}
_ => panic!("trailing sub-sub-option bytes must be ignored"),
}
}
#[test]
fn is_for_server_first_identifier_wins() {
let ours = duid(&[1, 2, 3]);
let mk = |opts| Packet { msg_type: MsgType::Request, transaction_id: 1, options: opts };
assert!(is_for_server(
&ours,
&mk(vec![
Dhcpv6Option::ServerId(duid(&[1, 2, 3])),
Dhcpv6Option::ServerId(duid(&[9, 9, 9])),
])
));
assert!(!is_for_server(
&ours,
&mk(vec![
Dhcpv6Option::ServerId(duid(&[9, 9, 9])),
Dhcpv6Option::ServerId(duid(&[1, 2, 3])),
])
));
}
#[test]
fn empty_clientid_decodes() {
match decode_option(&[0, 1, 0, 0]) {
Ok((rest, Dhcpv6Option::ClientId(d))) => {
assert!(d.is_empty());
assert!(rest.is_empty());
}
_ => panic!("empty ClientId must decode"),
}
// and it round-trips through a packet
let p = Packet {
msg_type: MsgType::Solicit,
transaction_id: 5,
options: vec![Dhcpv6Option::ClientId(vec![])],
};
assert_eq!(Packet::from(&p.encode().unwrap()).unwrap(), p);
}
#[test]
fn opaque_messages_roundtrip() {
// Confirm / Reconfigure / InformationRequest / RelayForward / RelayReply
// carry no special-cased structure: header + opaque options round-trip.
for mt in [
MsgType::Confirm,
MsgType::Reconfigure,
MsgType::InformationRequest,
MsgType::RelayForward,
MsgType::RelayReply,
] {
let p = Packet {
msg_type: mt,
transaction_id: 0x0A0B0C,
options: vec![
Dhcpv6Option::ClientId(duid(&[7])),
Dhcpv6Option::Unrecognized(RawDhcpv6Option {
code: 65000,
data: vec![1, 2, 3],
}),
],
};
let enc = p.encode().unwrap();
assert_eq!(enc[0], mt as u8);
assert_eq!(Packet::from(&enc).unwrap(), p);
}
}
#[test]
fn transaction_id_zero_and_max_roundtrip() {
for tid in [0u32, 0xFF_FF_FF] {
let p = Packet { msg_type: MsgType::Solicit, transaction_id: tid, options: vec![] };
let enc = p.encode().unwrap();
assert_eq!(Packet::from(&enc).unwrap().transaction_id, tid);
}
}
#[test]
fn pool_request_out_of_range_err_no_insert() {
let mut p = pool();
assert!(p.request(&duid(&[1]), v6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 1])).is_err());
assert!(p.is_empty(), "out-of-range request must not insert");
}
#[test]
fn pool_single_address_pool_cycles() {
// count == 1: modulo-1 scan cannot divide by zero (loop runs once) and
// the lone address is offered, committed, then reported current.
let mut p = V6Pool::new(ip(0x100), 1, Duration::from_secs(60));
assert_eq!(p.discover(&duid(&[1])), Some(ip(0x100)));
assert_eq!(p.request(&duid(&[1]), ip(0x100)), Ok(ip(0x100)));
assert_eq!(p.discover(&duid(&[1])), Some(ip(0x100)));
assert_eq!(p.discover(&duid(&[2])), None, "taken single slot");
}