mirror of
https://github.com/pj1234678/RustyDHCP.git
synced 2026-10-15 00:06:42 +08:00
700 lines
25 KiB
Rust
700 lines
25 KiB
Rust
//! Exact-functionality regression tests for `dhcp4r::packet`.
|
|
//!
|
|
//! Locks current wire format, including known quirks:
|
|
//! - unknown `op` decodes as request (`reply=false`)
|
|
//! - broadcast flag checks `flags & 128` (low byte 0x80), NOT RFC 0x8000,
|
|
//! so `encode(broadcast=true)` -> raw `[128,0]` -> `decode` yields `false`
|
|
//! - `decode_option` panics on END (255) as first byte
|
|
//! - `Router` / `DomainNameServer` decode ALWAYS fails with `InvalidHlen`
|
|
//! (custom_many0 expects NomError terminator but decode_ipv4 gives InvalidHlen)
|
|
//! - `Packet::from` swallows all option-level errors and succeeds with
|
|
//! truncated/empty options; it only errors on header/cookie/hlen issues
|
|
//! - missing END with exact consumption panics on `split_at(1)`
|
|
|
|
use dhcp4r::options::*;
|
|
use dhcp4r::packet::*;
|
|
use std::net::Ipv4Addr;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// helpers (CustomErr has no Debug, so no unwrap())
|
|
// ---------------------------------------------------------------------------
|
|
|
|
fn unwrap_packet(r: Result<Packet, CustomErr<&[u8]>>) -> Packet {
|
|
match r {
|
|
Ok(p) => p,
|
|
Err(_) => panic!("expected Ok Packet, got Err"),
|
|
}
|
|
}
|
|
|
|
fn is_invalid_hlen(r: &Result<Packet, CustomErr<&[u8]>>) -> bool {
|
|
matches!(r, Err(CustomErr::InvalidHlen))
|
|
}
|
|
|
|
fn is_tag_error(r: &Result<Packet, CustomErr<&[u8]>>) -> bool {
|
|
matches!(r, Err(CustomErr::NomError(_)))
|
|
}
|
|
|
|
/// Build a raw DHCP packet: 236-byte BOOTP header + cookie + options bytes.
|
|
/// Header: op,htype=1,hlen,hops,xid,secs,flags,ci,yi,si,gi,chaddr(6)+pad.
|
|
#[allow(clippy::too_many_arguments)]
|
|
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);
|
|
// 34..236 already zero (sname/file/chaddr-pad)
|
|
v.extend_from_slice(&[99, 130, 83, 99]); // magic cookie
|
|
v.extend_from_slice(&opts_after_cookie);
|
|
v
|
|
}
|
|
|
|
fn make_simple_raw(op: u8, flags: [u8; 2], hlen: u8, opts: Vec<u8>) -> Vec<u8> {
|
|
make_raw(
|
|
op,
|
|
hlen,
|
|
7,
|
|
0x12345678,
|
|
0x0102,
|
|
flags,
|
|
[1, 2, 3, 4],
|
|
[5, 6, 7, 8],
|
|
[9, 10, 11, 12],
|
|
[13, 14, 15, 16],
|
|
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
|
|
opts,
|
|
)
|
|
}
|
|
|
|
fn opt_raw(code: u8, data: &[u8]) -> Vec<u8> {
|
|
let mut v = vec![code, data.len() as u8];
|
|
v.extend_from_slice(data);
|
|
v
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// decode: header fields
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn decode_valid_discover_preserves_all_header_fields() {
|
|
let raw = make_raw(
|
|
1, 6, 9, 0xAABBCCDD, 0x1122, [0, 0],
|
|
[192, 168, 1, 10], [192, 168, 1, 20], [192, 168, 1, 1], [0, 0, 0, 0],
|
|
[1, 2, 3, 4, 5, 6],
|
|
[vec![53, 1, 1], vec![255]].concat(),
|
|
);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert!(!p.reply);
|
|
assert_eq!(p.hops, 9);
|
|
assert_eq!(p.xid, 0xAABBCCDD);
|
|
assert_eq!(p.secs, 0x1122);
|
|
assert!(!p.broadcast);
|
|
assert_eq!(p.ciaddr, Ipv4Addr::new(192, 168, 1, 10));
|
|
assert_eq!(p.yiaddr, Ipv4Addr::new(192, 168, 1, 20));
|
|
assert_eq!(p.siaddr, Ipv4Addr::new(192, 168, 1, 1));
|
|
assert_eq!(p.giaddr, Ipv4Addr::new(0, 0, 0, 0));
|
|
assert_eq!(p.chaddr, [1, 2, 3, 4, 5, 6]);
|
|
assert_eq!(p.options.len(), 1);
|
|
assert_eq!(p.options[0], DhcpOption::DhcpMessageType(MessageType::Discover));
|
|
}
|
|
|
|
#[test]
|
|
fn decode_reply_flag_op2_true_op1_false() {
|
|
let req = unwrap_packet(Packet::from(&make_simple_raw(1, [0, 0], 6, vec![53, 1, 1, 255])));
|
|
assert!(!req.reply);
|
|
let rep = unwrap_packet(Packet::from(&make_simple_raw(2, [0, 0], 6, vec![53, 1, 2, 255])));
|
|
assert!(rep.reply);
|
|
}
|
|
|
|
#[test]
|
|
fn decode_unknown_op_treated_as_request_quirk() {
|
|
// Current code maps any non-2 op to false (TODO in source).
|
|
for op in [0u8, 3, 99, 255] {
|
|
let p = unwrap_packet(Packet::from(&make_simple_raw(op, [0, 0], 6, vec![53, 1, 1, 255])));
|
|
assert!(!p.reply, "op {} should decode to reply=false", op);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_rejects_bad_hlen() {
|
|
for hlen in [0u8, 1, 5, 7, 16, 255] {
|
|
let raw = make_simple_raw(1, [0, 0], hlen, vec![53, 1, 1, 255]);
|
|
let r = Packet::from(&raw);
|
|
assert!(is_invalid_hlen(&r), "hlen {} should be InvalidHlen", hlen);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_rejects_short_header() {
|
|
assert!(is_invalid_hlen(&Packet::from(&[1u8; 10])));
|
|
assert!(is_invalid_hlen(&Packet::from(&[0u8; 235])));
|
|
assert!(is_invalid_hlen(&Packet::from(&[])));
|
|
}
|
|
|
|
#[test]
|
|
fn decode_rejects_bad_cookie() {
|
|
let mut raw = make_simple_raw(1, [0, 0], 6, vec![53, 1, 1, 255]);
|
|
raw[236] = 0;
|
|
raw[237] = 0;
|
|
raw[238] = 0;
|
|
raw[239] = 0;
|
|
let r = Packet::from(&raw);
|
|
assert!(is_tag_error(&r), "bad cookie should be NomError(Tag)");
|
|
}
|
|
|
|
#[test]
|
|
fn decode_broadcast_flag_checks_low_byte_quirk() {
|
|
// encode(true) writes flags [128,0] (0x8000) but decode checks `flags & 128`
|
|
// (0x0080), so it decodes to false. Only [x,128|..] decodes true.
|
|
let p1 = unwrap_packet(Packet::from(&make_simple_raw(1, [128, 0], 6, vec![53, 1, 1, 255])));
|
|
assert!(!p1.broadcast, "[128,0] (0x8000) decodes to false (bug locked)");
|
|
|
|
let p2 = unwrap_packet(Packet::from(&make_simple_raw(1, [0, 128], 6, vec![53, 1, 1, 255])));
|
|
assert!(p2.broadcast, "[0,128] (0x0080) decodes to true");
|
|
|
|
let p3 = unwrap_packet(Packet::from(&make_simple_raw(1, [0, 0], 6, vec![53, 1, 1, 255])));
|
|
assert!(!p3.broadcast);
|
|
|
|
let p4 = unwrap_packet(Packet::from(&make_simple_raw(1, [255, 255], 6, vec![53, 1, 1, 255])));
|
|
assert!(p4.broadcast, "[255,255] contains 0x80 in low byte");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// decode_option direct: each type
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn decode_option_message_type_all_values() {
|
|
for (byte, expect) in [
|
|
(1, MessageType::Discover),
|
|
(2, MessageType::Offer),
|
|
(3, MessageType::Request),
|
|
(4, MessageType::Decline),
|
|
(5, MessageType::Ack),
|
|
(6, MessageType::Nak),
|
|
(7, MessageType::Release),
|
|
(8, MessageType::Inform),
|
|
] {
|
|
match decode_option(&[53, 1, byte]) {
|
|
Ok((_, DhcpOption::DhcpMessageType(t))) => assert_eq!(t, expect),
|
|
other => panic!("53,1,{} should Ok, got {:?}", byte, other.is_ok()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_option_message_type_invalid_is_unrecognized() {
|
|
assert!(matches!(
|
|
decode_option(&[53, 1, 9]),
|
|
Err(CustomErr::UnrecognizedMessageType)
|
|
));
|
|
assert!(matches!(
|
|
decode_option(&[53, 1, 0]),
|
|
Err(CustomErr::UnrecognizedMessageType)
|
|
));
|
|
assert!(matches!(
|
|
decode_option(&[53, 1, 255]),
|
|
Err(CustomErr::UnrecognizedMessageType)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn decode_option_server_id_requested_ip_subnet() {
|
|
match decode_option(&[54, 4, 192, 168, 1, 1]) {
|
|
Ok((_, DhcpOption::ServerIdentifier(ip))) => assert_eq!(ip, Ipv4Addr::new(192, 168, 1, 1)),
|
|
_ => panic!("server id failed"),
|
|
}
|
|
match decode_option(&[50, 4, 10, 0, 0, 5]) {
|
|
Ok((_, DhcpOption::RequestedIpAddress(ip))) => assert_eq!(ip, Ipv4Addr::new(10, 0, 0, 5)),
|
|
_ => panic!("requested ip failed"),
|
|
}
|
|
match decode_option(&[1, 4, 255, 255, 255, 0]) {
|
|
Ok((_, DhcpOption::SubnetMask(ip))) => assert_eq!(ip, Ipv4Addr::new(255, 255, 255, 0)),
|
|
_ => panic!("subnet failed"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_option_ipv4_ignores_trailing_bytes_quirk() {
|
|
// decode_ipv4 only reads first 4 bytes, extra ignored.
|
|
match decode_option(&[54, 5, 192, 168, 1, 1, 9]) {
|
|
Ok((_, DhcpOption::ServerIdentifier(ip))) => assert_eq!(ip, Ipv4Addr::new(192, 168, 1, 1)),
|
|
_ => panic!("should ignore trailing byte"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_option_ipv4_short_is_invalid_hlen() {
|
|
assert!(matches!(
|
|
decode_option(&[54, 3, 1, 2, 3]),
|
|
Err(CustomErr::InvalidHlen)
|
|
));
|
|
assert!(matches!(
|
|
decode_option(&[51, 2, 0, 1]),
|
|
Err(CustomErr::InvalidHlen)
|
|
));
|
|
assert!(matches!(decode_option(&[53, 0]), Err(CustomErr::InvalidHlen)));
|
|
assert!(matches!(decode_option(&[53, 1]), Err(CustomErr::InvalidHlen)));
|
|
assert!(matches!(decode_option(&[]), Err(CustomErr::InvalidHlen)));
|
|
assert!(matches!(decode_option(&[53]), Err(CustomErr::InvalidHlen)));
|
|
}
|
|
|
|
#[test]
|
|
fn decode_option_hostname_and_message() {
|
|
match decode_option(&[12, 3, 65, 66, 67]) {
|
|
Ok((_, DhcpOption::HostName(s))) => assert_eq!(s, "ABC"),
|
|
_ => panic!("hostname failed"),
|
|
}
|
|
match decode_option(&[56, 5, 104, 101, 108, 108, 111]) {
|
|
Ok((_, DhcpOption::Message(s))) => assert_eq!(s, "hello"),
|
|
_ => panic!("message failed"),
|
|
}
|
|
assert!(matches!(
|
|
decode_option(&[12, 2, 0xFF, 0xFE]),
|
|
Err(CustomErr::NonUtf8String)
|
|
));
|
|
assert!(matches!(
|
|
decode_option(&[56, 2, 0xFF, 0xFE]),
|
|
Err(CustomErr::NonUtf8String)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn decode_option_prl_and_lease_and_unrecognized() {
|
|
match decode_option(&[55, 3, 1, 3, 6]) {
|
|
Ok((_, DhcpOption::ParameterRequestList(v))) => assert_eq!(v, vec![1, 3, 6]),
|
|
_ => panic!("prl failed"),
|
|
}
|
|
match decode_option(&[55, 0]) {
|
|
Ok((_, DhcpOption::ParameterRequestList(v))) => assert!(v.is_empty()),
|
|
_ => panic!("empty prl failed"),
|
|
}
|
|
match decode_option(&[51, 4, 0, 0, 0, 10]) {
|
|
Ok((_, DhcpOption::IpAddressLeaseTime(t))) => assert_eq!(t, 10),
|
|
_ => panic!("lease failed"),
|
|
}
|
|
match decode_option(&[99, 2, 1, 2]) {
|
|
Ok((_, DhcpOption::Unrecognized(raw))) => {
|
|
assert_eq!(raw.code, 99);
|
|
assert_eq!(raw.data, vec![1, 2]);
|
|
}
|
|
_ => panic!("unrecognized failed"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_option_router_and_dns_always_fail_quirk() {
|
|
// BUG LOCK: custom_many0(decode_ipv4) can never succeed because
|
|
// decode_ipv4 signals exhaustion with InvalidHlen, not NomError,
|
|
// so the combinator propagates Err instead of terminating.
|
|
// Even a single valid address fails.
|
|
assert!(matches!(
|
|
decode_option(&[3, 4, 192, 168, 1, 1]),
|
|
Err(CustomErr::InvalidHlen)
|
|
));
|
|
assert!(matches!(
|
|
decode_option(&[3, 8, 192, 168, 1, 1, 10, 0, 0, 1]),
|
|
Err(CustomErr::InvalidHlen)
|
|
));
|
|
assert!(matches!(decode_option(&[3, 0]), Err(CustomErr::InvalidHlen)));
|
|
assert!(matches!(
|
|
decode_option(&[6, 4, 8, 8, 8, 8]),
|
|
Err(CustomErr::InvalidHlen)
|
|
));
|
|
assert!(matches!(decode_option(&[6, 0]), Err(CustomErr::InvalidHlen)));
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn decode_option_end_as_first_byte_panics_quirk() {
|
|
let _ = decode_option(&[255, 1, 1]);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Packet::from swallowing / END handling quirks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn packet_from_swallows_invalid_msgtype_and_yields_empty_options() {
|
|
// decode_option would Err(UnrecognizedMessageType), but Packet::from
|
|
// catches it in `while let Ok` and still returns Ok with 0 options.
|
|
let raw = make_simple_raw(1, [0, 0], 6, vec![53, 1, 9, 255]);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert!(p.options.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn packet_from_swallows_non_utf8_and_yields_empty_options() {
|
|
let raw = make_simple_raw(1, [0, 0], 6, vec![12, 2, 0xFF, 0xFE, 255]);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert!(p.options.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn packet_from_swallows_router_and_yields_empty_options() {
|
|
let mut opts = opt_raw(3, &[192, 168, 1, 1]);
|
|
opts.push(255);
|
|
let raw = make_simple_raw(1, [0, 0], 6, opts);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert!(p.options.is_empty(), "router poisons whole list: {:?}", p.options);
|
|
}
|
|
|
|
#[test]
|
|
fn packet_from_keeps_decodable_prefix_before_poison_option() {
|
|
// [MsgType, Subnet] decode, then Router fails -> first two kept.
|
|
let mut opts = vec![];
|
|
opts.extend(opt_raw(53, &[1]));
|
|
opts.extend(opt_raw(1, &[255, 255, 255, 0]));
|
|
opts.extend(opt_raw(3, &[192, 168, 1, 1]));
|
|
opts.push(255);
|
|
let raw = make_simple_raw(1, [0, 0], 6, opts);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert_eq!(p.options.len(), 2);
|
|
assert_eq!(p.options[0], DhcpOption::DhcpMessageType(MessageType::Discover));
|
|
assert_eq!(
|
|
p.options[1],
|
|
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn packet_from_truncated_option_swallows_and_succeeds() {
|
|
// len says 1 but no data and no END: decode_option fails, loop exits,
|
|
// split_at(1) skips one byte, Ok with empty options.
|
|
let raw = make_simple_raw(1, [0, 0], 6, vec![53, 1]);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert!(p.options.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn packet_from_with_only_end_panics_quirk() {
|
|
let raw = make_simple_raw(1, [0, 0], 6, vec![255, 0, 0]);
|
|
let _ = Packet::from(&raw);
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn packet_from_missing_end_with_exact_consumption_panics_quirk() {
|
|
// One valid option with no END consumes all bytes -> rest empty -> split_at(1) panics.
|
|
let raw = make_simple_raw(1, [0, 0], 6, vec![53, 1, 1]);
|
|
let _ = Packet::from(&raw);
|
|
}
|
|
|
|
#[test]
|
|
fn packet_from_ignores_trailing_bytes_after_end() {
|
|
let mut opts = opt_raw(53, &[1]);
|
|
opts.push(255);
|
|
opts.extend_from_slice(&[0, 0, 0, 99, 100]); // PAD + garbage ignored
|
|
let raw = make_simple_raw(1, [0, 0], 6, opts);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert_eq!(p.options.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn packet_from_multiple_options_preserves_order() {
|
|
let mut opts = vec![];
|
|
opts.extend(opt_raw(53, &[3]));
|
|
opts.extend(opt_raw(54, &[192, 168, 1, 1]));
|
|
opts.extend(opt_raw(55, &[1, 3, 6]));
|
|
opts.extend(opt_raw(50, &[192, 168, 1, 100]));
|
|
opts.extend(opt_raw(1, &[255, 255, 255, 0]));
|
|
opts.extend(opt_raw(51, &[0, 0, 0, 10]));
|
|
opts.extend(opt_raw(12, b"host"));
|
|
opts.extend(opt_raw(56, b"msg"));
|
|
opts.push(255);
|
|
let raw = make_simple_raw(1, [0, 0], 6, opts);
|
|
let p = unwrap_packet(Packet::from(&raw));
|
|
assert_eq!(p.options.len(), 8);
|
|
assert_eq!(p.options[0].code(), 53);
|
|
assert_eq!(p.options[1].code(), 54);
|
|
assert_eq!(p.options[2].code(), 55);
|
|
assert_eq!(p.options[3].code(), 50);
|
|
assert_eq!(p.options[4].code(), 1);
|
|
assert_eq!(p.options[5].code(), 51);
|
|
assert_eq!(p.options[6].code(), 12);
|
|
assert_eq!(p.options[7].code(), 56);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// option() and message_type()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn packet_option_finds_by_code() {
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6],
|
|
options: vec![
|
|
DhcpOption::DhcpMessageType(MessageType::Discover),
|
|
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
|
|
],
|
|
};
|
|
assert!(p.option(53).is_some());
|
|
assert!(p.option(1).is_some());
|
|
assert!(p.option(3).is_none());
|
|
assert!(p.option(255).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn packet_message_type_ok() {
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6],
|
|
options: vec![DhcpOption::DhcpMessageType(MessageType::Request)],
|
|
};
|
|
assert_eq!(p.message_type(), Ok(MessageType::Request));
|
|
}
|
|
|
|
#[test]
|
|
fn packet_message_type_missing_is_err() {
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6], options: vec![],
|
|
};
|
|
let r = p.message_type();
|
|
assert!(r.is_err());
|
|
assert!(r.unwrap_err().contains("MessageType"));
|
|
}
|
|
|
|
#[test]
|
|
fn packet_message_type_wrong_variant_is_err() {
|
|
// Manually inject Unrecognized with code 53 (decoder never produces this,
|
|
// but message_type() has a branch for it).
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6],
|
|
options: vec![DhcpOption::Unrecognized(RawDhcpOption { code: 53, data: vec![1] })],
|
|
};
|
|
let r = p.message_type();
|
|
assert!(r.is_err());
|
|
assert!(r.unwrap_err().contains("53"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// encode()
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn encode_header_bytes_are_exact() {
|
|
let p = Packet {
|
|
reply: true, hops: 9, xid: 0xAABBCCDD, secs: 0x1122, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(1, 2, 3, 4), yiaddr: Ipv4Addr::new(5, 6, 7, 8),
|
|
siaddr: Ipv4Addr::new(9, 10, 11, 12), giaddr: Ipv4Addr::new(13, 14, 15, 16),
|
|
chaddr: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
|
|
options: vec![DhcpOption::DhcpMessageType(MessageType::Offer)],
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
// op=2 (reply), htype always 1, hlen 6, hops
|
|
assert_eq!(enc[0], 2);
|
|
assert_eq!(enc[1], 1);
|
|
assert_eq!(enc[2], 6);
|
|
assert_eq!(enc[3], 9);
|
|
assert_eq!(&enc[4..8], &[0xAA, 0xBB, 0xCC, 0xDD]);
|
|
assert_eq!(&enc[8..10], &[0x11, 0x22]);
|
|
assert_eq!(&enc[10..12], &[0, 0]); // broadcast false
|
|
assert_eq!(&enc[12..16], &[1, 2, 3, 4]);
|
|
assert_eq!(&enc[16..20], &[5, 6, 7, 8]);
|
|
assert_eq!(&enc[20..24], &[9, 10, 11, 12]);
|
|
assert_eq!(&enc[24..28], &[13, 14, 15, 16]);
|
|
assert_eq!(&enc[28..34], &[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
|
// sname/file/chaddr-pad zeroed
|
|
assert!(enc[34..236].iter().all(|&b| b == 0));
|
|
// cookie
|
|
assert_eq!(&enc[236..240], &[99, 130, 83, 99]);
|
|
// option + END
|
|
assert_eq!(&enc[240..243], &[53, 1, 2]);
|
|
assert_eq!(enc[243], 255);
|
|
assert_eq!(enc.len(), 244);
|
|
}
|
|
|
|
#[test]
|
|
fn encode_request_op_and_broadcast_flag() {
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 1, secs: 0, broadcast: true,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6], options: vec![DhcpOption::DhcpMessageType(MessageType::Discover)],
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
assert_eq!(enc[0], 1);
|
|
assert_eq!(&enc[10..12], &[128, 0], "broadcast true must encode as [128,0]");
|
|
}
|
|
|
|
#[test]
|
|
fn encode_empty_options_yields_only_end() {
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6], options: vec![],
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
assert_eq!(enc.len(), 241);
|
|
assert_eq!(enc[240], 255);
|
|
}
|
|
|
|
#[test]
|
|
fn encode_pads_buffer_to_300_with_zero() {
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6], options: vec![DhcpOption::DhcpMessageType(MessageType::Discover)],
|
|
};
|
|
let mut buf = [0xFFu8; 1500];
|
|
let len = p.encode(&mut buf).len();
|
|
assert_eq!(len, 244);
|
|
// bytes after returned slice up to 300 must be PAD (0)
|
|
assert!(buf[len..300].iter().all(|&b| b == 0));
|
|
}
|
|
|
|
#[test]
|
|
fn encode_truncates_options_to_fit_300() {
|
|
// Each Unrecognized(10 bytes data) costs 12 bytes. Start 240, END 1.
|
|
// 240+12*4=288, next would be 300 >= 300 -> break. So 4 fit.
|
|
let many: Vec<DhcpOption> = (0..50)
|
|
.map(|i| DhcpOption::Unrecognized(RawDhcpOption { code: 100, data: vec![i; 10] }))
|
|
.collect();
|
|
let p = Packet {
|
|
reply: true, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(1, 1, 1, 1),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [0; 6], options: many,
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
assert!(enc.len() <= 300, "len {}", enc.len());
|
|
assert_eq!(enc.len(), 289);
|
|
assert_eq!(enc[enc.len() - 1], 255);
|
|
// only first four options present
|
|
assert_eq!(&enc[240..252], &[100, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
}
|
|
|
|
#[test]
|
|
fn encode_broadcast_roundtrip_loses_flag_quirk() {
|
|
let p = Packet {
|
|
reply: true, hops: 0, xid: 0x12345678, secs: 0, broadcast: true,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(192, 168, 1, 10),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [1, 2, 3, 4, 5, 6],
|
|
options: vec![DhcpOption::DhcpMessageType(MessageType::Offer)],
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
assert_eq!(&enc[10..12], &[128, 0]);
|
|
let dec = unwrap_packet(Packet::from(&enc));
|
|
assert!(!dec.broadcast, "roundtrip of broadcast=true yields false (bug locked)");
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_preserves_decodable_fields() {
|
|
// NOTE: total options must fit in the 300-byte packet (240 + opts + 1 END).
|
|
// 3+6+6+6+5 = 26 bytes -> 267 total, fits. Adding more would trigger
|
|
// truncation (see encode_truncates_options_to_fit_300).
|
|
let p = Packet {
|
|
reply: true, hops: 3, xid: 0xDEADBEEF, secs: 99, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(192, 168, 1, 10), yiaddr: Ipv4Addr::new(192, 168, 1, 20),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(192, 168, 1, 1),
|
|
chaddr: [10, 20, 30, 40, 50, 60],
|
|
options: vec![
|
|
DhcpOption::DhcpMessageType(MessageType::Ack),
|
|
DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)),
|
|
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
|
|
DhcpOption::IpAddressLeaseTime(3600),
|
|
DhcpOption::ParameterRequestList(vec![1, 3, 6]),
|
|
],
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
let q = unwrap_packet(Packet::from(&enc));
|
|
assert_eq!(q.reply, p.reply);
|
|
assert_eq!(q.hops, p.hops);
|
|
assert_eq!(q.xid, p.xid);
|
|
assert_eq!(q.secs, p.secs);
|
|
assert_eq!(q.broadcast, p.broadcast);
|
|
assert_eq!(q.ciaddr, p.ciaddr);
|
|
assert_eq!(q.yiaddr, p.yiaddr);
|
|
assert_eq!(q.siaddr, p.siaddr);
|
|
assert_eq!(q.giaddr, p.giaddr);
|
|
assert_eq!(q.chaddr, p.chaddr);
|
|
assert_eq!(q.options, p.options);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_preserves_hostname_message_requestedip_when_fitting() {
|
|
let p = Packet {
|
|
reply: false, hops: 0, xid: 0x99, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [1, 2, 3, 4, 5, 6],
|
|
options: vec![
|
|
DhcpOption::DhcpMessageType(MessageType::Discover),
|
|
DhcpOption::HostName("h".to_string()),
|
|
DhcpOption::Message("m".to_string()),
|
|
DhcpOption::RequestedIpAddress(Ipv4Addr::new(192, 168, 1, 20)),
|
|
],
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
let q = unwrap_packet(Packet::from(&enc));
|
|
assert_eq!(q.options, p.options);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_loses_router_and_dns_quirk() {
|
|
let p = Packet {
|
|
reply: true, hops: 0, xid: 1, secs: 0, broadcast: false,
|
|
ciaddr: Ipv4Addr::new(0, 0, 0, 0), yiaddr: Ipv4Addr::new(10, 0, 0, 2),
|
|
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
|
|
chaddr: [1, 2, 3, 4, 5, 6],
|
|
options: vec![
|
|
DhcpOption::DhcpMessageType(MessageType::Offer),
|
|
DhcpOption::ServerIdentifier(Ipv4Addr::new(10, 0, 0, 1)),
|
|
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
|
|
DhcpOption::Router(vec![Ipv4Addr::new(10, 0, 0, 1)]),
|
|
],
|
|
};
|
|
let mut buf = [0u8; 1500];
|
|
let enc = p.encode(&mut buf).to_vec();
|
|
// raw bytes DO contain router option 3 ...
|
|
assert!(enc.windows(2).any(|w| w == [3, 4]), "raw must contain router");
|
|
// ... but decode drops it and everything after it
|
|
let q = unwrap_packet(Packet::from(&enc));
|
|
assert_eq!(q.options.len(), 3, "router + tail lost: {:?}", q.options);
|
|
}
|