mirror of
https://github.com/pj1234678/RustyDHCP.git
synced 2026-10-15 00:06:42 +08:00
529 lines
20 KiB
Rust
529 lines
20 KiB
Rust
//! DHCP security: denial-of-service behavior, part 6.
|
||
//!
|
||
//! Covers DOS-relevant behavior NOT locked by `test_security.rs`,
|
||
//! `test_security_extra.rs`, `test_security_edge.rs`, `test_security_wire.rs`,
|
||
//! `test_security_gates.rs`, `test_security_hijack.rs`,
|
||
//! `test_security_slowloris.rs`, or the RFC suites. All tests assert the
|
||
//! *current* behavior and pass against it; tests for known-unsafe behavior
|
||
//! end in `_quirk` and state the threat in the docs.
|
||
//!
|
||
//! Findings locked here: the server performs NO rate limiting (every
|
||
//! unsatisfiable Request earns a NAK, every Release/Inform is processed),
|
||
//! the single-threaded loop serializes parallel clients without drops,
|
||
//! replies bear no relation to request count, and both decode and encode
|
||
//! paths stay bounded (option count capped by datagram size on decode and
|
||
//! by the 300-byte wire cap on encode, including the reply path).
|
||
//!
|
||
//! Groups:
|
||
//! - A. No rate limiting: NAK storms, Release storms, Inform floods are all
|
||
//! processed; the server stays responsive throughout.
|
||
//! - B. Concurrency: parallel clients are serialized without drops,
|
||
//! cross-talk, or xid confusion.
|
||
//! - C. Allocation bounds: minimal-option bomb hits the theoretical decoder
|
||
//! maximum; a 500-option reply is capped to 300 bytes on the wire.
|
||
|
||
use dhcp4r::options::*;
|
||
use dhcp4r::packet::*;
|
||
use dhcp4r::server;
|
||
use std::collections::{HashMap, HashSet};
|
||
use std::net::{Ipv4Addr, UdpSocket};
|
||
use std::ops::Add;
|
||
use std::time::{Duration, Instant};
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// helpers (same patterns as test_security_slowloris.rs)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
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::UNSPECIFIED,
|
||
yiaddr: Ipv4Addr::UNSPECIFIED,
|
||
siaddr: Ipv4Addr::UNSPECIFIED,
|
||
giaddr: Ipv4Addr::UNSPECIFIED,
|
||
chaddr,
|
||
options: opts,
|
||
}
|
||
}
|
||
|
||
// Exact replica of examples/server.rs lease state (see test_examples_extra).
|
||
const IP_START: [u8; 4] = [192, 168, 2, 2];
|
||
const IP_START_NUM: u32 = u32::from_be_bytes(IP_START);
|
||
const LEASE_NUM: u32 = 252;
|
||
const LEASE_DURATION_SECS: u32 = 86400;
|
||
|
||
// Example-like handler: Discover->Offer, Request->Ack, Release|Decline gated,
|
||
// everything else ignored. Mirrors examples/server.rs match arms.
|
||
struct ExampleLike {
|
||
leases: HashMap<Ipv4Addr, ([u8; 6], Option<Instant>)>,
|
||
last_lease: u32,
|
||
lease_duration: Duration,
|
||
}
|
||
|
||
impl ExampleLike {
|
||
fn new() -> Self {
|
||
Self {
|
||
leases: HashMap::new(),
|
||
last_lease: 0,
|
||
lease_duration: Duration::new(LEASE_DURATION_SECS as u64, 0),
|
||
}
|
||
}
|
||
fn available(&self, chaddr: &[u8; 6], addr: &Ipv4Addr) -> bool {
|
||
let pos: u32 = (*addr).into();
|
||
pos >= IP_START_NUM
|
||
&& pos < IP_START_NUM + LEASE_NUM
|
||
&& match self.leases.get(addr) {
|
||
Some((mac, expiry)) => {
|
||
*mac == *chaddr || expiry.map_or(true, |exp| Instant::now().gt(&exp))
|
||
}
|
||
None => true,
|
||
}
|
||
}
|
||
fn current_lease(&self, chaddr: &[u8; 6]) -> Option<Ipv4Addr> {
|
||
for (i, v) in &self.leases {
|
||
if v.0 == *chaddr {
|
||
return Some(*i);
|
||
}
|
||
}
|
||
None
|
||
}
|
||
}
|
||
|
||
impl server::Handler for ExampleLike {
|
||
fn handle_request(&mut self, server: &server::Server, in_packet: Packet) {
|
||
match in_packet.message_type() {
|
||
Ok(MessageType::Discover) => {
|
||
if let Some(ip) = self.current_lease(&in_packet.chaddr) {
|
||
let _ = server.reply(
|
||
MessageType::Offer,
|
||
vec![DhcpOption::IpAddressLeaseTime(LEASE_DURATION_SECS)],
|
||
ip,
|
||
in_packet,
|
||
);
|
||
return;
|
||
}
|
||
for _ in 0..LEASE_NUM {
|
||
self.last_lease = (self.last_lease + 1) % LEASE_NUM;
|
||
let cand: Ipv4Addr = (IP_START_NUM + self.last_lease).into();
|
||
if self.available(&in_packet.chaddr, &cand) {
|
||
let _ = server.reply(
|
||
MessageType::Offer,
|
||
vec![DhcpOption::IpAddressLeaseTime(LEASE_DURATION_SECS)],
|
||
cand,
|
||
in_packet,
|
||
);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
Ok(MessageType::Request) => {
|
||
// NOTE: mirrors the example — the for_this_server gate is
|
||
// commented out upstream, so Requests are honored regardless.
|
||
let req_ip = match in_packet.option(REQUESTED_IP_ADDRESS) {
|
||
Some(DhcpOption::RequestedIpAddress(x)) => *x,
|
||
_ => in_packet.ciaddr,
|
||
};
|
||
if let Some(ip) = self.current_lease(&in_packet.chaddr) {
|
||
let _ = server.reply(
|
||
MessageType::Ack,
|
||
vec![DhcpOption::IpAddressLeaseTime(LEASE_DURATION_SECS)],
|
||
ip,
|
||
in_packet,
|
||
);
|
||
return;
|
||
}
|
||
if !self.available(&in_packet.chaddr, &req_ip) {
|
||
let _ = server.reply(
|
||
MessageType::Nak,
|
||
vec![DhcpOption::Message("Requested IP not available".to_string())],
|
||
Ipv4Addr::UNSPECIFIED,
|
||
in_packet,
|
||
);
|
||
return;
|
||
}
|
||
self.leases.insert(
|
||
req_ip,
|
||
(
|
||
in_packet.chaddr,
|
||
Some(Instant::now().add(self.lease_duration)),
|
||
),
|
||
);
|
||
let _ = server.reply(
|
||
MessageType::Ack,
|
||
vec![DhcpOption::IpAddressLeaseTime(LEASE_DURATION_SECS)],
|
||
req_ip,
|
||
in_packet,
|
||
);
|
||
}
|
||
Ok(MessageType::Release) | Ok(MessageType::Decline) => {
|
||
if !server.for_this_server(&in_packet) {
|
||
return;
|
||
}
|
||
if let Some(ip) = self.current_lease(&in_packet.chaddr) {
|
||
self.leases.remove(&ip);
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn serve_example_like() -> (std::net::SocketAddr, Ipv4Addr) {
|
||
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);
|
||
std::thread::spawn(move || {
|
||
let _ = server::Server::serve(
|
||
srv_sock,
|
||
server_ip,
|
||
Ipv4Addr::new(192, 168, 2, 255),
|
||
ExampleLike::new(),
|
||
);
|
||
});
|
||
std::thread::sleep(Duration::from_millis(100));
|
||
(srv_addr, server_ip)
|
||
}
|
||
|
||
fn send_recv(
|
||
client: &UdpSocket,
|
||
srv_addr: &std::net::SocketAddr,
|
||
p: &Packet,
|
||
buf: &mut [u8; 1500],
|
||
rbuf: &mut [u8; 1500],
|
||
) -> Packet {
|
||
client.send_to(&p.encode(buf).to_vec(), srv_addr).unwrap();
|
||
let (n, _) = client.recv_from(rbuf).expect("expected reply");
|
||
unwrap_packet(Packet::from(&rbuf[..n]))
|
||
}
|
||
|
||
fn discover(xid: u32, chaddr: [u8; 6]) -> Packet {
|
||
test_packet(
|
||
xid,
|
||
chaddr,
|
||
vec![DhcpOption::DhcpMessageType(MessageType::Discover)],
|
||
)
|
||
}
|
||
|
||
fn request_selecting(
|
||
xid: u32,
|
||
chaddr: [u8; 6],
|
||
server_ip: Ipv4Addr,
|
||
wanted: Ipv4Addr,
|
||
) -> Packet {
|
||
test_packet(
|
||
xid,
|
||
chaddr,
|
||
vec![
|
||
DhcpOption::DhcpMessageType(MessageType::Request),
|
||
DhcpOption::ServerIdentifier(server_ip),
|
||
DhcpOption::RequestedIpAddress(wanted),
|
||
],
|
||
)
|
||
}
|
||
|
||
fn drain_replies(client: &UdpSocket, rbuf: &mut [u8; 1500], wait_ms: u64) -> usize {
|
||
client
|
||
.set_read_timeout(Some(Duration::from_millis(wait_ms)))
|
||
.unwrap();
|
||
let mut count = 0;
|
||
loop {
|
||
match client.recv_from(rbuf) {
|
||
Ok(_) => count += 1,
|
||
Err(_) => break,
|
||
}
|
||
}
|
||
client
|
||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||
.unwrap();
|
||
count
|
||
}
|
||
|
||
// ===========================================================================
|
||
// A. No rate limiting: storms are fully processed, server stays responsive
|
||
// ===========================================================================
|
||
|
||
/// QUIRK (NAK storm): 20 rapid Requests for a taken IP each earn a full NAK
|
||
/// — no suppression, no backoff — then the victim still holds the lease.
|
||
/// An attacker can elicit unbounded replies at roughly 1:1 cost. Locked.
|
||
#[test]
|
||
fn sec7_nak_storm_all_answered_no_suppression() {
|
||
let (srv_addr, server_ip) = serve_example_like();
|
||
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||
client
|
||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||
.unwrap();
|
||
let mut buf = [0u8; 1500];
|
||
let mut rbuf = [0u8; 1500];
|
||
let victim = [0x71; 6];
|
||
// victim takes .60
|
||
let req = request_selecting(0xA001, victim, server_ip, Ipv4Addr::new(192, 168, 2, 60));
|
||
assert_eq!(
|
||
send_recv(&client, &srv_addr, &req, &mut buf, &mut rbuf).message_type(),
|
||
Ok(MessageType::Ack)
|
||
);
|
||
// storm: 20 distinct chaddrs hammer the taken IP, every one NAK'd
|
||
for i in 0..20u32 {
|
||
let ch = [0x70 + i as u8, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
|
||
let grab = request_selecting(0xA100 + i, ch, server_ip, Ipv4Addr::new(192, 168, 2, 60));
|
||
let rep = send_recv(&client, &srv_addr, &grab, &mut buf, &mut rbuf);
|
||
assert_eq!(rep.message_type(), Ok(MessageType::Nak), "storm {}", i);
|
||
assert_eq!(rep.xid, 0xA100 + i, "per-request xid echo in storm");
|
||
}
|
||
// victim still holds .60 afterwards
|
||
let still = send_recv(&client, &srv_addr, &discover(0xA002, victim), &mut buf, &mut rbuf);
|
||
assert_eq!(still.yiaddr, Ipv4Addr::new(192, 168, 2, 60));
|
||
}
|
||
|
||
/// 50 Releases for unknown chaddrs are silent no-ops that still cost a
|
||
/// decode + HashMap lookup each — then a valid Discover is answered,
|
||
/// proving the storm neither crashed the loop nor planted state.
|
||
#[test]
|
||
fn sec7_release_storm_unknown_chaddrs_no_state_no_crash() {
|
||
let (srv_addr, server_ip) = serve_example_like();
|
||
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||
client
|
||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||
.unwrap();
|
||
let mut buf = [0u8; 1500];
|
||
let mut rbuf = [0u8; 1500];
|
||
for i in 0..50u32 {
|
||
let ch = [0x80 + (i % 250) as u8, 0x11, 0x22, 0x33, 0x44, 0x55];
|
||
let rel = Packet {
|
||
reply: false, hops: 0, xid: 0xB000 + i, secs: 0, broadcast: false,
|
||
ciaddr: Ipv4Addr::UNSPECIFIED, yiaddr: Ipv4Addr::UNSPECIFIED,
|
||
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
|
||
chaddr: ch,
|
||
options: vec![
|
||
DhcpOption::DhcpMessageType(MessageType::Release),
|
||
DhcpOption::ServerIdentifier(server_ip),
|
||
],
|
||
};
|
||
client.send_to(&rel.encode(&mut buf).to_vec(), srv_addr).unwrap();
|
||
}
|
||
// no state planted: fresh client gets the first offer, server alive
|
||
let rep = send_recv(&client, &srv_addr, &discover(0xB100, [0x99; 6]), &mut buf, &mut rbuf);
|
||
assert_eq!(rep.message_type(), Ok(MessageType::Offer));
|
||
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 2, 3));
|
||
}
|
||
|
||
/// 20 INFORMs earn zero replies (each ignored) yet each is fully decoded —
|
||
/// pure inbound processing cost with no outbound traffic and no state.
|
||
/// Drain proves the count is exactly 0, then Discover proves liveness.
|
||
#[test]
|
||
fn sec7_inform_flood_zero_replies_no_state() {
|
||
let (srv_addr, _) = serve_example_like();
|
||
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||
let mut buf = [0u8; 1500];
|
||
let mut rbuf = [0u8; 1500];
|
||
for i in 0..20u32 {
|
||
let ch = [0x90 + i as u8, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
|
||
let inf = Packet {
|
||
reply: false, hops: 0, xid: 0xC000 + i, secs: 0, broadcast: false,
|
||
ciaddr: Ipv4Addr::new(192, 168, 2, 60), yiaddr: Ipv4Addr::UNSPECIFIED,
|
||
siaddr: Ipv4Addr::UNSPECIFIED, giaddr: Ipv4Addr::UNSPECIFIED,
|
||
chaddr: ch,
|
||
options: vec![
|
||
DhcpOption::DhcpMessageType(MessageType::Inform),
|
||
DhcpOption::ParameterRequestList(vec![1, 15]),
|
||
],
|
||
};
|
||
client.send_to(&inf.encode(&mut buf).to_vec(), srv_addr).unwrap();
|
||
}
|
||
assert_eq!(drain_replies(&client, &mut rbuf, 400), 0, "INFORM flood: no replies");
|
||
let rep = send_recv(&client, &srv_addr, &discover(0xC100, [0x9A; 6]), &mut buf, &mut rbuf);
|
||
assert_eq!(rep.message_type(), Ok(MessageType::Offer));
|
||
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 2, 3), "no state planted");
|
||
}
|
||
|
||
// ===========================================================================
|
||
// B. Concurrency: parallel clients serialized without drops or cross-talk
|
||
// ===========================================================================
|
||
|
||
/// Five threads × 8 Discovers (40 distinct identities) run concurrently
|
||
/// against the single-threaded loop: all 40 Offers arrive, xids echo
|
||
/// per-transaction, and all 40 yiaddrs are distinct pool addresses.
|
||
/// Locked: serialization is correct under parallel load.
|
||
#[test]
|
||
fn sec7_concurrent_clients_all_answered_distinct() {
|
||
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) {
|
||
// fixed offer allocator keyed off the full client identity:
|
||
// stateless, so concurrent order cannot affect correctness.
|
||
let n = p.chaddr[0] as u16 * 8 + p.chaddr[5] as u16;
|
||
let _ = s.reply(
|
||
MessageType::Offer,
|
||
vec![],
|
||
Ipv4Addr::new(192, 168, 9, 10 + (n % 200) as u8),
|
||
p,
|
||
);
|
||
}
|
||
}
|
||
std::thread::spawn(move || {
|
||
let _ = server::Server::serve(
|
||
srv_sock,
|
||
Ipv4Addr::new(192, 168, 9, 1),
|
||
Ipv4Addr::new(192, 168, 9, 255),
|
||
H,
|
||
);
|
||
});
|
||
std::thread::sleep(Duration::from_millis(100));
|
||
let mut handles = Vec::new();
|
||
for t in 0..5u32 {
|
||
let srv = srv_addr;
|
||
handles.push(std::thread::spawn(move || {
|
||
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||
client
|
||
.set_read_timeout(Some(Duration::from_secs(5)))
|
||
.unwrap();
|
||
let mut buf = [0u8; 1500];
|
||
let mut rbuf = [0u8; 1500];
|
||
let mut out = Vec::new();
|
||
for i in 0..8u32 {
|
||
// distinct chaddr per request; first octet unique per thread
|
||
let ch = [(10 + t) as u8, 0xAA, 0xBB, 0xCC, 0xDD, i as u8];
|
||
let xid = 0xD000 + t * 100 + i;
|
||
let p = discover(xid, ch);
|
||
client.send_to(&p.encode(&mut buf).to_vec(), srv).unwrap();
|
||
let (n, _) = client.recv_from(&mut rbuf).expect("concurrent reply");
|
||
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
|
||
out.push((rep.xid, rep.yiaddr, rep.chaddr));
|
||
}
|
||
out
|
||
}));
|
||
}
|
||
let mut xids = HashSet::new();
|
||
let mut ips = HashSet::new();
|
||
for h in handles {
|
||
for (xid, ip, ch) in h.join().expect("client thread") {
|
||
// xid echoed for its own transaction: full identity inverts
|
||
// cleanly from it, proving the reply matches its requester.
|
||
let expect_t = (xid - 0xD000) / 100;
|
||
let expect_i = (xid - 0xD000) % 100;
|
||
assert!(expect_t < 5 && expect_i < 8);
|
||
assert_eq!(
|
||
[10 + expect_t as u8, 0xAA, 0xBB, 0xCC, 0xDD, expect_i as u8],
|
||
ch,
|
||
"reply matches its requester"
|
||
);
|
||
xids.insert(xid);
|
||
ips.insert(ip);
|
||
}
|
||
}
|
||
assert_eq!(xids.len(), 40, "every transaction answered exactly once");
|
||
assert_eq!(ips.len(), 40, "no cross-talk between concurrent clients");
|
||
}
|
||
|
||
/// Same xid from two distinct chaddrs: both answered independently with the
|
||
/// same xid echoed and different yiaddrs — the server keys nothing on xid,
|
||
/// so collisions cannot confuse or suppress a peer. Locked.
|
||
#[test]
|
||
fn sec7_same_xid_distinct_chaddrs_independent() {
|
||
let (srv_addr, _) = serve_example_like();
|
||
let client = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||
client
|
||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||
.unwrap();
|
||
let mut buf = [0u8; 1500];
|
||
let mut rbuf = [0u8; 1500];
|
||
let a = send_recv(&client, &srv_addr, &discover(0xD0D0, [0xA1; 6]), &mut buf, &mut rbuf);
|
||
let b = send_recv(&client, &srv_addr, &discover(0xD0D0, [0xA2; 6]), &mut buf, &mut rbuf);
|
||
assert_eq!(a.xid, 0xD0D0);
|
||
assert_eq!(b.xid, 0xD0D0);
|
||
assert_ne!(a.yiaddr, b.yiaddr, "distinct clients, distinct offers");
|
||
assert_eq!(a.yiaddr, Ipv4Addr::new(192, 168, 2, 3));
|
||
assert_eq!(b.yiaddr, Ipv4Addr::new(192, 168, 2, 4));
|
||
}
|
||
|
||
// ===========================================================================
|
||
// C. Allocation bounds: decoder maximum and reply-path cap
|
||
// ===========================================================================
|
||
|
||
/// Minimal 2-byte options hit the theoretical decoder maximum: header+cookie
|
||
/// (240) + Discover (3) + 628×`[200,0]` (1256) + END (1) = exactly 1500.
|
||
/// All 629 decode without panic; re-encode caps at 300 with Discover first.
|
||
/// Tightens the 419-entry bomb test to the true bound. Locked.
|
||
#[test]
|
||
fn sec7_minimal_option_bomb_hits_decoder_maximum() {
|
||
let mut opts = vec![53, 1, 1];
|
||
for _ in 0..628 {
|
||
opts.extend_from_slice(&[200, 0]);
|
||
}
|
||
opts.push(255);
|
||
let mut raw = vec![0u8; 236];
|
||
raw[0] = 1;
|
||
raw[1] = 1;
|
||
raw[2] = 6;
|
||
raw[4..8].copy_from_slice(&0xE001u32.to_be_bytes());
|
||
raw[28..34].copy_from_slice(&[0xE1; 6]);
|
||
raw.extend_from_slice(&[99, 130, 83, 99]);
|
||
raw.extend_from_slice(&opts);
|
||
assert_eq!(raw.len(), 1500, "construction must fill the MTU exactly");
|
||
let p = unwrap_packet(Packet::from(&raw));
|
||
assert_eq!(p.options.len(), 629, "1 Discover + 628 minimal fillers");
|
||
assert_eq!(p.message_type(), Ok(MessageType::Discover));
|
||
let mut buf = [0u8; 1500];
|
||
let enc = p.encode(&mut buf).to_vec();
|
||
assert!(enc.len() <= 300, "re-encode capped");
|
||
let q = unwrap_packet(Packet::from(&enc));
|
||
assert_eq!(q.message_type(), Ok(MessageType::Discover), "Discover kept first");
|
||
assert_eq!(q.options.len(), 29, "Discover + 28 fillers + END accounting");
|
||
}
|
||
|
||
/// Reply path with 500 extra options still emits at most 300 bytes: the
|
||
/// encode cap applies to server replies too, so a greedy handler config
|
||
/// cannot bloat answers (no reply-side amplification past the cap). Locked.
|
||
#[test]
|
||
fn sec7_reply_with_hundreds_of_options_capped_live() {
|
||
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 mut extra = Vec::with_capacity(500);
|
||
for i in 0..500u16 {
|
||
extra.push(DhcpOption::Unrecognized(RawDhcpOption {
|
||
code: 200,
|
||
data: vec![(i & 0xFF) as u8],
|
||
}));
|
||
}
|
||
let _ = s.reply(MessageType::Offer, extra, Ipv4Addr::new(192, 168, 8, 10), p);
|
||
}
|
||
}
|
||
std::thread::spawn(move || {
|
||
let _ = server::Server::serve(
|
||
srv_sock,
|
||
Ipv4Addr::new(192, 168, 8, 1),
|
||
Ipv4Addr::new(192, 168, 8, 255),
|
||
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_secs(2)))
|
||
.unwrap();
|
||
let mut buf = [0u8; 1500];
|
||
let mut rbuf = [0u8; 1500];
|
||
let p = discover(0xE010, [0xE1; 6]);
|
||
client.send_to(&p.encode(&mut buf).to_vec(), srv_addr).unwrap();
|
||
let (n, _) = client.recv_from(&mut rbuf).expect("capped reply");
|
||
assert!(n <= 300, "reply capped at wire cap, got {}", n);
|
||
let rep = unwrap_packet(Packet::from(&rbuf[..n]));
|
||
assert_eq!(rep.message_type(), Ok(MessageType::Offer));
|
||
assert_eq!(rep.yiaddr, Ipv4Addr::new(192, 168, 8, 10));
|
||
}
|