mirror of
https://github.com/pj1234678/RustyDHCP.git
synced 2026-10-16 17:23:15 +08:00
Version 2 Added config file and IPV6 support and benchmarks with massive performance uplifts.
This commit is contained in:
+323
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
+444
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user