mirror of
https://github.com/pj1234678/RustyDHCP.git
synced 2026-10-17 01:33:13 +08:00
Version 2 Added config file and IPV6 support and benchmarks with massive performance uplifts.
This commit is contained in:
@@ -0,0 +1,728 @@
|
||||
//! 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
|
||||
)]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user