Files

729 lines
28 KiB
Rust

//! Exact-functionality regression tests for `dhcp4r::server`.
use dhcp4r::options::*;
use dhcp4r::packet::*;
use dhcp4r::server;
use std::net::{Ipv4Addr, UdpSocket};
use std::sync::mpsc;
use std::time::Duration;
fn unwrap_packet(r: Result<Packet, CustomErr<&[u8]>>) -> Packet {
match r {
Ok(p) => p,
Err(_) => panic!("expected Ok Packet"),
}
}
fn test_packet(xid: u32, chaddr: [u8; 6], opts: Vec<DhcpOption>) -> Packet {
Packet {
reply: false, hops: 0, xid, 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, options: opts,
}
}
fn codes(opts: &[DhcpOption]) -> Vec<u8> {
opts.iter().map(|o| o.code()).collect()
}
// ---------------------------------------------------------------------------
// filter_options_by_req: pure function, exact ordering/truncation locked
// ---------------------------------------------------------------------------
#[test]
fn filter_empty_opts_stays_empty() {
let mut opts: Vec<DhcpOption> = vec![];
server::filter_options_by_req(&mut opts, &[1, 3, 6]);
assert!(opts.is_empty());
let mut opts: Vec<DhcpOption> = vec![];
server::filter_options_by_req(&mut opts, &[]);
assert!(opts.is_empty());
}
#[test]
fn filter_no_req_keeps_only_default_set_in_default_order() {
// h = [53,54,1,51,6,3]
let mut opts = vec![
DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 1)]), // 3
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)), // 1
DhcpOption::Message("hi".to_string()), // 56 not in h
DhcpOption::DhcpMessageType(MessageType::Offer), // 53
];
server::filter_options_by_req(&mut opts, &[]);
// 56 dropped; rest ordered by h: 53,1,3 (54,51,6 absent)
assert_eq!(codes(&opts), vec![53, 1, 3]);
}
#[test]
fn filter_req_order_comes_first_then_defaults() {
let mut opts = vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)), // 1
DhcpOption::Router(vec![Ipv4Addr::new(192, 168, 1, 1)]), // 3
DhcpOption::DomainNameServer(vec![Ipv4Addr::new(8, 8, 8, 8)]), // 6
DhcpOption::IpAddressLeaseTime(86400), // 51
DhcpOption::DhcpMessageType(MessageType::Offer), // 53
DhcpOption::ServerIdentifier(Ipv4Addr::new(192, 168, 1, 1)), // 54
];
server::filter_options_by_req(&mut opts, &[6, 1]);
// req [6,1] first, then remaining h in h-order [53,54,51,3]
assert_eq!(codes(&opts), vec![6, 1, 53, 54, 51, 3]);
}
#[test]
fn filter_drops_options_not_in_req_or_defaults() {
let mut opts = vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::Message("extra".to_string()), // 56 never kept
DhcpOption::HostName("h".to_string()), // 12 never kept
DhcpOption::ParameterRequestList(vec![1]), // 55 never kept (not in h)
];
server::filter_options_by_req(&mut opts, &[]);
assert_eq!(codes(&opts), vec![1]);
let mut opts = vec![DhcpOption::Message("hi".to_string())];
server::filter_options_by_req(&mut opts, &[1]);
assert!(opts.is_empty(), "nothing matched -> truncated to 0");
}
#[test]
fn filter_unknown_req_codes_ignored() {
let mut opts = vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::DhcpMessageType(MessageType::Offer),
];
server::filter_options_by_req(&mut opts, &[200, 201]);
// unknown req adds nothing; defaults still apply: [53,1]
// (order: req finds nothing, h finds 53 then 1)
assert_eq!(codes(&opts), vec![53, 1]);
}
#[test]
fn filter_duplicate_req_does_not_duplicate() {
let mut opts = vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::DhcpMessageType(MessageType::Offer),
];
server::filter_options_by_req(&mut opts, &[1, 1, 53, 53]);
assert_eq!(codes(&opts), vec![1, 53]);
assert_eq!(opts.len(), 2);
}
#[test]
fn filter_does_not_invent_missing_options() {
let mut opts = vec![DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0))];
server::filter_options_by_req(&mut opts, &[1, 3, 6, 53, 54, 51]);
// only 1 present
assert_eq!(codes(&opts), vec![1]);
}
// ---------------------------------------------------------------------------
// integration helpers: real UDP loopback, Server via serve thread
// ---------------------------------------------------------------------------
fn recv_with_timeout(sock: &UdpSocket, buf: &mut [u8]) -> (usize, std::net::SocketAddr) {
sock.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
sock.recv_from(buf).expect("timed out waiting for reply")
}
#[test]
fn for_this_server_true_only_on_matching_id() {
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
let server_ip = Ipv4Addr::new(10, 20, 30, 40);
let (tx, rx) = mpsc::channel::<Vec<bool>>();
struct H {
tx: mpsc::Sender<Vec<bool>>,
}
impl server::Handler for H {
fn handle_request(&mut self, s: &server::Server, p: Packet) {
// For xid 4 we also check a manually-built packet holding
// Unrecognized(54): wire code 54 ALWAYS decodes as ServerIdentifier,
// so only a manual (non-decoded) Unrecognized hits the `_ => false` branch.
if p.xid == 4 {
let wire = s.for_this_server(&p);
let manual = Packet {
reply: false, hops: 0, xid: 4, 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: 54,
data: Ipv4Addr::new(10, 20, 30, 40).octets().to_vec(),
})],
};
let manual_res = s.for_this_server(&manual);
let _ = self.tx.send(vec![wire, manual_res]);
} else {
let _ = self.tx.send(vec![s.for_this_server(&p)]);
}
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(srv_sock, server_ip, Ipv4Addr::new(10, 20, 30, 255), H { tx });
});
std::thread::sleep(Duration::from_millis(100));
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
let mut buf = [0u8; 1500];
// matching
let p = test_packet(1, [1, 2, 3, 4, 5, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Request),
DhcpOption::ServerIdentifier(server_ip),
]);
let enc = p.encode(&mut buf).to_vec();
client.send_to(&enc, srv_addr).unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), vec![true]);
// different IP
let p = test_packet(2, [1, 2, 3, 4, 5, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Request),
DhcpOption::ServerIdentifier(Ipv4Addr::new(1, 1, 1, 1)),
]);
let enc = p.encode(&mut buf).to_vec();
client.send_to(&enc, srv_addr).unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), vec![false]);
// missing option
let p = test_packet(3, [1, 2, 3, 4, 5, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Request),
]);
let enc = p.encode(&mut buf).to_vec();
client.send_to(&enc, srv_addr).unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), vec![false]);
// Unrecognized with code 54 on the WIRE decodes as ServerIdentifier,
// so for_this_server is true (quirk locked). A manual Unrecognized(54)
// that never went through decode hits `_ => false`.
let p = test_packet(4, [1, 2, 3, 4, 5, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Request),
DhcpOption::Unrecognized(RawDhcpOption { code: 54, data: server_ip.octets().to_vec() }),
]);
let enc = p.encode(&mut buf).to_vec();
client.send_to(&enc, srv_addr).unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), vec![true, false]);
}
#[test]
fn reply_preserves_xid_chaddr_giaddr_and_sets_fixed_fields() {
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
let server_ip = Ipv4Addr::new(192, 168, 2, 1);
struct H;
impl server::Handler for H {
fn handle_request(&mut self, s: &server::Server, in_packet: Packet) {
// use only decodable options so client can verify all survive
let _ = s.reply(
MessageType::Offer,
vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::IpAddressLeaseTime(86400),
],
Ipv4Addr::new(192, 168, 2, 100),
in_packet,
);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(srv_sock, server_ip, Ipv4Addr::new(192, 168, 2, 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];
let req = Packet {
reply: false, hops: 5, xid: 0xAABBCCDD, secs: 77, broadcast: false,
ciaddr: Ipv4Addr::new(192, 168, 2, 50), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(192, 168, 2, 254),
chaddr: [9, 8, 7, 6, 5, 4],
options: vec![DhcpOption::DhcpMessageType(MessageType::Discover)],
};
let enc = req.encode(&mut buf).to_vec();
client.send_to(&enc, srv_addr).unwrap();
let (n, _) = recv_with_timeout(&client, &mut rbuf);
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
assert!(rep.reply, "reply flag must be true");
assert_eq!(rep.hops, 0, "reply hops forced to 0");
assert_eq!(rep.xid, 0xAABBCCDD, "xid echoed");
assert_eq!(rep.secs, 0, "reply secs forced to 0");
assert_eq!(rep.chaddr, [9, 8, 7, 6, 5, 4]);
assert_eq!(rep.giaddr, Ipv4Addr::new(192, 168, 2, 254), "giaddr echoed");
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 2, 100));
assert_eq!(rep.siaddr, Ipv4Addr::new(0, 0, 0, 0), "siaddr forced to 0.0.0.0");
assert_eq!(rep.ciaddr, Ipv4Addr::new(192, 168, 2, 50), "ciaddr echoed for non-Nak");
assert_eq!(rep.message_type().unwrap(), MessageType::Offer);
// auto-added MsgType + ServerId first, then additional in order
assert_eq!(codes(&rep.options), vec![53, 54, 1, 51]);
assert_eq!(rep.options[1], DhcpOption::ServerIdentifier(server_ip));
}
#[test]
fn reply_nak_zeroes_ciaddr_and_yiaddr_is_offer_ip() {
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, in_packet: Packet) {
let _ = s.reply(
MessageType::Nak,
vec![DhcpOption::Message("no".to_string())],
Ipv4Addr::new(0, 0, 0, 0),
in_packet,
);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(
srv_sock,
Ipv4Addr::new(10, 0, 0, 1),
Ipv4Addr::new(10, 0, 0, 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];
let req = Packet {
reply: false, hops: 0, xid: 0x2222, secs: 0, broadcast: false,
ciaddr: Ipv4Addr::new(10, 0, 0, 99), yiaddr: Ipv4Addr::new(0, 0, 0, 0),
siaddr: Ipv4Addr::new(0, 0, 0, 0), giaddr: Ipv4Addr::new(0, 0, 0, 0),
chaddr: [1, 1, 1, 1, 1, 1],
options: vec![DhcpOption::DhcpMessageType(MessageType::Request)],
};
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
let (n, _) = recv_with_timeout(&client, &mut rbuf);
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
assert_eq!(rep.message_type().unwrap(), MessageType::Nak);
assert_eq!(rep.ciaddr, Ipv4Addr::new(0, 0, 0, 0), "Nak forces ciaddr 0");
assert_eq!(rep.yiaddr, Ipv4Addr::new(0, 0, 0, 0));
}
#[test]
fn reply_filters_by_prl_shows_exact_order() {
// PRL [6,1] with additional [Subnet, Lease] plus auto [Msg,Server]:
// filter keeps req order then h-order. Raw bytes must show
// DNS? No DNS in additional here, so result is [1,53,54,51] filtered?
// Use additional with Subnet+Lease+Message: req [1] -> [1,53,54,51], Message dropped.
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
let server_ip = Ipv4Addr::new(10, 0, 0, 1);
struct H;
impl server::Handler for H {
fn handle_request(&mut self, s: &server::Server, in_packet: Packet) {
let _ = s.reply(
MessageType::Ack,
vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0)),
DhcpOption::IpAddressLeaseTime(3600),
DhcpOption::Message("should be dropped when PRL present".to_string()),
],
Ipv4Addr::new(10, 0, 0, 50),
in_packet,
);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(srv_sock, server_ip, Ipv4Addr::new(10, 0, 0, 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];
// Request asks only for Subnet(1). Message(56) not in req nor h -> dropped.
let req = Packet {
reply: false, hops: 0, xid: 0x9999, 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: [5, 5, 5, 5, 5, 5],
options: vec![
DhcpOption::DhcpMessageType(MessageType::Request),
DhcpOption::ServerIdentifier(server_ip),
DhcpOption::ParameterRequestList(vec![1]),
],
};
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
let (n, _) = recv_with_timeout(&client, &mut rbuf);
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
// req [1] first, then h remainder [53,54,51] (6,3 absent). Message dropped.
assert_eq!(codes(&rep.options), vec![1, 53, 54, 51]);
}
#[test]
fn reply_without_prl_keeps_all_additional_in_order() {
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
let server_ip = Ipv4Addr::new(10, 0, 0, 1);
struct H;
impl server::Handler for H {
fn handle_request(&mut self, s: &server::Server, in_packet: Packet) {
// NOTE: Message must be short to fit the packet.
// 240 + 3(Msg) + 6(Server) + 6(Subnet) + (2+len) + 1(END) < 300
// => len < 43. "hi" fits; longer strings trigger truncation.
let _ = s.reply(
MessageType::Ack,
vec![
DhcpOption::SubnetMask(Ipv4Addr::new(255, 0, 0, 0)),
DhcpOption::Message("hi".to_string()),
],
Ipv4Addr::new(10, 0, 0, 60),
in_packet,
);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(srv_sock, server_ip, Ipv4Addr::new(10, 0, 0, 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];
let req = test_packet(0x7777, [6, 6, 6, 6, 6, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Request),
]);
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
let (n, _) = recv_with_timeout(&client, &mut rbuf);
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
assert_eq!(codes(&rep.options), vec![53, 54, 1, 56]);
}
#[test]
fn send_unicast_goes_to_request_source() {
// broadcast=false + real src IP -> reply sent to src (client receives).
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, in_packet: Packet) {
let _ = s.reply(
MessageType::Offer,
vec![DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0))],
Ipv4Addr::new(192, 168, 1, 10),
in_packet,
);
}
}
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];
let req = test_packet(0x5555, [1, 2, 3, 4, 5, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Discover),
]);
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
let (n, src) = recv_with_timeout(&client, &mut rbuf);
assert!(n > 240);
// src must be the server socket (reply came from server)
assert_eq!(src.ip(), srv_addr.ip());
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 1, 10));
}
#[test]
fn send_broadcast_flag_routes_to_broadcast_ip() {
// To set broadcast=true on the server side, the REQUEST must decode
// with broadcast=true, which requires low-byte 0x80 ([0,128]) due to
// the `flags & 128` quirk. [128,0] (correct RFC 0x8000) decodes false.
// Server is configured with broadcast_ip=127.0.0.1 so the reply still
// reaches this test client (same port, loopback IP).
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, in_packet: Packet) {
assert!(in_packet.broadcast, "test precondition: req must decode broadcast=true");
let _ = s.reply(
MessageType::Offer,
vec![DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0))],
Ipv4Addr::new(192, 168, 1, 11),
in_packet,
);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(
srv_sock,
Ipv4Addr::new(192, 168, 1, 1),
Ipv4Addr::new(127, 0, 0, 1), // loopback so test can receive
H,
);
});
std::thread::sleep(Duration::from_millis(100));
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
let mut rbuf = [0u8; 1500];
// hand-crafted raw with flags [0,128] -> decodes broadcast=true
let mut raw = vec![0u8; 236];
raw[0] = 1; raw[1] = 1; raw[2] = 6; raw[3] = 0;
raw[4..8].copy_from_slice(&0xBEEFu32.to_be_bytes());
raw[10] = 0; raw[11] = 128;
raw[28..34].copy_from_slice(&[7, 7, 7, 7, 7, 7]);
raw.extend_from_slice(&[99, 130, 83, 99, 53, 1, 1, 255]);
client.send_to(&raw, srv_addr).unwrap();
let (n, _) = recv_with_timeout(&client, &mut rbuf);
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 1, 11));
}
#[test]
fn serve_ignores_malformed_and_handles_valid() {
let srv_sock = UdpSocket::bind("127.0.0.1:0").unwrap();
let srv_addr = srv_sock.local_addr().unwrap();
let (tx, rx) = mpsc::channel::<u32>();
struct H {
tx: mpsc::Sender<u32>,
}
impl server::Handler for H {
fn handle_request(&mut self, _s: &server::Server, p: Packet) {
let _ = self.tx.send(p.xid);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve(
srv_sock,
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(1, 1, 1, 255),
H { tx },
);
});
std::thread::sleep(Duration::from_millis(100));
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
let mut buf = [0u8; 1500];
// malformed: bad cookie -> must be ignored (no handler call)
let mut bad = vec![0u8; 236];
bad[0] = 1; bad[1] = 1; bad[2] = 6;
bad.extend_from_slice(&[0, 0, 0, 0, 53, 1, 1, 255]);
client.send_to(&bad, srv_addr).unwrap();
assert!(rx.recv_timeout(Duration::from_millis(300)).is_err(), "bad packet must be ignored");
// truncated header -> ignored
client.send_to(&[1u8; 10], srv_addr).unwrap();
assert!(rx.recv_timeout(Duration::from_millis(300)).is_err(), "short packet must be ignored");
// valid -> handled
let p = test_packet(0xABCD, [1, 2, 3, 4, 5, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Discover),
]);
client.send_to(&p.encode(&mut buf).to_vec(), srv_addr).unwrap();
assert_eq!(rx.recv_timeout(Duration::from_secs(2)).unwrap(), 0xABCD);
}
#[test]
fn serve_with_broadcasts_fans_out_to_each_address() {
// Broadcast-routed replies go to EVERY listed address (unicast still
// goes once to the peer). Two loopback clients on 127.0.0.1 and
// 127.0.0.2 must each receive the same Offer.
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, in_packet: Packet) {
assert!(in_packet.broadcast, "test precondition: req must decode broadcast=true");
let _ = s.reply(
MessageType::Offer,
vec![DhcpOption::SubnetMask(Ipv4Addr::new(255, 255, 255, 0))],
Ipv4Addr::new(192, 168, 1, 12),
in_packet,
);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve_with_broadcasts(
srv_sock,
Ipv4Addr::new(192, 168, 1, 1),
vec![Ipv4Addr::new(127, 0, 0, 1), Ipv4Addr::new(127, 0, 0, 2)],
H,
);
});
std::thread::sleep(Duration::from_millis(100));
let client_a = UdpSocket::bind("127.0.0.1:0").unwrap();
// Same port number on the second loopback IP: the server fans out to
// <each-broadcast-ip>:<sender-port>, so B must listen on A's port.
let port_a = client_a.local_addr().unwrap().port();
let client_b = UdpSocket::bind(("127.0.0.2", port_a)).unwrap();
let mut rbuf = [0u8; 1500];
// hand-crafted raw with flags [0,128] -> decodes broadcast=true
let mut raw = vec![0u8; 236];
raw[0] = 1; raw[1] = 1; raw[2] = 6; raw[3] = 0;
raw[4..8].copy_from_slice(&0xF00Du32.to_be_bytes());
raw[10] = 0; raw[11] = 128;
raw[28..34].copy_from_slice(&[8, 8, 8, 8, 8, 8]);
raw.extend_from_slice(&[99, 130, 83, 99, 53, 1, 1, 255]);
client_a.send_to(&raw, srv_addr).unwrap();
let (n_a, _) = recv_with_timeout(&client_a, &mut rbuf);
let rep_a = unwrap_packet(Packet::from(&rbuf[..n_a]));
assert_eq!(rep_a.yiaddr, Ipv4Addr::new(192, 168, 1, 12));
let (n_b, _) = recv_with_timeout(&client_b, &mut rbuf);
let rep_b = unwrap_packet(Packet::from(&rbuf[..n_b]));
assert_eq!(rep_b.yiaddr, Ipv4Addr::new(192, 168, 1, 12));
assert_eq!(rep_a.xid, rep_b.xid, "both copies are the same reply");
}
#[test]
fn serve_with_broadcasts_empty_list_falls_back_without_panic() {
// Empty list falls back to the limited broadcast address and must not
// panic; the server must stay alive for later unicast requests. (No
// delivery assertion: limited broadcast is not loopback-reliable, and
// the test socket lacks SO_BROADCAST so the send itself errors out.)
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, in_packet: Packet) {
let _ = s.reply(
MessageType::Offer,
vec![],
Ipv4Addr::new(192, 168, 1, 13),
in_packet,
);
}
}
std::thread::spawn(move || {
let _ = server::Server::serve_with_broadcasts(
srv_sock,
Ipv4Addr::new(192, 168, 1, 1),
vec![],
H,
);
});
std::thread::sleep(Duration::from_millis(100));
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
client
.set_read_timeout(Some(Duration::from_millis(300)))
.unwrap();
let mut buf = [0u8; 1500];
let mut rbuf = [0u8; 1500];
// broadcast-flagged Discover: routed to 255.255.255.255, must not panic.
let mut raw = vec![0u8; 236];
raw[0] = 1; raw[1] = 1; raw[2] = 6; raw[3] = 0;
raw[4..8].copy_from_slice(&0xBEEFu32.to_be_bytes());
raw[10] = 0; raw[11] = 128;
raw[28..34].copy_from_slice(&[9, 9, 9, 9, 9, 9]);
raw.extend_from_slice(&[99, 130, 83, 99, 53, 1, 1, 255]);
client.send_to(&raw, srv_addr).unwrap();
let _ = client.recv_from(&mut rbuf); // delivered or not; must not hang the server
// server still alive: plain unicast Discover gets its Offer.
let req = test_packet(0x7777, [1, 2, 3, 4, 5, 6], vec![
DhcpOption::DhcpMessageType(MessageType::Discover),
]);
client.send_to(&req.encode(&mut buf).to_vec(), srv_addr).unwrap();
let (n, _) = recv_with_timeout(&client, &mut rbuf);
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 1, 13));
}
// ---------------------------------------------------------------------------
// route_dests: RFC 2131 s4.1 delivery plan (pure, no sockets)
// ---------------------------------------------------------------------------
fn peer(ip: Ipv4Addr) -> std::net::SocketAddr {
std::net::SocketAddr::new(std::net::IpAddr::V4(ip), 68)
}
#[test]
fn route_sourced_peer_unicasts_once() {
// Renewing client with a real source address: unicast back, even with
// yiaddr set and broadcasts configured (no fan-out, no unicast copy).
let dsts = server::Server::route_dests(
peer(Ipv4Addr::new(192, 168, 2, 50)),
false,
Ipv4Addr::new(192, 168, 2, 50),
&[Ipv4Addr::new(192, 168, 2, 255), Ipv4Addr::BROADCAST],
);
assert_eq!(dsts, vec![peer(Ipv4Addr::new(192, 168, 2, 50))]);
}
#[test]
fn route_broadcast_flag_fans_out_without_unicast_copy() {
// Flag set means broadcast-only, even with a usable yiaddr: the client
// announced it cannot take unicast.
let dsts = server::Server::route_dests(
peer(Ipv4Addr::UNSPECIFIED),
true,
Ipv4Addr::new(192, 168, 2, 3),
&[Ipv4Addr::new(192, 168, 2, 255), Ipv4Addr::BROADCAST],
);
assert_eq!(
dsts,
vec![
std::net::SocketAddr::new(
std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)),
68
),
std::net::SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::BROADCAST), 68),
]
);
}
#[test]
fn route_unspecified_no_flag_adds_unicast_yiaddr_copy() {
// The dnsmasq case: flag clear, source 0.0.0.0, offered address present.
// Broadcast copies go out AND a unicast copy to yiaddr:port.
let dsts = server::Server::route_dests(
peer(Ipv4Addr::UNSPECIFIED),
false,
Ipv4Addr::new(192, 168, 2, 71),
&[Ipv4Addr::new(192, 168, 2, 255), Ipv4Addr::BROADCAST],
);
assert_eq!(
dsts,
vec![
std::net::SocketAddr::new(
std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)),
68
),
std::net::SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::BROADCAST), 68),
std::net::SocketAddr::new(
std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 2, 71)),
68
),
]
);
}
#[test]
fn route_nak_has_no_unicast_target() {
// yiaddr 0.0.0.0 (NAK): broadcast fan-out only, no 0.0.0.0 copy.
let dsts = server::Server::route_dests(
peer(Ipv4Addr::UNSPECIFIED),
false,
Ipv4Addr::UNSPECIFIED,
&[Ipv4Addr::new(192, 168, 2, 255)],
);
assert_eq!(
dsts,
vec![std::net::SocketAddr::new(
std::net::IpAddr::V4(Ipv4Addr::new(192, 168, 2, 255)),
68
)]
);
}