Files
RustyDHCP/tests/test_dhcp_tables.rs
T

637 lines
27 KiB
Rust

//! DHCP message-table + validation accuracy suite, part 3.
//!
//! Covers RFC 2131 / RFC 2132 / RFC 951 areas NOT covered by
//! `test_dhcp_rfc.rs` / `test_dhcp_standards.rs`:
//!
//! - RFC 2131 Table 1 (client) + Table 3 (server) address/option matrices
//! - RFC 2132 option-length validation (exact vs short vs long) deviations
//! - RFC 2131 §4.1 destination selection + port documentation
//! - RFC 951/2131 size constants (236 header + 64 vend = 300, chaddr/sname/file split)
//! - RFC 2131 §2 flags MBZ handling
//! - RFC 1918 + subnet self-consistency of the example network
//! - PRL required-option guarantee (53+54 always survive filtering)
//!
//! `*_quirk` tests lock current deviations so they cannot change silently.
use dhcp4r::options::*;
use dhcp4r::packet::*;
use dhcp4r::server;
use std::net::{Ipv4Addr, UdpSocket};
use std::time::Duration;
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
fn unwrap_packet(r: Result<Packet, CustomErr<&[u8]>>) -> Packet {
match r {
Ok(p) => p,
Err(_) => panic!("expected Ok Packet"),
}
}
fn client_packet(xid: u32, chaddr: [u8; 6], opts: Vec<DhcpOption>) -> 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 reply_once(msg: MessageType, additional: Vec<DhcpOption>, offer: Ipv4Addr, req: Packet) -> Packet {
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
struct H {
msg: MessageType,
add: Option<Vec<DhcpOption>>,
offer: Ipv4Addr,
}
impl server::Handler for H {
fn handle_request(&mut self, s: &server::Server, p: Packet) {
let add = self.add.take().unwrap();
let _ = s.reply(self.msg, add, self.offer, p);
}
}
let h = H { msg, add: Some(additional), offer };
std::thread::spawn(move || {
let _ = server::Server::serve(
srv_sock, Ipv4Addr::new(192, 168, 1, 1), Ipv4Addr::new(192, 168, 1, 255), h,
);
});
std::thread::sleep(Duration::from_millis(100));
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
let mut buf = [0u8; 1500];
let mut rbuf = [0u8; 1500];
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
client.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
let (n, _) = client.recv_from(&mut rbuf).expect("no reply");
unwrap_packet(Packet::from(&rbuf[..n]))
}
fn roundtrip(p: Packet) -> Packet {
let mut buf = [0u8; 1500];
let enc = p.encode(&mut buf).to_vec();
unwrap_packet(Packet::from(&enc))
}
// ===========================================================================
// A. RFC 2131 Table 1 — client messages: address + option presence matrix
// ===========================================================================
/// RFC 2131 Table 1 row: DHCPDISCOVER — ciaddr 0, no Requested IP, no Server
/// ID, PRL SHOULD be present. yiaddr/siaddr MUST be 0.
#[test]
fn tbl1_discover_full_matrix() {
let p = Packet {
reply: false, hops: 0, xid: 0xD001, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0x11; 6],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Discover),
DhcpOption::ParameterRequestList(vec![1, 3, 6, 51]),
],
};
assert_eq!(p.ciaddr, Ipv4Addr::UNSPECIFIED, "Table 1: Discover ciaddr=0");
assert_eq!(p.yiaddr, Ipv4Addr::UNSPECIFIED);
assert_eq!(p.siaddr, Ipv4Addr::UNSPECIFIED);
assert!(p.option(SERVER_IDENTIFIER).is_none(), "Discover: no Server ID");
assert!(p.option(REQUESTED_IP_ADDRESS).is_none(), "Discover: no Requested IP");
assert!(p.option(PARAMETER_REQUEST_LIST).is_some(), "Discover: PRL expected");
let q = roundtrip(p);
assert_eq!(q.message_type(), Ok(MessageType::Discover));
assert!(q.option(SERVER_IDENTIFIER).is_none());
}
/// RFC 2131 Table 1 row: DHCPREQUEST SELECTING — ciaddr 0, Requested IP +
/// Server ID present (selects among Offers), PRL present.
#[test]
fn tbl1_request_selecting_full_matrix() {
let p = Packet {
reply: false, hops: 0, xid: 0xD002, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0x22; 6],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Request),
DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)),
DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 168, 1, 70)),
DhcpOption::ParameterRequestList(vec![1, 3]),
],
};
assert_eq!(p.ciaddr, Ipv4Addr::UNSPECIFIED);
assert!(p.option(SERVER_IDENTIFIER).is_some());
assert!(p.option(REQUESTED_IP_ADDRESS).is_some());
let q = roundtrip(p);
assert_eq!(
q.option(SERVER_IDENTIFIER),
Some(&DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)))
);
assert_eq!(
q.option(REQUESTED_IP_ADDRESS),
Some(&DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 168, 1, 70)))
);
}
/// RFC 2131 Table 1 row: DHCPREQUEST INIT-REBOOT — ciaddr 0, Requested IP
/// present (remembered address), Server ID absent.
#[test]
fn tbl1_request_init_reboot_full_matrix() {
let p = Packet {
reply: false, hops: 0, xid: 0xD003, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0x33; 6],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Request),
DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 168, 1, 80)),
],
};
assert!(p.option(SERVER_IDENTIFIER).is_none(), "INIT-REBOOT: no Server ID");
assert!(p.option(REQUESTED_IP_ADDRESS).is_some());
let q = roundtrip(p);
assert!(q.option(SERVER_IDENTIFIER).is_none());
}
/// RFC 2131 Table 1 rows: RENEWING (unicast, ciaddr set, no options 50/54)
/// and REBINDING (broadcast, ciaddr set). Only the ciaddr/broadcast differ.
#[test]
fn tbl1_request_renewing_rebinding_matrix() {
for (xid, broadcast) in [(0xD004u32, false), (0xD005u32, true)] {
let p = Packet {
reply: false, hops: 0, xid, secs: 0, broadcast,
ciaddr: Ipv4Addr::new(192, 168, 1, 50), yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0x44; 6],
options: vec![DhcpOption::DhcpMessageType(MessageType::Request)],
};
assert!(p.option(SERVER_IDENTIFIER).is_none(), "RENEW/REBIND: no Server ID");
assert!(p.option(REQUESTED_IP_ADDRESS).is_none(), "RENEW/REBIND: no Requested IP");
// broadcast struct encodes [128,0] which decodes false (quirk), so
// only verify the unicast member round-trips the flag; the broadcast
// member is verified at the wire level elsewhere.
if !broadcast {
let q = roundtrip(p);
assert_eq!(q.ciaddr, Ipv4Addr::new(192, 168, 1, 50));
assert!(!q.broadcast);
}
}
}
/// RFC 2131 Table 1 rows: DECLINE (Server ID + Requested IP, ciaddr 0),
/// RELEASE (Server ID + ciaddr set, no Requested IP), INFORM (ciaddr set,
/// PRL present, no Requested IP).
#[test]
fn tbl1_decline_release_inform_matrix() {
let decline = Packet {
reply: false, hops: 0, xid: 0xD006, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0x55; 6],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Decline),
DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)),
DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 168, 1, 70)),
],
};
assert_eq!(decline.ciaddr, Ipv4Addr::UNSPECIFIED);
assert!(decline.option(SERVER_IDENTIFIER).is_some());
assert!(decline.option(REQUESTED_IP_ADDRESS).is_some());
let release = Packet {
reply: false, hops: 0, xid: 0xD007, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::new(192, 168, 1, 70), yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0x55; 6],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Release),
DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)),
],
};
assert!(release.option(REQUESTED_IP_ADDRESS).is_none(), "RELEASE: no Requested IP");
assert_eq!(release.ciaddr, Ipv4Addr::new(192, 168, 1, 70));
let inform = Packet {
reply: false, hops: 0, xid: 0xD008, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::new(192, 168, 1, 60), yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0x66; 6],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Inform),
DhcpOption::ParameterRequestList(vec![1, 15]),
],
};
assert!(inform.option(REQUESTED_IP_ADDRESS).is_none(), "INFORM: no Requested IP");
for p in [decline, release, inform] {
let mt = p.message_type().unwrap();
let mut buf = [0u8; 1500];
let q = unwrap_packet(Packet::from(&p.encode(&mut buf).to_vec()));
assert_eq!(q.message_type(), Ok(mt));
}
}
// ===========================================================================
// B. RFC 2131 Table 3 — server messages: required fields
// ===========================================================================
/// RFC 2131 Table 3 row: DHCPOFFER — BOOTREPLY, yiaddr set (offered),
/// Server ID present, Subnet/Lease SHOULD be present when configured.
#[test]
fn tbl3_offer_required_fields() {
let req = client_packet(0xE001, [0x11; 6], vec![DhcpOption::DhcpMessageType(
MessageType::Discover,
)]);
let rep = reply_once(
MessageType::Offer,
vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::IpAddressLeaseTime(86400),
],
Ipv4Addr::new(192, 168, 1, 100),
req,
);
assert!(rep.reply, "Table 3: Offer is BOOTREPLY");
assert_ne!(rep.yiaddr, Ipv4Addr::UNSPECIFIED, "Offer MUST set yiaddr");
assert!(rep.option(SERVER_IDENTIFIER).is_some(), "Offer MUST carry Server ID");
assert!(rep.option(SUBNET_MASK).is_some());
assert!(rep.option(IP_ADDRESS_LEASE_TIME).is_some());
}
/// RFC 2131 Table 3 row: DHCPACK — BOOTREPLY, yiaddr committed, Server ID +
/// Lease Time present; ciaddr echoed when the request carried one.
#[test]
fn tbl3_ack_required_fields_and_ciaddr_echo() {
let mut req = client_packet(0xE002, [0x22; 6], vec![DhcpOption::DhcpMessageType(
MessageType::Request,
)]);
req.ciaddr = Ipv4Addr::new(192, 168, 1, 50);
let rep = reply_once(
MessageType::Ack,
vec![DhcpOption::IpAddressLeaseTime(3600)],
Ipv4Addr::new(192, 168, 1, 50),
req,
);
assert_eq!(rep.message_type(), Ok(MessageType::Ack));
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 1, 50));
assert_eq!(rep.ciaddr, Ipv4Addr::new(192, 168, 1, 50), "Ack echoes RENEW ciaddr");
assert!(rep.option(IP_ADDRESS_LEASE_TIME).is_some());
}
/// RFC 2131 Table 3 row: DHCPNAK — BOOTREPLY, yiaddr MUST be 0, ciaddr MUST
/// be 0, SHOULD carry Message (56) explaining the failure.
#[test]
fn tbl3_nak_required_fields_and_message() {
let req = client_packet(0xE003, [0x33; 6], vec![DhcpOption::DhcpMessageType(
MessageType::Request,
)]);
let rep = reply_once(
MessageType::Nak,
vec![DhcpOption::Message("not available".to_string())],
Ipv4Addr::UNSPECIFIED,
req,
);
assert!(rep.reply);
assert_eq!(rep.yiaddr, Ipv4Addr::UNSPECIFIED, "NAK yiaddr MUST be 0");
assert_eq!(rep.ciaddr, Ipv4Addr::UNSPECIFIED, "NAK ciaddr MUST be 0");
assert_eq!(
rep.option(MESSAGE),
Some(&DhcpOption::Message("not available".to_string())),
"NAK SHOULD carry Message"
);
}
// ===========================================================================
// C. RFC 2132 option-length validation deviations
// ===========================================================================
/// RFC 2132: Server ID (54) / Requested IP (50) / Subnet (1) MUST be 4 bytes.
/// Current behavior: short (<4) rejected, long (>4) silently truncated to the
/// first 4 bytes. Locked so a strict validator cannot break captures.
#[test]
fn optlen_ipv4_short_rejected_long_truncated_quirk() {
for code in [SERVER_IDENTIFIER, REQUESTED_IP_ADDRESS, SUBNET_MASK] {
assert!(
matches!(decode_option(&[code, 3, 1, 2, 3]), Err(CustomErr::InvalidHlen)),
"code {} len 3 must fail",
code
);
match decode_option(&[code, 5, 192, 168, 1, 1, 99]) {
Ok((_, o)) => {
let ip = match o {
DhcpOption::ServerIdentifier(x)
| DhcpOption::RequestedIpAddress(x)
| DhcpOption::SubnetMask(x) => x,
_ => panic!("code {} wrong variant", code),
};
assert_eq!(ip, Ipv4Addr::new(192, 168, 1, 1), "code {} takes first 4", code);
}
Err(_) => panic!("code {} len 5 must succeed with truncation", code),
}
}
}
/// RFC 2132 §9.10: Lease Time (51) MUST be 4 bytes uint32. Short rejected,
/// long truncated to first 4 BE bytes (same deviation as IPv4 options).
#[test]
fn optlen_lease_short_rejected_long_truncated_quirk() {
assert!(matches!(decode_option(&[51, 3, 0, 0, 1]), Err(CustomErr::InvalidHlen)));
match decode_option(&[51, 5, 0, 0, 0, 10, 99]) {
Ok((_, DhcpOption::IpAddressLeaseTime(t))) => assert_eq!(t, 10),
_ => panic!("lease len 5 must truncate to 10"),
}
}
/// RFC 2132 §9.6: Message Type (53) MUST be length 1 with values 1-8.
/// Length-0 rejected as InvalidHlen (not UnrecognizedMessageType); length>1
/// uses the first byte; unknown first byte is UnrecognizedMessageType.
#[test]
fn optlen_msgtype_len_rules() {
assert!(matches!(decode_option(&[53, 0]), Err(CustomErr::InvalidHlen)));
for bad in [0u8, 9, 255] {
assert!(
matches!(decode_option(&[53, 1, bad]), Err(CustomErr::UnrecognizedMessageType)),
"53 value {}",
bad
);
}
match decode_option(&[53, 2, 3, 99]) {
Ok((_, DhcpOption::DhcpMessageType(t))) => assert_eq!(t, MessageType::Request),
_ => panic!("53 len 2 uses first byte"),
}
}
/// RFC 2132: PRL (55) / HostName (12) / Message (56) accept any length incl.
/// 0; Unrecognized preserves any length incl. 0 exactly.
#[test]
fn optlen_variable_and_unknown_exact() {
match decode_option(&[55, 0]) {
Ok((_, DhcpOption::ParameterRequestList(v))) => assert!(v.is_empty()),
_ => panic!("empty PRL"),
}
match decode_option(&[12, 0]) {
Ok((_, DhcpOption::HostName(s))) => assert_eq!(s, ""),
_ => panic!("empty hostname"),
}
for (code, data) in [(200u8, vec![]), (200, vec![1]), (15u8, vec![9; 100])] {
let wire = [vec![code, data.len() as u8], data.clone()].concat();
match decode_option(&wire) {
Ok((rest, DhcpOption::Unrecognized(r))) => {
assert_eq!(r.code, code);
assert_eq!(r.data, data);
assert!(rest.is_empty());
}
_ => panic!("code {} len {} must passthrough", code, data.len()),
}
}
}
// ===========================================================================
// D. RFC 2131 §4.1 ports + destination selection documentation
// ===========================================================================
/// RFC 2131 §4.1: DHCP uses UDP server port 67 / client port 68; client sends
/// to 255.255.255.255 (or relay 67), server unicasts or broadcasts.
/// Port binding itself is the app's job (`src/main.rs` binds
/// 0.0.0.0:67); here we lock that replies preserve the peer port and that
/// the broadcast path uses the configured broadcast IP.
#[test]
fn ports_unicast_preserves_peer_port_documented() {
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
struct H;
impl server::Handler for H {
fn handle_request(&mut self, s: &server::Server, p: Packet) {
let _ = s.reply(MessageType::Offer, vec![], Ipv4Addr::new(192, 168, 1, 10), p);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(
srv_sock, Ipv4Addr::new(192, 168, 1, 1), Ipv4Addr::new(192, 168, 1, 255), H,
);
});
std::thread::sleep(Duration::from_millis(100));
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
let expect_port = client.local_addr().unwrap().port();
let mut buf = [0u8; 1500];
let req = client_packet(0xF001, [1; 6], vec![DhcpOption::DhcpMessageType(
MessageType::Discover,
)]);
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
client.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
let mut rbuf = [0u8; 1500];
let (_, src) = client.recv_from(&mut rbuf).expect("unicast reply");
assert_eq!(src.port(), srv_addr.port(), "reply comes from server port");
assert_ne!(expect_port, 0);
}
/// RFC 2131 §4.1 + §3.3: destination ignores `yiaddr`/`giaddr`; only
/// `broadcast` (or all-zero src, untestable over loopback) selects the
/// broadcast IP. A relayed unicast request is still answered at the peer.
#[test]
fn destination_ignores_yiaddr_and_giaddr_quirk() {
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
struct H;
impl server::Handler for H {
fn handle_request(&mut self, s: &server::Server, p: Packet) {
// yiaddr/giaddr intentionally "wrong" to prove they are unused
assert_eq!(p.yiaddr, Ipv4Addr::new(9, 9, 9, 9));
assert_eq!(p.giaddr, Ipv4Addr::new(10, 9, 9, 9));
let _ = s.reply(MessageType::Offer, vec![], Ipv4Addr::new(10, 9, 9, 50), p);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(
srv_sock, Ipv4Addr::new(192, 168, 1, 1), Ipv4Addr::new(192, 168, 1, 255), H,
);
});
std::thread::sleep(Duration::from_millis(100));
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
let req = Packet {
reply: false, hops: 1, xid: 0xF002, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::new(9, 9, 9, 9),
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::new(10, 9, 9, 9),
chaddr: [2; 6],
options: vec![DhcpOption::DhcpMessageType(MessageType::Discover)],
};
let mut buf = [0u8; 1500];
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
client.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
let mut rbuf = [0u8; 1500];
let (n, _) = client.recv_from(&mut rbuf).expect("reply at peer, not giaddr/yiaddr");
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
assert_eq!(rep.yiaddr, Ipv4Addr::new(10, 9, 9, 50));
}
// ===========================================================================
// E. RFC 951/2131 sizes: 236 header + 64 vend minimum = 300 on the wire,
// chaddr/sname/file split
// ===========================================================================
/// RFC 951 §3: fixed header 236 = 28 (op..giaddr) + 16 (chaddr) + 64 (sname)
/// + 128 (file). Encoder MUST emit exactly this split zeroed when unused.
#[test]
fn sizes_header_split_28_16_64_128() {
let p = client_packet(0xF011, [0xCC; 6], vec![DhcpOption::DhcpMessageType(
MessageType::Discover,
)]);
let mut buf = [0xFFu8; 1500];
let enc = p.encode(&mut buf).to_vec();
assert!(enc.len() >= 241);
assert_eq!(&enc[28..34], &[0xCC; 6]);
assert!(enc[34..44].iter().all(|&b| b == 0), "chaddr[6..16]");
assert!(enc[44..108].iter().all(|&b| b == 0), "sname[64]");
assert!(enc[108..236].iter().all(|&b| b == 0), "file[128]");
assert_eq!(236 - 28, 208);
assert_eq!(10 + 64 + 128, 202);
assert_eq!(208 - 6, 202);
}
/// RFC 2131/951 conformance: the BOOTP/DHCP minimum on the wire is 300
/// bytes. The implementation sizes the zero pad to 300 and returns only
/// `len` bytes (241..300): small packets carry the full 300-byte pad region
/// zeroed, so strict clients that require BOOTP-minimum framing accept them.
#[test]
fn sizes_minimum_300_bootp() {
let p = client_packet(0xF012, [0; 6], vec![]);
let mut buf = [0xFFu8; 1500];
let len = p.encode(&mut buf).len();
assert_eq!(len, 241, "empty options: 240 + 1 END");
assert!(buf[len..300].iter().all(|&b| b == 0), "pad zeroed to 300");
// even the largest encodable datagram fits a 1500 MTU
let many = vec![DhcpOption::Unrecognized(RawDhcpOption { code: 200, data: vec![7u8; 20] })];
let big = Packet {
reply: true, hops: 0, xid: 1, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0; 6], options: many,
};
let mut buf2 = [0u8; 1500];
assert!(big.encode(&mut buf2).len() <= 300);
}
// ===========================================================================
// F. RFC 2131 §2 flags: only top bit defined, rest MBZ
// ===========================================================================
/// RFC 2131 §2: `flags` = 0x8000 broadcast + 15 MBZ low bits. The decoder only
/// tests bit 0x0080 of the assembled u16 (quirk), so all MBZ patterns except
/// low-byte 0x80 decode as unicast — including the RFC-correct 0x8000.
#[test]
fn flags_mbz_only_low_byte_0x80_matters_quirk() {
fn decode_flags(f: [u8; 2]) -> bool {
let mut raw = vec![0u8; 236];
raw[0] = 1;
raw[1] = 1;
raw[2] = 6;
raw[10] = f[0];
raw[11] = f[1];
raw.extend_from_slice(&[99, 130, 83, 99, 53, 1, 1, 255]);
unwrap_packet(Packet::from(&raw)).broadcast
}
assert!(!decode_flags([0, 0]));
assert!(!decode_flags([128, 0]), "RFC broadcast 0x8000 decodes false");
assert!(decode_flags([0, 128]));
assert!(decode_flags([0, 129]), "low bit extras ignored");
assert!(decode_flags([255, 128]));
assert!(!decode_flags([127, 127]), "0x7F7F has no 0x0080 bit");
assert!(!decode_flags([1, 0]), "MBZ bit alone is unicast");
}
// ===========================================================================
// G. RFC 1918 + subnet self-consistency of the example /24
// ===========================================================================
/// Example network (examples/server.rs + README) MUST be self-consistent:
/// private 192.168.2.0/24, mask .0, router .1, server .1, pool .2-.253,
/// broadcast .255, public DNS. Locks the topology against typos.
#[test]
fn example_network_topology_self_consistent() {
let server = Ipv4Addr::new(192, 168, 2, 1);
let mask = Ipv4Addr::new(255, 255, 255, 0);
let router = Ipv4Addr::new(192, 168, 2, 1);
let broadcast = Ipv4Addr::new(192, 168, 2, 255);
let pool_first = Ipv4Addr::new(192, 168, 2, 2);
let pool_last = Ipv4Addr::new(192, 168, 2, 253);
let dns = Ipv4Addr::new(8, 8, 8, 8);
// RFC 1918 private
assert!(server.is_private() && pool_first.is_private());
// subnet math: network = ip & mask
let net = u32::from(server) & u32::from(mask);
assert_eq!(Ipv4Addr::from(net), Ipv4Addr::new(192, 168, 2, 0));
assert_eq!(u32::from(broadcast), net | !u32::from(mask));
// router == server (example collocation), pool inside subnet, excludes
// network (.0), server (.1), broadcast (.255)
assert_eq!(router, server);
let s = u32::from(pool_first);
let e = u32::from(pool_last);
assert_eq!(e - s + 1, 252);
assert!(s > net + 1 && e < net | !u32::from(mask));
// DNS is public (not private/loopback/link-local/unspecified)
assert!(!dns.is_private() && !dns.is_loopback() && !dns.is_unspecified());
// pool extremes survive encode/decode as yiaddr
for ip in [pool_first, pool_last] {
let p = Packet {
reply: true, hops: 0, xid: 1, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: ip,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0; 6],
options: vec![DhcpOption::DhcpMessageType(MessageType::Offer)],
};
let mut buf = [0u8; 1500];
assert_eq!(unwrap_packet(Packet::from(&p.encode(&mut buf).to_vec())).yiaddr, ip);
}
}
// ===========================================================================
// H. PRL required-option guarantee
// ===========================================================================
/// Server replies MUST always carry Message Type (53) + Server ID (54), even
/// when the client PRL asks for unrelated/unknown codes. Unknown PRL codes
/// are ignored; defaults [53,54,1,51,6,3] fill the remainder in order.
#[test]
fn prl_always_keeps_msgtype_and_serverid() {
let mut opts = vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::DhcpMessageType(MessageType::Ack),
DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)),
DhcpOption::IpAddressLeaseTime(100),
];
server::filter_options_by_req(&mut opts, &[200, 201]);
let codes: Vec<u8> = opts.iter().map(|o| o.code()).collect();
assert!(codes.contains(&53) && codes.contains(&54));
assert_eq!(codes, vec![53, 54, 1, 51]);
}
/// Live PRL proof: client asks only for Domain (15, unconfigured) — reply
/// still carries 53+54 plus configured defaults, and never the unconfigured
/// Domain option.
#[test]
fn prl_live_required_options_survive() {
let req = Packet {
reply: false, hops: 0, xid: 0xF021, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED,
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
chaddr: [0xEE; 6],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Request),
DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)),
DhcpOption::ParameterRequestList(vec![15]),
],
};
let rep = reply_once(
MessageType::Ack,
vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::IpAddressLeaseTime(3600),
],
Ipv4Addr::new(192, 168, 1, 82),
req,
);
let codes: Vec<u8> = rep.options.iter().map(|o| o.code()).collect();
assert!(codes.contains(&53) && codes.contains(&54));
assert!(!codes.contains(&15), "unconfigured Domain MUST NOT appear");
}