Version 2 Added config file and IPV6 support and benchmarks with massive performance uplifts.

This commit is contained in:
pj1234678
2026-09-24 15:25:57 -07:00
committed by GitHub
parent 4ec95e9f84
commit 589351f6da
38 changed files with 19067 additions and 26 deletions
+323
View File
@@ -0,0 +1,323 @@
//! `dhcp.conf` configuration file support (zero dependencies).
//!
//! All server settings — previously hard-coded constants in
//! `examples/server.rs` — now live in a single `dhcp.conf` file using a
//! minimal `key = value` format:
//!
//! ```text
//! # lines starting with '#' or ';' are comments; blank lines are ignored
//! server_ip = 192.168.2.1
//! dns_ips = 8.8.8.8, 8.8.4.4
//! ```
//!
//! Rules (locked by `tests/test_config.rs`):
//! - keys are case-insensitive; surrounding whitespace is trimmed;
//! - a trailing ` # comment` (whitespace + `#`) is stripped from values;
//! - duplicate keys: last occurrence wins;
//! - unknown keys are ignored (forward compatibility);
//! - a line without `=` (that is not blank/comment) is an error;
//! - empty keys and empty values are errors;
//! - every value is validated; failures name the key and line number.
//!
//! [`Config::defaults`] exactly matches the historical hard-coded constants,
//! so running without a `dhcp.conf` behaves exactly as before.
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
/// All tunable server settings. See [`Config::defaults`] for the values the
/// server historically used when everything was hard-coded.
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
// -- IPv4 (DHCPv4, RFC 2131) --
pub server_ip: Ipv4Addr,
pub ip_start: Ipv4Addr,
pub lease_num: u32,
pub subnet_mask: Ipv4Addr,
pub router_ip: Ipv4Addr,
pub dns_ips: Vec<Ipv4Addr>,
/// Master switch for advertising NTP servers (DHCP option 42). Off by
/// default so a missing config file preserves old behavior exactly
/// (the old server never sent option 42).
pub enable_ntp: bool,
/// NTP servers advertised to clients (option 42), comma-separated.
/// Only sent when `enable_ntp` is true and the list is non-empty.
pub ntp_ips: Vec<Ipv4Addr>,
/// Master switch for DHCP Rapid Commit (RFC 4039 option 80 for IPv4,
/// RFC 8415 option 14 for IPv6): when true, a Solicit/Discover carrying
/// the rapid-commit option is answered immediately (Reply/Ack) instead
/// of Advertise/Offer. Off by default so a missing config file preserves
/// old behavior exactly.
pub enable_rapid_commit: bool,
pub broadcast_ip: Ipv4Addr,
/// Extra broadcast destinations for replies (comma-separated). Every
/// broadcast-routed reply is sent to each address in
/// [`Config::broadcasts`]: some clients/APs only accept the limited
/// broadcast `255.255.255.255`, others only the subnet directed
/// broadcast — listing both reaches both populations. Unicast replies
/// are unaffected (sent once to the peer).
pub extra_broadcast_ips: Vec<Ipv4Addr>,
pub lease_duration_secs: u32,
pub leases_file: String,
pub listen_addr: SocketAddr,
// -- IPv6 managed range (DHCPv6, RFC 8415) --
pub enable_dhcpv6: bool,
/// Server DUID served in SERVERID options (opaque bytes, 1..=128).
pub server_duid: Vec<u8>,
/// First address of the managed IPv6 pool.
pub ipv6_start: Ipv6Addr,
/// Number of addresses in the managed IPv6 pool.
pub ipv6_lease_num: u64,
pub ipv6_dns: Vec<Ipv6Addr>,
pub ipv6_lease_duration_secs: u32,
pub leases_file_v6: String,
pub listen_addr_v6: SocketAddr,
}
impl Config {
/// Defaults identical to the historical hard-coded constants
/// (`examples/server.rs` before `dhcp.conf` existed). DHCPv6 is off by
/// default so a missing config file preserves old behavior exactly.
pub fn defaults() -> Config {
Config {
server_ip: Ipv4Addr::new(192, 168, 2, 1),
ip_start: Ipv4Addr::new(192, 168, 2, 2),
lease_num: 252,
subnet_mask: Ipv4Addr::new(255, 255, 255, 0),
router_ip: Ipv4Addr::new(192, 168, 2, 1),
dns_ips: vec![Ipv4Addr::new(8, 8, 8, 8)],
enable_ntp: false,
ntp_ips: vec![],
enable_rapid_commit: false,
broadcast_ip: Ipv4Addr::new(192, 168, 2, 255),
extra_broadcast_ips: vec![],
lease_duration_secs: 86400,
leases_file: "leases".to_string(),
listen_addr: SocketAddr::from(([0, 0, 0, 0], 67)),
enable_dhcpv6: false,
server_duid: vec![
0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x5e, 0xaa, 0xbb, 0xcc,
],
ipv6_start: Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0, 0x100),
ipv6_lease_num: 1000,
ipv6_dns: vec![Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888)],
ipv6_lease_duration_secs: 86400,
leases_file_v6: "leases6".to_string(),
listen_addr_v6: SocketAddr::from((
Ipv6Addr::UNSPECIFIED,
547,
)),
}
}
/// First usable IPv4 address as a number (pool is
/// `[ip_start_num, ip_start_num + lease_num)`).
pub fn ip_start_num(&self) -> u32 {
u32::from_be_bytes(self.ip_start.octets())
}
/// First usable IPv6 address as a number (pool is
/// `[ipv6_start_num, ipv6_start_num + ipv6_lease_num)`).
pub fn ipv6_start_num(&self) -> u128 {
u128::from(self.ipv6_start)
}
/// All broadcast destinations for replies, primary first,
/// de-duplicated. Never empty (falls back to the primary alone).
pub fn broadcasts(&self) -> Vec<Ipv4Addr> {
let mut out = Vec::with_capacity(1 + self.extra_broadcast_ips.len());
out.push(self.broadcast_ip);
for ip in &self.extra_broadcast_ips {
if !out.contains(ip) {
out.push(*ip);
}
}
out
}
/// Load and parse the file at `path`.
pub fn load(path: &str) -> Result<Config, String> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("cannot read {}: {}", path, e))?;
Config::parse(&text)
}
/// Parse `dhcp.conf` text; starts from [`Config::defaults`] and applies
/// each key in order.
pub fn parse(text: &str) -> Result<Config, String> {
let mut cfg = Config::defaults();
for (idx, raw_line) in text.lines().enumerate() {
let line_no = idx + 1;
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
let eq = line.find('=').ok_or_else(|| {
format!("{}: expected `key = value`, got {:?}", line_no, raw_line.trim())
})?;
let key = line[..eq].trim().to_lowercase();
let mut value = line[eq + 1..].trim().to_string();
// strip trailing " # comment" (whitespace + '#'); a '#' glued to
// the value is kept verbatim.
if let Some(pos) = value.find(" #").or_else(|| value.find('\t').and_then(|p| {
if value[p + 1..].starts_with('#') {
Some(p)
} else {
None
}
})) {
value.truncate(pos);
value = value.trim_end().to_string();
}
if key.is_empty() {
return Err(format!("{}: empty key", line_no));
}
if value.is_empty() {
return Err(format!("{}: empty value for key {:?}", line_no, key));
}
cfg.apply(&key, &value, line_no)?;
}
cfg.validate()
}
fn apply(&mut self, key: &str, value: &str, line: usize) -> Result<(), String> {
let bad = |what: &str| format!("{}: invalid {} for key {:?}: {:?}", line, what, key, value);
match key {
"server_ip" => self.server_ip = value.parse().map_err(|_| bad("IPv4 address"))?,
"ip_start" => self.ip_start = value.parse().map_err(|_| bad("IPv4 address"))?,
"lease_num" => self.lease_num = value.parse().map_err(|_| bad("u32"))?,
"subnet_mask" => self.subnet_mask = value.parse().map_err(|_| bad("IPv4 address"))?,
"router_ip" => self.router_ip = value.parse().map_err(|_| bad("IPv4 address"))?,
"dns_ips" => self.dns_ips = parse_ipv4_list(value).map_err(|_| bad("IPv4 list"))?,
"enable_ntp" => self.enable_ntp = parse_bool(value).ok_or_else(|| bad("bool"))?,
"ntp_ips" => self.ntp_ips = parse_ipv4_list(value).map_err(|_| bad("IPv4 list"))?,
"enable_rapid_commit" => {
self.enable_rapid_commit = parse_bool(value).ok_or_else(|| bad("bool"))?
}
"broadcast_ip" => {
// Accepts a comma-separated list: the first address stays the
// primary `broadcast_ip`, the rest extend
// `extra_broadcast_ips` (de-duplicated by `broadcasts()`).
let mut parts = value.split(',');
let first = parts.next().ok_or_else(|| bad("IPv4 address"))?;
self.broadcast_ip = first
.trim()
.parse()
.map_err(|_| bad("IPv4 address"))?;
for part in parts {
self.extra_broadcast_ips.push(
part.trim().parse().map_err(|_| bad("IPv4 address"))?,
);
}
}
"extra_broadcast_ips" => {
let mut list =
parse_ipv4_list(value).map_err(|_| bad("IPv4 list"))?;
self.extra_broadcast_ips.append(&mut list);
}
"lease_duration_secs" => {
self.lease_duration_secs = value.parse().map_err(|_| bad("u32"))?
}
"leases_file" => self.leases_file = value.to_string(),
"listen_addr" => {
self.listen_addr = value.parse().map_err(|_| bad("socket address"))?
}
"enable_dhcpv6" => self.enable_dhcpv6 = parse_bool(value).ok_or_else(|| bad("bool"))?,
"server_duid" => self.server_duid = parse_duid_hex(value).map_err(|_| bad("DUID hex"))?,
"ipv6_start" => self.ipv6_start = value.parse().map_err(|_| bad("IPv6 address"))?,
"ipv6_lease_num" => self.ipv6_lease_num = value.parse().map_err(|_| bad("u64"))?,
"ipv6_dns" => self.ipv6_dns = parse_ipv6_list(value).map_err(|_| bad("IPv6 list"))?,
"ipv6_lease_duration_secs" => {
self.ipv6_lease_duration_secs = value.parse().map_err(|_| bad("u32"))?
}
"leases_file_v6" => self.leases_file_v6 = value.to_string(),
"listen_addr_v6" => {
self.listen_addr_v6 = value.parse().map_err(|_| bad("socket address"))?
}
// unknown keys are ignored for forward compatibility
_ => {}
}
Ok(())
}
/// Cross-field validation: pool ranges must not overflow the address
/// space, and the DUID must fit RFC 8415 limits (1..=128 bytes).
fn validate(self) -> Result<Config, String> {
if self.server_duid.is_empty() || self.server_duid.len() > 128 {
return Err("server_duid must be 1..=128 bytes".to_string());
}
let v4_end = u64::from(self.ip_start_num()) + u64::from(self.lease_num);
if v4_end > u64::from(u32::MAX) + 1 {
return Err("ip_start + lease_num overflows IPv4 space".to_string());
}
if (self.ipv6_start_num())
.checked_add(self.ipv6_lease_num as u128)
.is_none()
{
return Err("ipv6_start + ipv6_lease_num overflows IPv6 space".to_string());
}
Ok(self)
}
}
fn parse_bool(s: &str) -> Option<bool> {
match s.to_lowercase().as_str() {
"true" | "yes" | "on" | "1" => Some(true),
"false" | "no" | "off" | "0" => Some(false),
_ => None,
}
}
fn parse_ipv4_list(s: &str) -> Result<Vec<Ipv4Addr>, String> {
s.split(',')
.map(|part| {
part.trim()
.parse::<Ipv4Addr>()
.map_err(|e| format!("{:?}: {}", part.trim(), e))
})
.collect()
}
fn parse_ipv6_list(s: &str) -> Result<Vec<Ipv6Addr>, String> {
s.split(',')
.map(|part| {
part.trim()
.parse::<Ipv6Addr>()
.map_err(|e| format!("{:?}: {}", part.trim(), e))
})
.collect()
}
/// Parse opaque hex bytes (a DUID), accepting `00:03:00:01...`, `00-03-...`
/// or plain `00030001...` (whitespace between bytes is also tolerated).
///
/// Used for `server_duid` values and the DUID field of `leases6` lines.
pub fn parse_duid_hex(s: &str) -> Result<Vec<u8>, String> {
#[inline]
fn hex_val(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len() / 2);
let mut hi: Option<u8> = None;
for &c in bytes {
if c == b':' || c == b'-' || c == b' ' || c == b'\t' {
continue;
}
let v = hex_val(c).ok_or_else(|| format!("invalid hex {:?}: {:?}", s, c as char))?;
if let Some(h) = hi {
out.push((h << 4) | v);
hi = None;
} else {
hi = Some(v);
}
}
if hi.is_some() || out.is_empty() {
return Err(format!("odd-length hex: {:?}", s));
}
Ok(out)
}
+875
View File
@@ -0,0 +1,875 @@
//! Minimal DHCPv6 (RFC 8415) packet codec plus a managed-range allocator.
//!
//! Wire format (RFC 8415 §7): `msg-type` (1 octet) + `transaction-id`
//! (3 octets) followed by options to the end of the datagram. Unlike DHCPv4
//! there is **no END byte and no PAD**: options are `code` (u16 BE) +
//! `len` (u16 BE) + `data`, parsed strictly — any truncation or malformed
//! option rejects the whole datagram with [`DecodeError`] (there is no
//! v4-style swallow-and-desync quirk here by design).
//!
//! Identity is the client DUID (opaque bytes, option 1); address state is
//! tracked per IA_NA (option 3, IAID + IA Address sub-options). [`V6Pool`]
//! manages one contiguous range `[start, start + count)` with round-robin
//! allocation mirroring the v4 example, except infinite (`None` expiry)
//! leases are **not** stealable (this intentionally differs from the v4
//! `available()` quirk — see its docs).
use std::collections::HashMap;
use std::net::Ipv6Addr;
use std::time::{Duration, Instant};
// ---------------------------------------------------------------------------
// Message types (RFC 8415 §7.3)
// ---------------------------------------------------------------------------
/// DHCPv6 message types with their RFC 8415 §7.3 numeric values.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum MsgType {
Solicit = 1,
Advertise = 2,
Request = 3,
Confirm = 4,
Renew = 5,
Rebind = 6,
Reply = 7,
Release = 8,
Decline = 9,
Reconfigure = 10,
InformationRequest = 11,
RelayForward = 12,
RelayReply = 13,
}
impl MsgType {
pub fn from(val: u8) -> Result<MsgType, DecodeError> {
match val {
1 => Ok(MsgType::Solicit),
2 => Ok(MsgType::Advertise),
3 => Ok(MsgType::Request),
4 => Ok(MsgType::Confirm),
5 => Ok(MsgType::Renew),
6 => Ok(MsgType::Rebind),
7 => Ok(MsgType::Reply),
8 => Ok(MsgType::Release),
9 => Ok(MsgType::Decline),
10 => Ok(MsgType::Reconfigure),
11 => Ok(MsgType::InformationRequest),
12 => Ok(MsgType::RelayForward),
13 => Ok(MsgType::RelayReply),
_ => Err(DecodeError::BadMessageType(val)),
}
}
}
// ---------------------------------------------------------------------------
// Errors
// ---------------------------------------------------------------------------
/// Reasons a DHCPv6 datagram (or option) cannot be decoded.
#[derive(Debug, Clone, PartialEq)]
pub enum DecodeError {
/// Fewer bytes than the header / declared length requires.
Truncated,
/// Unknown message type byte.
BadMessageType(u8),
/// Text option is not valid UTF-8.
BadUtf8,
/// Well-framed but internally inconsistent (short IA_NA, short IAADDR,
/// DNS length not a multiple of 16, short status, trailing < 4 bytes).
Malformed,
}
/// Reasons a [`Packet`] cannot be encoded.
#[derive(Debug, Clone, PartialEq)]
pub enum EncodeError {
/// A single option payload exceeds the 16-bit length field (65535).
OptionTooLarge,
}
// ---------------------------------------------------------------------------
// Options (RFC 8415 §24 and friends)
// ---------------------------------------------------------------------------
pub const OPT_CLIENTID: u16 = 1;
pub const OPT_SERVERID: u16 = 2;
pub const OPT_IA_NA: u16 = 3;
pub const OPT_IA_TA: u16 = 4;
pub const OPT_IAADDR: u16 = 5;
pub const OPT_ORO: u16 = 6;
pub const OPT_PREFERENCE: u16 = 7;
pub const OPT_ELAPSED_TIME: u16 = 8;
pub const OPT_RELAY_MSG: u16 = 9;
pub const OPT_AUTH: u16 = 11;
pub const OPT_UNICAST: u16 = 12;
pub const OPT_STATUS_CODE: u16 = 13;
pub const OPT_RAPID_COMMIT: u16 = 14;
pub const OPT_USER_CLASS: u16 = 15;
pub const OPT_VENDOR_CLASS: u16 = 16;
pub const OPT_VENDOR_OPTS: u16 = 17;
pub const OPT_INTERFACE_ID: u16 = 18;
pub const OPT_RECONF_MSG: u16 = 19;
pub const OPT_RECONF_ACCEPT: u16 = 20;
pub const OPT_DNS_SERVERS: u16 = 23;
pub const OPT_DOMAIN_LIST: u16 = 24;
pub const OPT_IA_PD: u16 = 25;
pub const OPT_IAPREFIX: u16 = 26;
/// RFC 8415 status codes (§21.13): 0 Success, 1 UnspecFail, 2 NoAddrsAvail,
/// 3 NoBinding, 4 UseMulticast, 5 NoPrefixAvail.
pub const STATUS_SUCCESS: u16 = 0;
pub const STATUS_NO_ADDRS_AVAIL: u16 = 2;
/// An IA Address sub-option body (RFC 8415 §21.6): address plus its
/// preferred and valid lifetimes in seconds.
#[derive(Debug, Clone, PartialEq)]
pub struct IaAddr {
pub addr: Ipv6Addr,
pub preferred: u32,
pub valid: u32,
}
/// An IA_NA option body (RFC 8415 §21.4): IAID + T1/T2 plus address bindings.
#[derive(Debug, Clone, PartialEq)]
pub struct IaNa {
pub iaid: u32,
pub t1: u32,
pub t2: u32,
pub addrs: Vec<IaAddr>,
}
/// Raw form of any option that has no typed variant here.
#[derive(Debug, Clone, PartialEq)]
pub struct RawDhcpv6Option {
pub code: u16,
pub data: Vec<u8>,
}
/// A decoded DHCPv6 option. Anything without a typed variant is preserved
/// transparently as [`Dhcpv6Option::Unrecognized`].
#[derive(Debug, Clone, PartialEq)]
pub enum Dhcpv6Option {
/// Option 1: client DUID (opaque bytes).
ClientId(Vec<u8>),
/// Option 2: server DUID (opaque bytes).
ServerId(Vec<u8>),
/// Option 3: identity association for non-temporary addresses.
IaNa(IaNa),
/// Option 23 (RFC 3646): recursive name servers.
DnsServers(Vec<Ipv6Addr>),
/// Option 13: status code + human-readable message.
StatusCode(u16, String),
Unrecognized(RawDhcpv6Option),
}
impl Dhcpv6Option {
#[inline]
pub fn wire_len(&self) -> usize {
4 + match self {
Self::ClientId(d) | Self::ServerId(d) => d.len(),
Self::Unrecognized(r) => r.data.len(),
Self::DnsServers(a) => a.len() * 16,
Self::StatusCode(_, m) => 2 + m.len(),
Self::IaNa(ia) => 12 + ia.addrs.len() * 28,
}
}
#[inline]
fn encode_into(&self, out: &mut Vec<u8>) -> Result<(), EncodeError> {
let payload = self.wire_len() - 4;
if payload > 0xFFFF {
return Err(EncodeError::OptionTooLarge);
}
out.extend_from_slice(&self.code().to_be_bytes());
out.extend_from_slice(&(payload as u16).to_be_bytes());
match self {
Self::ClientId(d) | Self::ServerId(d) => out.extend_from_slice(d),
Self::Unrecognized(r) => out.extend_from_slice(&r.data),
Self::DnsServers(a) => {
for ip in a {
out.extend_from_slice(&ip.octets());
}
}
Self::StatusCode(c, m) => {
out.extend_from_slice(&c.to_be_bytes());
out.extend_from_slice(m.as_bytes());
}
Self::IaNa(ia) => {
out.extend_from_slice(&ia.iaid.to_be_bytes());
out.extend_from_slice(&ia.t1.to_be_bytes());
out.extend_from_slice(&ia.t2.to_be_bytes());
for a in &ia.addrs {
let mut sub = [0u8; 28];
sub[0..2].copy_from_slice(&OPT_IAADDR.to_be_bytes());
sub[2..4].copy_from_slice(&24u16.to_be_bytes());
sub[4..20].copy_from_slice(&a.addr.octets());
sub[20..24].copy_from_slice(&a.preferred.to_be_bytes());
sub[24..28].copy_from_slice(&a.valid.to_be_bytes());
out.extend_from_slice(&sub);
}
}
}
Ok(())
}
pub fn to_raw(&self) -> RawDhcpv6Option {
match self {
Self::ClientId(duid) => RawDhcpv6Option {
code: OPT_CLIENTID,
data: duid.clone(),
},
Self::ServerId(duid) => RawDhcpv6Option {
code: OPT_SERVERID,
data: duid.clone(),
},
Self::IaNa(ia) => {
let mut data = Vec::with_capacity(12 + ia.addrs.len() * 28);
data.extend_from_slice(&ia.iaid.to_be_bytes());
data.extend_from_slice(&ia.t1.to_be_bytes());
data.extend_from_slice(&ia.t2.to_be_bytes());
for a in &ia.addrs {
// Fixed 24-byte IAADDR body written directly (no per-addr
// sub-Vec alloc); trailing sub-sub-options are never
// emitted here by construction.
data.extend_from_slice(&OPT_IAADDR.to_be_bytes());
data.extend_from_slice(&24u16.to_be_bytes());
data.extend_from_slice(&a.addr.octets());
data.extend_from_slice(&a.preferred.to_be_bytes());
data.extend_from_slice(&a.valid.to_be_bytes());
}
RawDhcpv6Option {
code: OPT_IA_NA,
data,
}
}
Self::DnsServers(addrs) => {
let mut data = Vec::with_capacity(addrs.len() * 16);
for a in addrs {
data.extend_from_slice(&a.octets());
}
RawDhcpv6Option {
code: OPT_DNS_SERVERS,
data,
}
}
Self::StatusCode(code, msg) => {
let mut data = Vec::with_capacity(2 + msg.len());
data.extend_from_slice(&code.to_be_bytes());
data.extend_from_slice(msg.as_bytes());
RawDhcpv6Option {
code: OPT_STATUS_CODE,
data,
}
}
Self::Unrecognized(raw) => raw.clone(),
}
}
#[inline]
pub fn code(&self) -> u16 {
match self {
Self::ClientId(_) => OPT_CLIENTID,
Self::ServerId(_) => OPT_SERVERID,
Self::IaNa(_) => OPT_IA_NA,
Self::DnsServers(_) => OPT_DNS_SERVERS,
Self::StatusCode(_, _) => OPT_STATUS_CODE,
Self::Unrecognized(x) => x.code,
}
}
}
/// Human-readable title for known option codes, if any.
pub fn title(code: u16) -> Option<&'static str> {
Some(match code {
OPT_CLIENTID => "Client Identifier",
OPT_SERVERID => "Server Identifier",
OPT_IA_NA => "Identity Association for Non-temporary Addresses",
OPT_IA_TA => "Identity Association for Temporary Addresses",
OPT_IAADDR => "IA Address",
OPT_ORO => "Option Request",
OPT_PREFERENCE => "Preference",
OPT_ELAPSED_TIME => "Elapsed Time",
OPT_RELAY_MSG => "Relay Message",
OPT_AUTH => "Authentication",
OPT_UNICAST => "Server Unicast",
OPT_STATUS_CODE => "Status Code",
OPT_RAPID_COMMIT => "Rapid Commit",
OPT_USER_CLASS => "User Class",
OPT_VENDOR_CLASS => "Vendor Class",
OPT_VENDOR_OPTS => "Vendor-specific Information",
OPT_INTERFACE_ID => "Interface-Id",
OPT_RECONF_MSG => "Reconfigure Message",
OPT_RECONF_ACCEPT => "Reconfigure Accept",
OPT_DNS_SERVERS => "DNS Recursive Name Server",
OPT_DOMAIN_LIST => "Domain Search List",
OPT_IA_PD => "Identity Association for Prefix Delegation",
OPT_IAPREFIX => "IA Prefix",
_ => return None,
})
}
// ---------------------------------------------------------------------------
// decode helpers
// ---------------------------------------------------------------------------
#[inline]
fn be_u16(input: &[u8]) -> Result<(&[u8], u16), DecodeError> {
if input.len() < 2 {
return Err(DecodeError::Truncated);
}
Ok((
&input[2..],
u16::from_be_bytes([input[0], input[1]]),
))
}
#[inline]
fn be_u32(input: &[u8]) -> Result<(&[u8], u32), DecodeError> {
if input.len() < 4 {
return Err(DecodeError::Truncated);
}
Ok((
&input[4..],
u32::from_be_bytes([input[0], input[1], input[2], input[3]]),
))
}
#[inline]
fn take<'a>(input: &'a [u8], n: usize) -> Result<(&'a [u8], &'a [u8]), DecodeError> {
if input.len() < n {
return Err(DecodeError::Truncated);
}
Ok((&input[n..], &input[..n]))
}
#[inline]
fn decode_iana(data: &[u8]) -> Result<IaNa, DecodeError> {
if data.len() < 12 {
return Err(DecodeError::Malformed);
}
let iaid = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let t1 = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let t2 = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let mut rest = &data[12..];
let mut addrs = Vec::with_capacity(rest.len() / 28);
while !rest.is_empty() {
if rest.len() < 4 {
return Err(DecodeError::Malformed);
}
let slen = u16::from_be_bytes([rest[2], rest[3]]) as usize;
if rest.len() - 4 < slen {
return Err(DecodeError::Malformed);
}
let code = u16::from_be_bytes([rest[0], rest[1]]);
let body = &rest[4..4 + slen];
rest = &rest[4 + slen..];
if code == OPT_IAADDR {
if body.len() < 24 {
return Err(DecodeError::Malformed);
}
let mut octets = [0u8; 16];
octets.copy_from_slice(&body[..16]);
addrs.push(IaAddr {
addr: Ipv6Addr::from(octets),
preferred: u32::from_be_bytes([body[16], body[17], body[18], body[19]]),
valid: u32::from_be_bytes([body[20], body[21], body[22], body[23]]),
});
// any sub-sub-options past byte 24 are ignored
}
// unknown sub-options are skipped by length
}
Ok(IaNa { iaid, t1, t2, addrs })
}
/// Decode one option; returns the remainder plus the option.
#[inline]
pub fn decode_option(input: &[u8]) -> Result<(&[u8], Dhcpv6Option), DecodeError> {
if input.len() < 4 {
return Err(DecodeError::Truncated);
}
let code = u16::from_be_bytes([input[0], input[1]]);
let len = u16::from_be_bytes([input[2], input[3]]) as usize;
if input.len() - 4 < len {
return Err(DecodeError::Truncated);
}
let data = &input[4..4 + len];
let input = &input[4 + len..];
let option = match code {
OPT_CLIENTID => Dhcpv6Option::ClientId(data.to_vec()),
OPT_SERVERID => Dhcpv6Option::ServerId(data.to_vec()),
OPT_IA_NA => Dhcpv6Option::IaNa(decode_iana(data)?),
OPT_DNS_SERVERS => {
if data.len() % 16 != 0 {
return Err(DecodeError::Malformed);
}
let mut addrs = Vec::with_capacity(data.len() / 16);
for chunk in data.chunks_exact(16) {
let mut octets = [0u8; 16];
octets.copy_from_slice(chunk);
addrs.push(Ipv6Addr::from(octets));
}
Dhcpv6Option::DnsServers(addrs)
}
OPT_STATUS_CODE => {
if data.len() < 2 {
return Err(DecodeError::Malformed);
}
let code = u16::from_be_bytes([data[0], data[1]]);
let msg = std::str::from_utf8(&data[2..]).map_err(|_| DecodeError::BadUtf8)?;
Dhcpv6Option::StatusCode(code, msg.to_string())
}
_ => Dhcpv6Option::Unrecognized(RawDhcpv6Option {
code,
data: data.to_vec(),
}),
};
Ok((input, option))
}
// ---------------------------------------------------------------------------
// Packet
// ---------------------------------------------------------------------------
/// A DHCPv6 datagram: message type + 24-bit transaction id + options.
#[derive(Debug, Clone, PartialEq)]
pub struct Packet {
pub msg_type: MsgType,
pub transaction_id: u32,
pub options: Vec<Dhcpv6Option>,
}
impl Packet {
pub fn from(input: &[u8]) -> Result<Packet, DecodeError> {
if input.len() < 4 {
return Err(DecodeError::Truncated);
}
let msg_type = MsgType::from(input[0])?;
let transaction_id =
((input[1] as u32) << 16) | ((input[2] as u32) << 8) | (input[3] as u32);
let mut options = Vec::with_capacity(input.len().saturating_sub(4) / 8);
let mut rest = &input[4..];
while !rest.is_empty() {
let (new_rest, option) = decode_option(rest)?;
rest = new_rest;
options.push(option);
}
Ok(Packet {
msg_type,
transaction_id,
options,
})
}
/// First option with the given code, if any.
#[inline]
pub fn option(&self, code: u16) -> Option<&Dhcpv6Option> {
for o in &self.options {
if o.code() == code {
return Some(o);
}
}
None
}
/// First IA_NA option, if any (handlers key address state off this).
#[inline]
pub fn first_iana(&self) -> Option<&IaNa> {
for o in &self.options {
if let Dhcpv6Option::IaNa(ia) = o {
return Some(ia);
}
}
None
}
/// Single-pass ClientId + IA_NA harvest (first-wins each); saves the
/// 2-3 linear scans handlers otherwise do per packet.
#[inline]
pub fn client_iana(&self) -> (Option<&[u8]>, Option<&IaNa>) {
let (mut c, mut ia) = (None, None);
for o in &self.options {
match o {
Dhcpv6Option::ClientId(d) if c.is_none() => c = Some(d.as_slice()),
Dhcpv6Option::IaNa(a) if ia.is_none() => ia = Some(a),
_ => {}
}
if c.is_some() && ia.is_some() {
break;
}
}
(c, ia)
}
/// Encode to wire bytes. Only the low 24 bits of `transaction_id` are
/// emitted (the field is 3 octets); a single option payload over 65535
/// bytes is rejected with [`EncodeError::OptionTooLarge`].
pub fn encode(&self) -> Result<Vec<u8>, EncodeError> {
let mut out = Vec::with_capacity(4 + self.options.iter().map(|o| o.wire_len()).sum::<usize>());
out.push(self.msg_type as u8);
let tid = self.transaction_id & 0xFF_FF_FF;
out.push(((tid >> 16) & 0xFF) as u8);
out.push(((tid >> 8) & 0xFF) as u8);
out.push((tid & 0xFF) as u8);
for option in &self.options {
option.encode_into(&mut out)?;
}
Ok(out)
}
}
/// True when the packet carries OUR Server ID (exact DUID byte match), i.e.
/// the client selected this server. Anything else (missing, wrong type,
/// different bytes) is false.
#[inline]
pub fn is_for_server(our_duid: &[u8], pkt: &Packet) -> bool {
match pkt.option(OPT_SERVERID) {
Some(Dhcpv6Option::ServerId(d)) => d.as_slice() == our_duid,
_ => false,
}
}
/// Renewal/rebinding defaults for a lease of `lease_secs` seconds, mirroring
/// the RFC 2131 client-side rule the v4 tests use (T1 = 1/2, T2 = 7/8):
/// T1 = secs/2, T2 = secs - secs/8 (overflow-free).
#[inline]
pub fn default_t1_t2(lease_secs: u32) -> (u32, u32) {
(lease_secs / 2, lease_secs - lease_secs / 8)
}
// ---------------------------------------------------------------------------
// Managed range allocator ("managed dhcpv6 range")
// ---------------------------------------------------------------------------
/// Allocator for one contiguous managed IPv6 range
/// `[start, start + count)`, keyed by client DUID bytes.
///
/// Semantics mirror the v4 example (`discover` prefers the current lease,
/// otherwise first-free round-robin; `request` prefers current, else inserts
/// a timed lease; `release` drops it) with one deliberate difference:
/// infinite (`None` expiry) leases are **not** stealable here — unlike the
/// v4 `available()` quirk, `None` means permanently reserved.
///
/// In-range addresses live in a dense slot array indexed by `addr - start`
/// (O(1) loads, no hashing on the miss scan); out-of-range reservations
/// (e.g. `leases6` entries outside the pool) spill to `overflow`. Huge pools
/// (over `MAX_SLOTS`) fall back to hash-only storage.
///
/// Discovery scans linearly like the v4 example, so keep `count` modest
/// (tens of thousands at most); absurd counts make each Solicitation walk
/// the whole range.
pub struct V6Pool {
start: u128,
end: Option<u128>,
count: u64,
last: u64,
lease_duration: Duration,
use_slots: bool,
slots: Vec<Option<(Vec<u8>, Option<Instant>)>>,
filled: usize,
overflow: HashMap<Ipv6Addr, (Vec<u8>, Option<Instant>)>,
by_duid: HashMap<Vec<u8>, Ipv6Addr>,
}
/// Upper bound for slot-array allocation (100k slots ≈ 5MB); larger pools
/// use hash-only storage with identical semantics.
const MAX_SLOTS: u64 = 100_000;
impl V6Pool {
pub fn new(start: Ipv6Addr, count: u64, lease_duration: Duration) -> V6Pool {
let start_num = u128::from(start);
let use_slots = count <= MAX_SLOTS && count <= usize::MAX as u64;
let slots = if use_slots {
let mut s = Vec::new();
s.resize_with(count as usize, || None);
s
} else {
Vec::new()
};
V6Pool {
start: start_num,
end: start_num.checked_add(count as u128),
count,
last: 0,
lease_duration,
use_slots,
slots,
filled: 0,
overflow: HashMap::new(),
by_duid: HashMap::with_capacity(count.min(4096) as usize),
}
}
pub fn start_addr(&self) -> Ipv6Addr {
Ipv6Addr::from(self.start)
}
pub fn count(&self) -> u64 {
self.count
}
/// Number of leases currently held.
pub fn len(&self) -> usize {
self.filled + self.overflow.len()
}
pub fn is_empty(&self) -> bool {
self.filled == 0 && self.overflow.is_empty()
}
fn end(&self) -> Option<u128> {
self.end
}
fn in_range(&self, addr: &Ipv6Addr) -> bool {
let pos: u128 = (*addr).into();
pos >= self.start && self.end.map_or(false, |end| pos < end)
}
/// Slot index for in-range addresses. `None` for out-of-range,
/// overflowed pools (`end` is `None`), and hash-only huge pools.
#[inline]
fn idx(&self, addr: &Ipv6Addr) -> Option<usize> {
if !self.use_slots || self.end.is_none() {
return None;
}
let off = u128::from(*addr).checked_sub(self.start)?;
if off < self.count as u128 {
Some(off as usize)
} else {
None
}
}
/// Unified lookup (slot or overflow) for verification/`get` paths.
#[inline]
fn lookup(&self, addr: &Ipv6Addr) -> Option<&(Vec<u8>, Option<Instant>)> {
match self.idx(addr) {
Some(i) => self.slots[i].as_ref(),
None => self.overflow.get(addr),
}
}
#[inline]
fn slot_available(&self, i: usize, duid: &[u8], now: Instant) -> bool {
match &self.slots[i] {
Some((d, expiry)) => {
d.as_slice() == duid || expiry.map_or(false, |exp| now > exp)
}
None => true,
}
}
/// Insert a reservation directly (used for `leases6` file entries).
/// `None` expiry means a permanent (infinite) reservation.
/// Duplicate DUIDs for different IPs are preserved (like the original:
/// one DUID may hold several IPs; `current_lease` returns one of them),
/// while `by_duid` stays as an O(1) hint for the common 1:1 case.
/// Duplicate IPs last-wins.
pub fn insert(&mut self, addr: Ipv6Addr, duid: Vec<u8>, expiry: Option<Instant>) {
if let Some(i) = self.idx(&addr) {
if let Some((old_duid, _)) = &self.slots[i] {
if *old_duid != duid && self.by_duid.get(old_duid) == Some(&addr) {
self.by_duid.remove(old_duid);
}
}
if self.slots[i].is_none() {
self.filled += 1;
}
self.slots[i] = Some((duid.clone(), expiry));
self.by_duid.insert(duid, addr);
} else if !self.use_slots && self.in_range(&addr) {
// Huge hash-only pool: in-range entries live in overflow.
if let Some((old_duid, _)) = self.overflow.get(&addr) {
if *old_duid != duid && self.by_duid.get(old_duid) == Some(&addr) {
let old = old_duid.clone();
self.by_duid.remove(&old);
}
}
self.by_duid.insert(duid.clone(), addr);
self.overflow.insert(addr, (duid, expiry));
} else {
// Out-of-range reservation spillover.
if let Some((old_duid, _)) = self.overflow.get(&addr) {
if *old_duid != duid && self.by_duid.get(old_duid) == Some(&addr) {
let old = old_duid.clone();
self.by_duid.remove(&old);
}
}
self.by_duid.insert(duid.clone(), addr);
self.overflow.insert(addr, (duid, expiry));
}
}
pub fn get(&self, addr: &Ipv6Addr) -> Option<&(Vec<u8>, Option<Instant>)> {
self.lookup(addr)
}
#[inline]
fn available_at(&self, duid: &[u8], addr: &Ipv6Addr, now: Instant) -> bool {
if let Some(i) = self.idx(addr) {
return self.slot_available(i, duid, now);
}
if !self.use_slots && self.in_range(addr) {
// Huge hash-only pool: consult overflow with the range gate.
return match self.overflow.get(addr) {
Some((d, expiry)) => {
d.as_slice() == duid || expiry.map_or(false, |exp| now > exp)
}
None => true,
};
}
// Out-of-range is never available (verbatim).
false
}
pub fn available(&self, duid: &[u8], addr: &Ipv6Addr) -> bool {
self.available_at(duid, addr, Instant::now())
}
#[inline]
pub fn current_lease(&self, duid: &[u8]) -> Option<Ipv6Addr> {
if self.is_empty() {
return None;
}
// Fast O(1) hint; verified because duplicate-DUID inserts and
// IP-takeover inserts can leave it stale. Falls back to a full scan
// (slots + overflow) so semantics stay identical.
if let Some(ip) = self.by_duid.get(duid).copied() {
if let Some((d, _)) = self.lookup(&ip) {
if d.as_slice() == duid {
return Some(ip);
}
}
}
if self.use_slots {
for (i, s) in self.slots.iter().enumerate() {
if let Some((d, _)) = s {
if d.as_slice() == duid {
return Some(Ipv6Addr::from(self.start + i as u128));
}
}
}
}
for (ip, (d, _)) in &self.overflow {
if d.as_slice() == duid {
return Some(*ip);
}
}
// Huge hash-only pools keep in-range entries in overflow (scanned
// above); nothing else to scan.
None
}
fn candidate(&self, offset: u64) -> Option<Ipv6Addr> {
self.start.checked_add(offset as u128).map(Ipv6Addr::from)
}
/// Offer an address: current lease first, else first-free round-robin.
/// Returns `None` when the pool is exhausted (or empty); never panics,
/// even if `start + count` overflows (candidates past the end are
/// skipped as unavailable).
pub fn discover(&mut self, duid: &[u8]) -> Option<Ipv6Addr> {
if let Some(ip) = self.current_lease(duid) {
return Some(ip);
}
if self.count == 0 {
return None;
}
if !self.use_slots {
// Huge hash-only pool: original candidate path.
let now = Instant::now();
let end = self.end;
for _ in 0..self.count {
self.last = (self.last + 1) % self.count;
if let Some(pos) = self.start.checked_add(self.last as u128) {
if end.map_or(false, |e| pos < e) {
let cand = Ipv6Addr::from(pos);
if self.available_at(duid, &cand, now) {
return Some(cand);
}
}
}
}
return None;
}
let now = Instant::now();
let end = self.end;
for _ in 0..self.count {
self.last = (self.last + 1) % self.count;
let i = self.last as usize;
// Overflowed candidates are skipped (never panics), mirroring
// the old `candidate()` semantics.
if let Some(pos) = self.start.checked_add(self.last as u128) {
if end.map_or(false, |e| pos < e) {
if self.slot_available(i, duid, now) {
return Some(Ipv6Addr::from(pos));
}
}
}
}
None
}
/// Commit a lease: current lease wins unchanged (no
/// expiry refresh, mirroring v4); otherwise insert a timed lease if free.
pub fn request(&mut self, duid: &[u8], addr: Ipv6Addr) -> Result<Ipv6Addr, &'static str> {
if let Some(ip) = self.current_lease(duid) {
return Ok(ip);
}
let now = Instant::now();
if !self.available_at(duid, &addr, now) {
return Err("Requested address not available");
}
let expiry = now + self.lease_duration;
if let Some(i) = self.idx(&addr) {
if self.slots[i].is_none() {
self.filled += 1;
}
self.slots[i] = Some((duid.to_vec(), Some(expiry)));
self.by_duid.insert(duid.to_vec(), addr);
} else {
self.by_duid.insert(duid.to_vec(), addr);
self.overflow.insert(addr, (duid.to_vec(), Some(expiry)));
}
Ok(addr)
}
/// Drop the caller's current lease, if any.
pub fn release(&mut self, duid: &[u8]) -> bool {
if let Some(ip) = self.current_lease(duid) {
if let Some(i) = self.idx(&ip) {
self.slots[i] = None;
self.filled -= 1;
} else {
self.overflow.remove(&ip);
}
if self.by_duid.get(duid) == Some(&ip) {
self.by_duid.remove(duid);
// Re-point the hint at a surviving duplicate, if any.
if self.use_slots {
for (j, s) in self.slots.iter().enumerate() {
if let Some((d, _)) = s {
if d.as_slice() == duid {
self.by_duid.insert(
duid.to_vec(),
Ipv6Addr::from(self.start + j as u128),
);
break;
}
}
}
}
if self.by_duid.get(duid).is_none() {
for (other_ip, (d, _)) in &self.overflow {
if d.as_slice() == duid {
self.by_duid.insert(duid.to_vec(), *other_ip);
break;
}
}
}
}
true
} else {
false
}
}
}
+11
View File
@@ -0,0 +1,11 @@
pub mod config;
pub mod dhcpv6;
pub mod options;
pub mod packet;
pub mod server;
#[cfg(test)]
mod tests {
#[test]
fn it_works() {}
}
+1168
View File
File diff suppressed because it is too large Load Diff
+444
View File
@@ -0,0 +1,444 @@
///use num_traits::FromPrimitive;
use std::net::Ipv4Addr;
#[derive(PartialEq, Clone, Debug)]
pub struct RawDhcpOption {
pub code: u8,
pub data: Vec<u8>,
}
#[derive(PartialEq, Clone, Debug)]
pub enum DhcpOption {
DhcpMessageType(MessageType),
ServerIdentifier(Ipv4Addr),
ParameterRequestList(Vec<u8>),
RequestedIpAddress(Ipv4Addr),
HostName(String),
Router(Vec<Ipv4Addr>),
DomainNameServer(Vec<Ipv4Addr>),
IpAddressLeaseTime(u32),
SubnetMask(Ipv4Addr),
Message(String),
Unrecognized(RawDhcpOption),
}
impl DhcpOption {
/// Wire payload length without allocating (mirrors `to_raw` arm-for-arm).
#[inline]
pub fn data_len(&self) -> usize {
match self {
Self::DhcpMessageType(_) => 1,
Self::ServerIdentifier(_)
| Self::RequestedIpAddress(_)
| Self::SubnetMask(_) => 4,
Self::ParameterRequestList(p) => p.len(),
Self::HostName(s) | Self::Message(s) => s.len(),
Self::Router(a) | Self::DomainNameServer(a) => a.len() * 4,
Self::IpAddressLeaseTime(_) => 4,
Self::Unrecognized(r) => r.data.len(),
}
}
/// Write payload bytes into `out` (must be `>= data_len()`); returns bytes
/// written. Allocation-free counterpart of `to_raw`.
#[inline]
pub fn write_data(&self, out: &mut [u8]) -> usize {
match self {
Self::DhcpMessageType(m) => {
out[0] = *m as u8;
1
}
Self::ServerIdentifier(a) | Self::RequestedIpAddress(a) | Self::SubnetMask(a) => {
out[..4].copy_from_slice(&a.octets());
4
}
Self::ParameterRequestList(p) => {
out[..p.len()].copy_from_slice(p);
p.len()
}
Self::HostName(s) | Self::Message(s) => {
out[..s.len()].copy_from_slice(s.as_bytes());
s.len()
}
Self::Router(addrs) | Self::DomainNameServer(addrs) => {
let mut n = 0;
for a in addrs {
out[n..n + 4].copy_from_slice(&a.octets());
n += 4;
}
n
}
Self::IpAddressLeaseTime(s) => {
out[..4].copy_from_slice(&s.to_be_bytes());
4
}
Self::Unrecognized(r) => {
out[..r.data.len()].copy_from_slice(&r.data);
r.data.len()
}
}
}
/// Cold path (tests/bench only): the hot encode path uses
/// [`DhcpOption::data_len`] + [`DhcpOption::write_data`] instead.
/// Kept out-of-line to protect the hot loop's I-cache.
#[inline(never)]
pub fn to_raw(&self) -> RawDhcpOption {
match self {
Self::DhcpMessageType(mtype) => RawDhcpOption {
code: DHCP_MESSAGE_TYPE,
data: vec![*mtype as u8],
},
Self::ServerIdentifier(addr) => RawDhcpOption {
code: SERVER_IDENTIFIER,
data: addr.octets().to_vec(),
},
Self::ParameterRequestList(prl) => RawDhcpOption {
code: PARAMETER_REQUEST_LIST,
data: prl.clone(),
},
Self::RequestedIpAddress(addr) => RawDhcpOption {
code: REQUESTED_IP_ADDRESS,
data: addr.octets().to_vec(),
},
Self::HostName(name) => RawDhcpOption {
code: HOST_NAME,
data: name.as_bytes().to_vec(),
},
Self::Router(addrs) => RawDhcpOption {
code: ROUTER,
data: {
let mut v = Vec::with_capacity(addrs.len() * 4);
for a in addrs {
v.extend_from_slice(&a.octets());
}
v
},
},
Self::DomainNameServer(addrs) => RawDhcpOption {
code: DOMAIN_NAME_SERVER,
data: {
let mut v = Vec::with_capacity(addrs.len() * 4);
for a in addrs {
v.extend_from_slice(&a.octets());
}
v
},
},
Self::IpAddressLeaseTime(secs) => RawDhcpOption {
code: IP_ADDRESS_LEASE_TIME,
data: secs.to_be_bytes().to_vec(),
},
Self::SubnetMask(mask) => RawDhcpOption {
code: SUBNET_MASK,
data: mask.octets().to_vec(),
},
Self::Message(msg) => RawDhcpOption {
code: MESSAGE,
data: msg.as_bytes().to_vec(),
},
Self::Unrecognized(raw) => raw.clone(),
}
}
#[inline]
pub fn code(&self) -> u8 {
match self {
Self::DhcpMessageType(_) => DHCP_MESSAGE_TYPE,
Self::ServerIdentifier(_) => SERVER_IDENTIFIER,
Self::ParameterRequestList(_) => PARAMETER_REQUEST_LIST,
Self::RequestedIpAddress(_) => REQUESTED_IP_ADDRESS,
Self::HostName(_) => HOST_NAME,
Self::Router(_) => ROUTER,
Self::DomainNameServer(_) => DOMAIN_NAME_SERVER,
Self::IpAddressLeaseTime(_) => IP_ADDRESS_LEASE_TIME,
Self::SubnetMask(_) => SUBNET_MASK,
Self::Message(_) => MESSAGE,
Self::Unrecognized(x) => x.code,
}
}
}
// DHCP Options;
pub const SUBNET_MASK: u8 = 1;
pub const TIME_OFFSET: u8 = 2;
pub const ROUTER: u8 = 3;
pub const TIME_SERVER: u8 = 4;
pub const NAME_SERVER: u8 = 5;
pub const DOMAIN_NAME_SERVER: u8 = 6;
pub const LOG_SERVER: u8 = 7;
pub const COOKIE_SERVER: u8 = 8;
pub const LPR_SERVER: u8 = 9;
pub const IMPRESS_SERVER: u8 = 10;
pub const RESOURCE_LOCATION_SERVER: u8 = 11;
pub const HOST_NAME: u8 = 12;
pub const BOOT_FILE_SIZE: u8 = 13;
pub const MERIT_DUMP_FILE: u8 = 14;
pub const DOMAIN_NAME: u8 = 15;
pub const SWAP_SERVER: u8 = 16;
pub const ROOT_PATH: u8 = 17;
pub const EXTENSIONS_PATH: u8 = 18;
// IP LAYER PARAMETERS PER HOST;
pub const IP_FORWARDING_ENABLE_DISABLE: u8 = 19;
pub const NON_LOCAL_SOURCE_ROUTING_ENABLE_DISABLE: u8 = 20;
pub const POLICY_FILTER: u8 = 21;
pub const MAXIMUM_DATAGRAM_REASSEMBLY_SIZE: u8 = 22;
pub const DEFAULT_IP_TIME_TO_LIVE: u8 = 23;
pub const PATH_MTU_AGING_TIMEOUT: u8 = 24;
pub const PATH_MTU_PLATEAU_TABLE: u8 = 25;
// IP LAYER PARAMETERS PER INTERFACE;
pub const INTERFACE_MTU: u8 = 26;
pub const ALL_SUBNETS_ARE_LOCAL: u8 = 27;
pub const BROADCAST_ADDRESS: u8 = 28;
pub const PERFORM_MASK_DISCOVERY: u8 = 29;
pub const MASK_SUPPLIER: u8 = 30;
pub const PERFORM_ROUTER_DISCOVERY: u8 = 31;
pub const ROUTER_SOLICITATION_ADDRESS: u8 = 32;
pub const STATIC_ROUTE: u8 = 33;
// LINK LAYER PARAMETERS PER INTERFACE;
pub const TRAILER_ENCAPSULATION: u8 = 34;
pub const ARP_CACHE_TIMEOUT: u8 = 35;
pub const ETHERNET_ENCAPSULATION: u8 = 36;
// TCP PARAMETERS;
pub const TCP_DEFAULT_TTL: u8 = 37;
pub const TCP_KEEPALIVE_INTERVAL: u8 = 38;
pub const TCP_KEEPALIVE_GARBAGE: u8 = 39;
// APPLICATION AND SERVICE PARAMETERS;
pub const NETWORK_INFORMATION_SERVICE_DOMAIN: u8 = 40;
pub const NETWORK_INFORMATION_SERVERS: u8 = 41;
pub const NETWORK_TIME_PROTOCOL_SERVERS: u8 = 42;
pub const VENDOR_SPECIFIC_INFORMATION: u8 = 43;
pub const NETBIOS_OVER_TCPIP_NAME_SERVER: u8 = 44;
pub const NETBIOS_OVER_TCPIP_DATAGRAM_DISTRIBUTION_SERVER: u8 = 45;
pub const NETBIOS_OVER_TCPIP_NODE_TYPE: u8 = 46;
pub const NETBIOS_OVER_TCPIP_SCOPE: u8 = 47;
pub const XWINDOW_SYSTEM_FONT_SERVER: u8 = 48;
pub const XWINDOW_SYSTEM_DISPLAY_MANAGER: u8 = 49;
pub const NETWORK_INFORMATION_SERVICEPLUS_DOMAIN: u8 = 64;
pub const NETWORK_INFORMATION_SERVICEPLUS_SERVERS: u8 = 65;
pub const MOBILE_IP_HOME_AGENT: u8 = 68;
pub const SIMPLE_MAIL_TRANSPORT_PROTOCOL: u8 = 69;
pub const POST_OFFICE_PROTOCOL_SERVER: u8 = 70;
pub const NETWORK_NEWS_TRANSPORT_PROTOCOL: u8 = 71;
pub const DEFAULT_WORLD_WIDE_WEB_SERVER: u8 = 72;
pub const DEFAULT_FINGER_SERVER: u8 = 73;
pub const DEFAULT_INTERNET_RELAY_CHAT_SERVER: u8 = 74;
pub const STREETTALK_SERVER: u8 = 75;
pub const STREETTALK_DIRECTORY_ASSISTANCE: u8 = 76;
pub const RELAY_AGENT_INFORMATION: u8 = 82;
// DHCP EXTENSIONS
pub const REQUESTED_IP_ADDRESS: u8 = 50;
pub const IP_ADDRESS_LEASE_TIME: u8 = 51;
pub const OVERLOAD: u8 = 52;
pub const DHCP_MESSAGE_TYPE: u8 = 53;
pub const SERVER_IDENTIFIER: u8 = 54;
pub const PARAMETER_REQUEST_LIST: u8 = 55;
pub const MESSAGE: u8 = 56;
pub const MAXIMUM_DHCP_MESSAGE_SIZE: u8 = 57;
pub const RENEWAL_TIME_VALUE: u8 = 58;
pub const REBINDING_TIME_VALUE: u8 = 59;
pub const VENDOR_CLASS_IDENTIFIER: u8 = 60;
pub const CLIENT_IDENTIFIER: u8 = 61;
pub const TFTP_SERVER_NAME: u8 = 66;
pub const BOOTFILE_NAME: u8 = 67;
pub const USER_CLASS: u8 = 77;
/// Rapid Commit (RFC 4039): empty option by which a client asks the server
/// to skip Offer/Advertise and answer Discover/Solicit immediately. There is
/// deliberately no typed `DhcpOption` variant for it (like NTP option 42):
/// it decodes as `Unrecognized` and handlers detect it by code presence.
pub const RAPID_COMMIT: u8 = 80;
pub const CLIENT_ARCHITECTURE: u8 = 93;
pub const TZ_POSIX_STRING: u8 = 100;
pub const TZ_DATABASE_STRING: u8 = 101;
pub const CLASSLESS_ROUTE_FORMAT: u8 = 121;
/// Returns title of DHCP Option code, if known.
pub fn title(code: u8) -> Option<&'static str> {
Some(match code {
SUBNET_MASK => "Subnet Mask",
TIME_OFFSET => "Time Offset",
ROUTER => "Router",
TIME_SERVER => "Time Server",
NAME_SERVER => "Name Server",
DOMAIN_NAME_SERVER => "Domain Name Server",
LOG_SERVER => "Log Server",
COOKIE_SERVER => "Cookie Server",
LPR_SERVER => "LPR Server",
IMPRESS_SERVER => "Impress Server",
RESOURCE_LOCATION_SERVER => "Resource Location Server",
HOST_NAME => "Host Name",
BOOT_FILE_SIZE => "Boot File Size",
MERIT_DUMP_FILE => "Merit Dump File",
DOMAIN_NAME => "Domain Name",
SWAP_SERVER => "Swap Server",
ROOT_PATH => "Root Path",
EXTENSIONS_PATH => "Extensions Path",
// IP LAYER PARAMETERS PER HOST",
IP_FORWARDING_ENABLE_DISABLE => "IP Forwarding Enable/Disable",
NON_LOCAL_SOURCE_ROUTING_ENABLE_DISABLE => "Non-Local Source Routing Enable/Disable",
POLICY_FILTER => "Policy Filter",
MAXIMUM_DATAGRAM_REASSEMBLY_SIZE => "Maximum Datagram Reassembly Size",
DEFAULT_IP_TIME_TO_LIVE => "Default IP Time-to-live",
PATH_MTU_AGING_TIMEOUT => "Path MTU Aging Timeout",
PATH_MTU_PLATEAU_TABLE => "Path MTU Plateau Table",
// IP LAYER PARAMETERS PER INTERFACE",
INTERFACE_MTU => "Interface MTU",
ALL_SUBNETS_ARE_LOCAL => "All Subnets are Local",
BROADCAST_ADDRESS => "Broadcast Address",
PERFORM_MASK_DISCOVERY => "Perform Mask Discovery",
MASK_SUPPLIER => "Mask Supplier",
PERFORM_ROUTER_DISCOVERY => "Perform Router Discovery",
ROUTER_SOLICITATION_ADDRESS => "Router Solicitation Address",
STATIC_ROUTE => "Static Route",
// LINK LAYER PARAMETERS PER INTERFACE",
TRAILER_ENCAPSULATION => "Trailer Encapsulation",
ARP_CACHE_TIMEOUT => "ARP Cache Timeout",
ETHERNET_ENCAPSULATION => "Ethernet Encapsulation",
// TCP PARAMETERS",
TCP_DEFAULT_TTL => "TCP Default TTL",
TCP_KEEPALIVE_INTERVAL => "TCP Keepalive Interval",
TCP_KEEPALIVE_GARBAGE => "TCP Keepalive Garbage",
// APPLICATION AND SERVICE PARAMETERS",
NETWORK_INFORMATION_SERVICE_DOMAIN => "Network Information Service Domain",
NETWORK_INFORMATION_SERVERS => "Network Information Servers",
NETWORK_TIME_PROTOCOL_SERVERS => "Network Time Protocol Servers",
VENDOR_SPECIFIC_INFORMATION => "Vendor Specific Information",
NETBIOS_OVER_TCPIP_NAME_SERVER => "NetBIOS over TCP/IP Name Server",
NETBIOS_OVER_TCPIP_DATAGRAM_DISTRIBUTION_SERVER => {
"NetBIOS over TCP/IP Datagram Distribution Server"
}
NETBIOS_OVER_TCPIP_NODE_TYPE => "NetBIOS over TCP/IP Node Type",
NETBIOS_OVER_TCPIP_SCOPE => "NetBIOS over TCP/IP Scope",
XWINDOW_SYSTEM_FONT_SERVER => "X Window System Font Server",
XWINDOW_SYSTEM_DISPLAY_MANAGER => "X Window System Display Manager",
NETWORK_INFORMATION_SERVICEPLUS_DOMAIN => "Network Information Service+ Domain",
NETWORK_INFORMATION_SERVICEPLUS_SERVERS => "Network Information Service+ Servers",
MOBILE_IP_HOME_AGENT => "Mobile IP Home Agent",
SIMPLE_MAIL_TRANSPORT_PROTOCOL => "Simple Mail Transport Protocol (SMTP) Server",
POST_OFFICE_PROTOCOL_SERVER => "Post Office Protocol (POP3) Server",
NETWORK_NEWS_TRANSPORT_PROTOCOL => "Network News Transport Protocol (NNTP) Server",
DEFAULT_WORLD_WIDE_WEB_SERVER => "Default World Wide Web (WWW) Server",
DEFAULT_FINGER_SERVER => "Default Finger Server",
DEFAULT_INTERNET_RELAY_CHAT_SERVER => "Default Internet Relay Chat (IRC) Server",
STREETTALK_SERVER => "StreetTalk Server",
STREETTALK_DIRECTORY_ASSISTANCE => "StreetTalk Directory Assistance (STDA) Server",
RELAY_AGENT_INFORMATION => "Relay Agent Information",
// DHCP EXTENSIONS
REQUESTED_IP_ADDRESS => "Requested IP Address",
IP_ADDRESS_LEASE_TIME => "IP Address Lease Time",
OVERLOAD => "Overload",
DHCP_MESSAGE_TYPE => "DHCP Message Type",
SERVER_IDENTIFIER => "Server Identifier",
PARAMETER_REQUEST_LIST => "Parameter Request List",
MESSAGE => "Message",
MAXIMUM_DHCP_MESSAGE_SIZE => "Maximum DHCP Message Size",
RENEWAL_TIME_VALUE => "Renewal (T1) Time Value",
REBINDING_TIME_VALUE => "Rebinding (T2) Time Value",
VENDOR_CLASS_IDENTIFIER => "Vendor class identifier",
CLIENT_IDENTIFIER => "Client-identifier",
// Find below
TFTP_SERVER_NAME => "TFTP server name",
BOOTFILE_NAME => "Bootfile name",
USER_CLASS => "User Class",
RAPID_COMMIT => "Rapid Commit",
CLIENT_ARCHITECTURE => "Client Architecture",
TZ_POSIX_STRING => "TZ-POSIX String",
TZ_DATABASE_STRING => "TZ-Database String",
CLASSLESS_ROUTE_FORMAT => "Classless Route Format",
_ => return None,
})
}
///
/// DHCP Message Type.
///
/// # Standards
///
/// The semantics of the various DHCP message types are described in RFC 2131 (see Table 2).
/// Their numeric values are described in Section 9.6 of RFC 2132, which begins:
///
/// > This option is used to convey the type of the DHCP message. The code for this option is 53,
/// > and its length is 1.
///
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum MessageType {
/// Client broadcast to locate available servers.
Discover = 1,
/// Server to client in response to DHCPDISCOVER with offer of configuration parameters.
Offer = 2,
/// Client message to servers either (a) requesting offered parameters from one server and
/// implicitly declining offers from all others, (b) confirming correctness of previously
/// allocated address after, e.g., system reboot, or (c) extending the lease on a particular
/// network address.
Request = 3,
/// Client to server indicating network address is already in use.
Decline = 4,
/// Server to client with configuration parameters, including committed network address.
Ack = 5,
/// Server to client indicating client's notion of network address is incorrect (e.g., client
/// has moved to new subnet) or client's lease as expired.
Nak = 6,
/// Client to server relinquishing network address and cancelling remaining lease.
Release = 7,
/// Client to server, asking only for local configuration parameters; client already has
/// externally configured network address.
Inform = 8,
}
impl MessageType {
/// Allocation-free check for hot paths; `from` stays as the
/// String-returning wrapper for API compatibility.
#[inline]
pub fn from_option(val: u8) -> Option<MessageType> {
match val {
1 => Some(MessageType::Discover),
2 => Some(MessageType::Offer),
3 => Some(MessageType::Request),
4 => Some(MessageType::Decline),
5 => Some(MessageType::Ack),
6 => Some(MessageType::Nak),
7 => Some(MessageType::Release),
8 => Some(MessageType::Inform),
_ => None,
}
}
#[inline(never)]
pub fn from(val: u8) -> Result<MessageType, String> {
Self::from_option(val).ok_or_else(|| format!("Invalid DHCP Message Type: {:?}", val))
}
}
+270
View File
@@ -0,0 +1,270 @@
use crate::options::*;
use std::net::Ipv4Addr;
pub enum CustomErr<I> {
NomError((I, ErrorKind)),
NonUtf8String,
UnrecognizedMessageType,
InvalidHlen,
}
pub enum ErrorKind {
Tag,
MapRes,
ManyTill,
Eof,
Custom(u32),
}
type IResult<I, O> = Result<(I, O), CustomErr<I>>;
/// DHCP Packet Structure
#[derive(Debug)]
pub struct Packet {
pub reply: bool, // false = request, true = reply
pub hops: u8,
pub xid: u32, // Random identifier
pub secs: u16,
pub broadcast: bool,
pub ciaddr: Ipv4Addr,
pub yiaddr: Ipv4Addr,
pub siaddr: Ipv4Addr,
pub giaddr: Ipv4Addr,
pub chaddr: [u8; 6],
pub options: Vec<DhcpOption>,
}
#[inline(always)]
fn ipv4_of(data: &[u8]) -> Result<Ipv4Addr, CustomErr<&[u8]>> {
if data.len() < 4 {
return Err(CustomErr::InvalidHlen);
}
Ok(Ipv4Addr::new(data[0], data[1], data[2], data[3]))
}
pub fn decode_option(input: &[u8]) -> IResult<&[u8], DhcpOption> {
if input.is_empty() {
return Err(CustomErr::InvalidHlen);
}
let code = input[0];
assert!(code != END);
let len = *input.get(1).ok_or(CustomErr::InvalidHlen)? as usize;
if input.len() - 2 < len {
return Err(CustomErr::InvalidHlen);
}
let data = &input[2..2 + len];
let input = &input[2 + len..];
let option = match code {
// Frequency-ordered: 53 on every packet, 55 on ~90%, then 54/50.
DHCP_MESSAGE_TYPE => {
// Preserve quirk: empty data -> InvalidHlen, bad value ->
// UnrecognizedMessageType, trailing bytes ignored.
if data.is_empty() {
return Err(CustomErr::InvalidHlen);
}
match MessageType::from_option(data[0]) {
Some(x) => DhcpOption::DhcpMessageType(x),
None => return Err(CustomErr::UnrecognizedMessageType),
}
}
PARAMETER_REQUEST_LIST => DhcpOption::ParameterRequestList(data.to_vec()),
SERVER_IDENTIFIER => DhcpOption::ServerIdentifier(ipv4_of(data)?),
REQUESTED_IP_ADDRESS => DhcpOption::RequestedIpAddress(ipv4_of(data)?),
HOST_NAME => DhcpOption::HostName(match std::str::from_utf8(data) {
Ok(s) => s.to_string(),
Err(_) => return Err(CustomErr::NonUtf8String),
}),
// Locked quirk: Router/DNS ALWAYS fail with InvalidHlen because the
// old custom_many0(decode_ipv4) expected NomError termination but
// decode_ipv4 yields InvalidHlen. Preserve without the overhead.
ROUTER | DOMAIN_NAME_SERVER => return Err(CustomErr::InvalidHlen),
IP_ADDRESS_LEASE_TIME => {
if data.len() < 4 {
return Err(CustomErr::InvalidHlen);
}
DhcpOption::IpAddressLeaseTime(u32::from_be_bytes([
data[0], data[1], data[2], data[3],
]))
}
SUBNET_MASK => DhcpOption::SubnetMask(ipv4_of(data)?),
MESSAGE => DhcpOption::Message(match std::str::from_utf8(data) {
Ok(s) => s.to_string(),
Err(_) => return Err(CustomErr::NonUtf8String),
}),
_ => DhcpOption::Unrecognized(RawDhcpOption {
code,
data: data.to_vec(),
}),
};
Ok((input, option))
}
/// Parses Packet from byte array
fn decode(input: &[u8]) -> IResult<&[u8], Packet> {
if input.len() < 236 {
return Err(CustomErr::InvalidHlen);
}
let (hdr, mut rest) = input.split_at(236);
// Header fields at fixed offsets (mirrors the closure version exactly).
let reply = match hdr[0] {
BOOT_REPLY => true,
BOOT_REQUEST => false,
_ => false,
};
let hlen = hdr[2];
if hlen != 6 {
return Err(CustomErr::InvalidHlen);
}
let hops = hdr[3];
let xid = u32::from_be_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
let secs = u16::from_be_bytes([hdr[8], hdr[9]]);
let flags = u16::from_be_bytes([hdr[10], hdr[11]]);
let ciaddr = Ipv4Addr::new(hdr[12], hdr[13], hdr[14], hdr[15]);
let yiaddr = Ipv4Addr::new(hdr[16], hdr[17], hdr[18], hdr[19]);
let siaddr = Ipv4Addr::new(hdr[20], hdr[21], hdr[22], hdr[23]);
let giaddr = Ipv4Addr::new(hdr[24], hdr[25], hdr[26], hdr[27]);
let chaddr = [hdr[28], hdr[29], hdr[30], hdr[31], hdr[32], hdr[33]];
// Cookie check preserves NomError(Tag) kind.
if rest.len() < 4 || rest[0..4] != COOKIE {
return Err(CustomErr::NomError((rest, ErrorKind::Tag)));
}
rest = &rest[4..];
// Live traffic carries 3-6 options (Discover 3, Request 6); 7-8-opt
// packets pay one cheap 6->12 growth instead of every decode wasting
// two slots (~64B) of malloc+memset.
let mut options = Vec::with_capacity(6);
while let Ok((new_rest, option)) = decode_option(rest) {
rest = new_rest;
options.push(option);
if rest.first().copied() == Some(END) {
break;
}
}
let input = rest.split_at(1).1; // Skip the END tag byte
Ok((
input,
Packet {
reply,
hops,
secs,
broadcast: flags & 128 == 128,
ciaddr,
yiaddr,
siaddr,
giaddr,
options,
chaddr,
xid,
},
))
}
impl Packet {
pub fn from(input: &[u8]) -> Result<Packet, CustomErr<&[u8]>> {
Ok(decode(input)?.1)
}
/// Extracts requested option payload from packet if available
#[inline]
pub fn option(&self, code: u8) -> Option<&DhcpOption> {
for o in &self.options {
if o.code() == code {
return Some(o);
}
}
None
}
/// Convenience function for extracting a packet's message type.
/// Single pass (no second scan via option()), preserving the
/// Unrecognized(53) wrong-enum quirk.
#[inline]
pub fn message_type(&self) -> Result<MessageType, String> {
for o in &self.options {
match o {
DhcpOption::DhcpMessageType(m) => return Ok(*m),
DhcpOption::Unrecognized(r) if r.code == DHCP_MESSAGE_TYPE => {
return Err(format![
"Got wrong enum code {} for DHCP_MESSAGE_TYPE",
r.code
])
}
_ => continue,
}
}
Err("Packet does not have MessageType option".to_string())
}
pub fn encode<'a>(&'a self, p: &'a mut [u8]) -> &[u8] {
let broadcast_flag = if self.broadcast { 128 } else { 0 };
let mut length = 240;
p[..12].copy_from_slice(&[
if self.reply { BOOT_REPLY } else { BOOT_REQUEST },
1,
6,
self.hops,
((self.xid >> 24) & 0xFF) as u8,
((self.xid >> 16) & 0xFF) as u8,
((self.xid >> 8) & 0xFF) as u8,
(self.xid & 0xFF) as u8,
(self.secs >> 8) as u8,
(self.secs & 255) as u8,
broadcast_flag,
0,
]);
p[12..16].copy_from_slice(&self.ciaddr.octets());
p[16..20].copy_from_slice(&self.yiaddr.octets());
p[20..24].copy_from_slice(&self.siaddr.octets());
p[24..28].copy_from_slice(&self.giaddr.octets());
p[28..34].copy_from_slice(&self.chaddr);
p[34..236].fill(0);
p[236..240].copy_from_slice(&COOKIE);
for option in &self.options {
let option_len = option.data_len();
if length + 2 + option_len >= WIRE_MAX {
break;
}
if let Some(dest) = p.get_mut(length..length + 2 + option_len) {
dest[0] = option.code();
dest[1] = option_len as u8;
option.write_data(&mut dest[2..]);
}
length += 2 + option_len;
}
if let Some(end_segment) = p.get_mut(length..length + 1) {
end_segment[0] = END;
}
length += 1;
if let Some(pad_segment) = p.get_mut(length..WIRE_MAX) {
pad_segment.fill(PAD);
}
&p[..length]
}
}
/// Maximum encoded packet size: the BOOTP minimum (RFC 951). The fixed
/// header + cookie occupy 240 bytes, leaving 59 bytes for options + END —
/// enough for a complete standard Offer (message type, server id, lease,
/// mask, router, DNS, NTP ≈ 43 bytes). Smaller ceilings amputate mandatory
/// options (e.g. Server Identifier) off real replies.
/// Callers must supply a buffer of at least this size.
pub const WIRE_MAX: usize = 300;
const COOKIE: [u8; 4] = [99, 130, 83, 99];
const BOOT_REQUEST: u8 = 1; // From Client;
const BOOT_REPLY: u8 = 2; // From Server;
const END: u8 = 255;
const PAD: u8 = 0;
+305
View File
@@ -0,0 +1,305 @@
//! This is a convenience module that simplifies the writing of a DHCP server service.
use std::net::{Ipv4Addr, SocketAddr, UdpSocket};
use crate::options;
use crate::options::{DhcpOption, MessageType};
use crate::packet::*;
pub struct Server {
socket: UdpSocket,
src: SocketAddr,
server_ip: Ipv4Addr,
/// Broadcast destinations, primary first. Unicast replies always go
/// once to the peer; broadcast-routed replies go to each address.
broadcasts: Vec<Ipv4Addr>,
}
pub trait Handler {
fn handle_request(&mut self, server: &Server, in_packet: Packet);
}
/// Short reason for a rejected datagram (diagnostics; no allocation).
fn describe_decode_error(e: &CustomErr<&[u8]>) -> &'static str {
match e {
CustomErr::NomError(_) => "bad magic cookie",
CustomErr::NonUtf8String => "non-UTF8 string option",
CustomErr::UnrecognizedMessageType => "unrecognized message type",
CustomErr::InvalidHlen => "truncated header or option",
}
}
pub fn filter_options_by_req(opts: &mut Vec<DhcpOption>, req_params: &[u8]) {
let n = opts.len();
if n == 0 {
return;
}
// Fast path: cache option codes once so the O(n*m) scan compares u8s
// instead of re-running the `code()` match per probe. Swap sequence and
// truncation are bit-identical to the original (including duplicate-code
// quirks) because `codes` is kept in lockstep with `opts`.
if n > 64 {
return filter_options_by_req_slow(opts, req_params);
}
let mut codes = [0u8; 64];
for (i, o) in opts.iter().enumerate() {
codes[i] = o.code();
}
let mut pos = 0usize;
for r in req_params.iter() {
let mut found = None;
for i in pos..n {
if codes[i] == *r {
found = Some(i);
break;
}
}
if let Some(i) = found {
if i != pos {
opts.swap(i, pos);
codes.swap(i, pos);
}
pos += 1;
}
}
const H: [u8; 6] = [
options::DHCP_MESSAGE_TYPE,
options::SERVER_IDENTIFIER,
options::SUBNET_MASK,
options::IP_ADDRESS_LEASE_TIME,
options::DOMAIN_NAME_SERVER,
options::ROUTER,
];
for r in H.iter() {
let mut found = None;
for i in pos..n {
if codes[i] == *r {
found = Some(i);
break;
}
}
if let Some(i) = found {
if i != pos {
opts.swap(i, pos);
codes.swap(i, pos);
}
pos += 1;
}
}
opts.truncate(pos);
}
fn filter_options_by_req_slow(opts: &mut Vec<DhcpOption>, req_params: &[u8]) {
let mut pos = 0;
let h = &[
options::DHCP_MESSAGE_TYPE,
options::SERVER_IDENTIFIER,
options::SUBNET_MASK,
options::IP_ADDRESS_LEASE_TIME,
options::DOMAIN_NAME_SERVER,
options::ROUTER,
] as &[u8];
// Process options from req_params
for r in req_params.iter() {
let mut found = false;
for (i, o) in opts[pos..].iter().enumerate() {
if o.code() == *r {
found = true;
if pos + i != pos {
opts.swap(pos + i, pos);
}
pos += 1;
break;
}
}
if !found {
// Option not found, continue searching
}
}
// Process options from h
for r in h.iter() {
let mut found = false;
for (i, o) in opts[pos..].iter().enumerate() {
if o.code() == *r {
found = true;
if pos + i != pos {
opts.swap(pos + i, pos);
}
pos += 1;
break;
}
}
if !found {
// Option not found, continue searching
}
}
// Truncate the options list if necessary
opts.truncate(pos);
}
impl Server {
pub fn serve<H: Handler>(
udp_soc: UdpSocket,
server_ip: Ipv4Addr,
broadcast_ip: Ipv4Addr,
handler: H,
) -> std::io::Error {
Self::serve_with_broadcasts(udp_soc, server_ip, vec![broadcast_ip], handler)
}
/// Serve with several broadcast destinations: broadcast-routed replies
/// are sent to each address (unicast replies still go once to the peer).
/// An empty list falls back to the limited broadcast address.
pub fn serve_with_broadcasts<H: Handler>(
udp_soc: UdpSocket,
server_ip: Ipv4Addr,
broadcast_ips: Vec<Ipv4Addr>,
mut handler: H,
) -> std::io::Error {
let broadcasts = if broadcast_ips.is_empty() {
vec![Ipv4Addr::BROADCAST]
} else {
broadcast_ips
};
let mut in_buf: [u8; 1500] = [0; 1500];
let mut s = Server {
socket: udp_soc,
server_ip,
broadcasts,
src: SocketAddr::new(std::net::IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0),
};
loop {
match s.socket.recv_from(&mut in_buf) {
Err(e) => return e,
Ok((l, src)) => match Packet::from(&in_buf[..l]) {
Ok(p) => {
s.src = src;
handler.handle_request(&s, p);
}
Err(e) => {
// Diagnose silent handshake failures: undecodable
// datagrams never reach the handler, so log why here.
eprintln!(
"ignoring malformed DHCP packet ({} bytes): {}",
l,
describe_decode_error(&e)
);
}
},
}
}
}
/// Constructs and sends a reply packet back to the client.
/// additional_options should not include DHCP_MESSAGE_TYPE nor SERVER_IDENTIFIER as these
/// are added automatically.
pub fn reply(
&self,
msg_type: MessageType,
additional_options: Vec<DhcpOption>,
offer_ip: Ipv4Addr,
req_packet: Packet,
) -> std::io::Result<usize> {
let ciaddr = match msg_type {
MessageType::Nak => Ipv4Addr::new(0, 0, 0, 0),
_ => req_packet.ciaddr,
};
let mut opts: Vec<DhcpOption> = Vec::with_capacity(additional_options.len() + 2);
opts.push(DhcpOption::DhcpMessageType(msg_type));
opts.push(DhcpOption::ServerIdentifier(self.server_ip));
opts.extend(additional_options);
if let Some(DhcpOption::ParameterRequestList(prl)) =
req_packet.option(options::PARAMETER_REQUEST_LIST)
{
filter_options_by_req(&mut opts, prl);
}
self.send(Packet {
reply: true,
hops: 0,
xid: req_packet.xid,
secs: 0,
broadcast: req_packet.broadcast,
ciaddr,
yiaddr: offer_ip,
siaddr: Ipv4Addr::new(0, 0, 0, 0),
giaddr: req_packet.giaddr,
chaddr: req_packet.chaddr,
options: opts,
})
}
/// Checks the packet see if it was intended for this DHCP server (as opposed to some other also on the network).
pub fn for_this_server(&self, packet: &Packet) -> bool {
match packet.option(options::SERVER_IDENTIFIER) {
Some(DhcpOption::ServerIdentifier(x)) => x.to_bits() == self.server_ip.to_bits(),
_ => false,
}
}
/// Peer address of the last-received datagram (diagnostics: combined
/// with the packet's broadcast flag this determines reply routing).
pub fn peer(&self) -> SocketAddr {
self.src
}
/// Reply delivery plan (pure, unit-testable).
///
/// Unicast back to the peer when it has a usable source address.
/// Otherwise broadcast — plus, with the broadcast flag clear and a
/// committed/offered address present, a unicast copy to `yiaddr`
/// (RFC 2131 §4.1: a clear flag advertises unicast capability).
/// That copy is what sleeping broadcast-deaf clients accept, and what
/// dnsmasq-style servers send; broadcast copies still go out alongside.
pub fn route_dests(
peer: SocketAddr,
broadcast: bool,
yiaddr: Ipv4Addr,
broadcasts: &[Ipv4Addr],
) -> Vec<SocketAddr> {
if broadcast || peer.ip().is_unspecified() {
let mut out = Vec::with_capacity(broadcasts.len() + 1);
for b in broadcasts {
let mut dst = peer;
dst.set_ip(std::net::IpAddr::V4(*b));
out.push(dst);
}
if !broadcast && peer.ip().is_unspecified() && !yiaddr.is_unspecified() {
out.push(SocketAddr::new(std::net::IpAddr::V4(yiaddr), peer.port()));
}
out
} else {
vec![peer]
}
}
/// Encodes and sends a DHCP packet back to the client. Delivery follows
/// [`Server::route_dests`]: unicast replies go once to the peer (zero
/// extra allocation, as before); broadcast-routed replies are encoded
/// once and fanned out. Returns the last send's byte count.
pub fn send(&self, p: Packet) -> std::io::Result<usize> {
let addr = self.src;
if !p.broadcast && !addr.ip().is_unspecified() {
// Fast path: sourced peer, no broadcast — byte-identical to the
// historical behavior (bench-sensitive: no allocation here).
let mut out_buf = [0u8; crate::packet::WIRE_MAX];
return self.socket.send_to(p.encode(&mut out_buf), addr);
}
// WIRE_MAX is the DHCP wire ceiling (see packet::Packet::encode):
// 240-byte header+cookie, options, END, PAD-to-WIRE_MAX. Bytes
// beyond are never read. Encoded once, fanned out to each address.
let mut out_buf = [0u8; crate::packet::WIRE_MAX];
let wire = p.encode(&mut out_buf);
let mut sent = 0;
for dst in Self::route_dests(addr, p.broadcast, p.yiaddr, &self.broadcasts) {
sent = self.socket.send_to(wire, dst)?;
}
Ok(sent)
}
}