mirror of
https://github.com/pj1234678/RustyDHCP.git
synced 2026-10-15 00:06:42 +08:00
637 lines
27 KiB
Rust
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");
|
|
}
|