use super::*;
use tokio::time::sleep;
#[cfg(feature = "dhcp-debug")]
macro_rules! dhcp_debug {
($($arg:tt)*) => {
debug!("[{}:{}] {}", file!(), line!(), format!($($arg)*))
};
}
#[cfg(not(feature = "dhcp-debug"))]
macro_rules! dhcp_debug {
($($arg:tt)*) => {};
}
#[cfg(feature = "dhcp-debug")]
macro_rules! dhcp_info {
($($arg:tt)*) => {
info!("[{}:{}] {}", file!(), line!(), format!($($arg)*))
};
}
#[cfg(not(feature = "dhcp-debug"))]
macro_rules! dhcp_info {
($($arg:tt)*) => {};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Dhcpv4State {
Init,
Selecting,
Requesting,
Bound,
Renewing,
Rebinding,
InitReboot,
}
impl std::fmt::Display for Dhcpv4State {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Init => write!(f, "INIT"),
Self::Selecting => write!(f, "SELECTING"),
Self::Requesting => write!(f, "REQUESTING"),
Self::Bound => write!(f, "BOUND"),
Self::Renewing => write!(f, "RENEWING"),
Self::Rebinding => write!(f, "REBINDING"),
Self::InitReboot => write!(f, "INIT-REBOOT"),
}
}
}
#[derive(Debug, Clone)]
pub struct Dhcpv4Lease {
pub ip_address: Ipv4Addr,
pub subnet_mask: Option<Ipv4Addr>,
pub router: Option<Ipv4Addr>,
pub dns_servers: Vec<Ipv4Addr>,
pub domain_name: Option<String>,
pub ntp_servers: Vec<Ipv4Addr>,
pub lease_time: Duration,
pub renewal_time: Duration,
pub rebinding_time: Duration,
pub server_id: Ipv4Addr,
pub acquired_at: SystemTime,
}
impl Dhcpv4Lease {
pub fn expires_at(&self) -> SystemTime {
self.acquired_at + self.lease_time
}
pub fn renewal_at(&self) -> SystemTime {
self.acquired_at + self.renewal_time
}
pub fn rebinding_at(&self) -> SystemTime {
self.acquired_at + self.rebinding_time
}
pub fn is_expired(&self) -> bool {
SystemTime::now() > self.expires_at()
}
pub fn needs_renewal(&self) -> bool {
SystemTime::now() > self.renewal_at()
}
pub fn needs_rebinding(&self) -> bool {
SystemTime::now() > self.rebinding_at()
}
pub fn to_info(&self) -> Dhcpv4LeaseInfo {
Dhcpv4LeaseInfo {
ip_address: self.ip_address,
subnet_mask: self.subnet_mask.unwrap_or(Ipv4Addr::new(255, 255, 255, 0)),
router: self.router,
dns_servers: self.dns_servers.clone(),
domain_name: self.domain_name.clone(),
ntp_servers: self.ntp_servers.clone(),
lease_time: self.lease_time,
renewal_time: self.renewal_time,
rebinding_time: self.rebinding_time,
acquired_at: self.acquired_at,
expires_at: self.expires_at(),
}
}
}
pub struct Dhcpv4Client {
interface: String,
mac_address: [u8; 6],
state: Arc<RwLock<Dhcpv4State>>,
config: Dhcpv4Config,
lease: Arc<RwLock<Option<Dhcpv4Lease>>>,
servers: Arc<RwLock<Vec<DhcpServer>>>,
security: SecurityContext,
shutdown: Arc<Mutex<bool>>,
xid: Arc<Mutex<u32>>,
offered_ip: Arc<RwLock<Option<Ipv4Addr>>>, selected_server: Arc<RwLock<Option<Ipv4Addr>>>, wifi_config: Option<WifiNetworkConfig>, }
impl std::fmt::Debug for Dhcpv4Client {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Dhcpv4Client")
.field("interface", &self.interface)
.field("mac_address", &self.mac_address)
.finish()
}
}
impl Dhcpv4Client {
pub fn new(
interface: &str,
config: Dhcpv4Config,
security: SecurityConfig,
wifi_config: Option<WifiNetworkConfig>,
) -> Result<Self> {
validate_interface_name(interface)?;
dhcp_debug!("========================================");
dhcp_debug!("DEBUG: Dhcpv4Client::new() called");
dhcp_debug!(" interface: {}", interface);
let mac_address = Self::get_mac_address(interface)?;
dhcp_debug!(" MAC address: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
mac_address[0], mac_address[1], mac_address[2],
mac_address[3], mac_address[4], mac_address[5]);
let security_ctx = SecurityContext::new(security);
security_ctx.validate_mac_address(&mac_address)?;
let xid = SecurityContext::generate_xid();
dhcp_debug!(" Generated transaction ID: 0x{:08x}", xid);
dhcp_debug!("DEBUG: Dhcpv4Client created successfully");
dhcp_debug!("========================================");
Ok(Self {
interface: interface.to_string(),
mac_address,
state: Arc::new(RwLock::new(Dhcpv4State::Init)),
config,
lease: Arc::new(RwLock::new(None)),
servers: Arc::new(RwLock::new(Vec::new())),
security: security_ctx,
shutdown: Arc::new(Mutex::new(false)),
xid: Arc::new(Mutex::new(xid)),
offered_ip: Arc::new(RwLock::new(None)),
selected_server: Arc::new(RwLock::new(None)),
wifi_config,
})
}
fn get_mac_address(interface: &str) -> Result<[u8; 6]> {
use std::fs;
use std::thread;
use std::time::Duration;
let mac_path = format!("/sys/class/net/{}/address", interface);
let max_attempts = 10;
let retry_delay = Duration::from_millis(500);
let mut last_error = String::new();
for attempt in 1..=max_attempts {
match fs::read_to_string(&mac_path) {
Ok(mac_str) => {
let mac_str = mac_str.trim();
let parts: Vec<&str> = mac_str.split(':').collect();
if parts.len() != 6 {
return Err(DhcpClientError::InterfaceNotFound(
format!("Invalid MAC address format for interface {}", interface)
));
}
let mut mac = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
mac[i] = u8::from_str_radix(part, 16)
.map_err(|_| DhcpClientError::InterfaceNotFound(
format!("Invalid MAC address for interface {}", interface)
))?;
}
if attempt > 1 {
debug!("Successfully read MAC address for {} after {} attempts", interface, attempt);
}
return Ok(mac);
}
Err(e) => {
last_error = format!("Attempt {}/{}: {}", attempt, max_attempts, e);
if attempt < max_attempts {
debug!("Interface {} not ready, retrying in {:?}... (attempt {}/{})",
interface, retry_delay, attempt, max_attempts);
thread::sleep(retry_delay);
}
}
}
}
Err(DhcpClientError::InterfaceNotFound(
format!("Interface {} not found after {} attempts ({}). Last error: {}",
interface, max_attempts, mac_path, last_error)
))
}
pub fn interface(&self) -> &str {
&self.interface
}
pub fn get_status(&self) -> Dhcpv4Status {
let state = self.state.blocking_read();
let lease = self.lease.blocking_read();
let servers = self.servers.blocking_read();
Dhcpv4Status {
interface: self.interface.clone(),
state: state.to_string(),
lease: lease.as_ref().map(|l| l.to_info()),
servers: servers.clone(),
}
}
pub async fn run(&self) -> Result<()> {
info!("========================================");
info!("Starting DHCPv4 client on {}", self.interface);
info!("========================================");
if let Err(e) = self.bring_up_interface().await {
warn!("Failed to bring up interface {} (continuing anyway): {}", self.interface, e);
}
loop {
if *self.shutdown.lock().await {
info!("DHCPv4 client shutting down on {}", self.interface);
break;
}
let state = self.state.read().await.clone();
dhcp_debug!("----------------------------------------");
dhcp_debug!("DEBUG: Current state: {}", state);
dhcp_debug!("----------------------------------------");
match state {
Dhcpv4State::Init => {
dhcp_info!("STATE: INIT -> Sending DISCOVER and waiting for OFFER");
let socket = self.do_discover().await?;
dhcp_debug!("DEBUG: State transition: INIT -> SELECTING");
*self.state.write().await = Dhcpv4State::Selecting;
dhcp_info!("STATE: SELECTING -> Waiting for OFFER");
if let Some(server) = self.wait_for_offer(socket).await? {
dhcp_info!("STATE: SELECTING -> Received offer from {}", server.address);
let request_socket = self.do_request(&server).await?;
dhcp_debug!("DEBUG: State transition: SELECTING -> REQUESTING");
*self.state.write().await = Dhcpv4State::Requesting;
dhcp_info!("STATE: REQUESTING -> Waiting for ACK");
if let Some(lease) = self.wait_for_ack(request_socket).await? {
dhcp_info!("Performing ARP probe for {} before accepting lease", lease.ip_address);
match self.arp_probe(lease.ip_address).await {
Ok(true) => {
warn!("IP address {} is already in use! Sending DHCPDECLINE", lease.ip_address);
if let Err(e) = self.decline(lease.ip_address, lease.server_id).await {
error!("Failed to send DHCPDECLINE: {}", e);
}
continue;
}
Ok(false) => {
dhcp_info!("ARP probe passed, IP {} is available", lease.ip_address);
}
Err(e) => {
warn!("ARP probe failed (continuing anyway): {}", e);
}
}
*self.lease.write().await = Some(lease.clone());
dhcp_debug!("DEBUG: State transition: REQUESTING -> BOUND");
*self.state.write().await = Dhcpv4State::Bound;
dhcp_info!("DHCPv4 lease acquired on {}: {:?}", self.interface,
self.lease.read().await.as_ref().map(|l| l.ip_address));
dhcp_info!("Configuring interface {} with lease", self.interface);
if let Err(e) = self.apply_lease(&lease).await {
error!("Failed to apply lease to {}: {}", self.interface, e);
} else {
info!("✓ Interface {} configured successfully", self.interface);
}
} else {
dhcp_info!("STATE: REQUESTING -> No ACK received, starting over");
dhcp_debug!("DEBUG: State transition: REQUESTING -> INIT");
*self.state.write().await = Dhcpv4State::Init;
dhcp_info!(" Sleeping {} seconds before retry", self.config.timeout);
sleep(Duration::from_secs(self.config.timeout)).await;
}
} else {
dhcp_info!("STATE: SELECTING -> No offer received, retrying");
dhcp_debug!("DEBUG: State transition: SELECTING -> INIT");
*self.state.write().await = Dhcpv4State::Init;
dhcp_info!(" Sleeping {} seconds before retry", self.config.timeout);
sleep(Duration::from_secs(self.config.timeout)).await;
}
}
Dhcpv4State::Selecting => {
dhcp_debug!("DEBUG: In Selecting state unexpectedly, transitioning to Init");
*self.state.write().await = Dhcpv4State::Init;
}
Dhcpv4State::Requesting => {
dhcp_info!("STATE: REQUESTING -> Unexpectedly in this state, starting over");
*self.state.write().await = Dhcpv4State::Init;
}
Dhcpv4State::Bound => {
if let Some(lease) = self.lease.read().await.as_ref() {
let now = SystemTime::now();
let renewal_at = lease.renewal_at();
if now >= renewal_at {
*self.state.write().await = Dhcpv4State::Renewing;
} else {
let wait_time = renewal_at.duration_since(now).unwrap_or(Duration::from_secs(60));
sleep(wait_time.min(Duration::from_secs(60))).await;
}
} else {
*self.state.write().await = Dhcpv4State::Init;
}
}
Dhcpv4State::Renewing => {
if let Err(e) = self.do_renew().await {
warn!("Renewal failed on {}: {}", self.interface, e);
if let Some(lease) = self.lease.read().await.as_ref() {
if lease.needs_rebinding() {
*self.state.write().await = Dhcpv4State::Rebinding;
}
}
} else {
*self.state.write().await = Dhcpv4State::Bound;
}
}
Dhcpv4State::Rebinding => {
if let Err(e) = self.do_rebind().await {
warn!("Rebinding failed on {}: {}", self.interface, e);
if let Some(lease) = self.lease.read().await.as_ref() {
if lease.is_expired() {
*self.lease.write().await = None;
*self.state.write().await = Dhcpv4State::Init;
}
}
} else {
*self.state.write().await = Dhcpv4State::Bound;
}
}
Dhcpv4State::InitReboot => {
*self.state.write().await = Dhcpv4State::Init;
}
}
sleep(Duration::from_millis(100)).await;
}
Ok(())
}
async fn do_discover(&self) -> Result<DhcpRawSocket> {
use crate::raw_socket::*;
dhcp_debug!("========================================");
dhcp_debug!("DEBUG: do_discover() called");
dhcp_debug!(" interface: {}", self.interface);
dhcp_debug!(" MAC address: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
self.mac_address[0], self.mac_address[1], self.mac_address[2],
self.mac_address[3], self.mac_address[4], self.mac_address[5]);
dhcp_debug!("DEBUG: Checking rate limit...");
self.security.check_rate_limit(&self.interface).await?;
dhcp_debug!("DEBUG: Rate limit check passed");
let xid = *self.xid.lock().await;
dhcp_debug!("DEBUG: Using transaction ID: 0x{:08x}", xid);
let hostname = self.config.hostname.as_deref();
dhcp_info!("Ensuring link is up before sending DISCOVER");
if let Err(e) = self.bring_up_interface().await {
warn!("Failed to bring up interface {} before DISCOVER: {}", self.interface, e);
}
dhcp_info!("Creating raw socket for DHCP (send and receive)");
let socket = DhcpRawSocket::new(RawSocketType::LinkLayer, Some(&self.interface))
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create raw socket: {}", e)
)))?;
dhcp_info!("Building DHCP DISCOVER packet");
let packet = build_dhcp_discover_link_frame(self.mac_address, xid, hostname);
dhcp_info!("Sending DHCP DISCOVER packet ({} bytes)", packet.len());
socket.send(&packet)
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to send DISCOVER: {}", e)
)))?;
info!("DHCP DISCOVER sent on {}", self.interface);
dhcp_debug!("========================================");
Ok(socket)
}
async fn wait_for_offer(&self, socket: DhcpRawSocket) -> Result<Option<DhcpServer>> {
use crate::raw_socket::*;
dhcp_debug!("========================================");
dhcp_debug!("DEBUG: wait_for_offer() called");
dhcp_debug!(" interface: {}", self.interface);
dhcp_info!("Waiting for DHCP OFFER on existing socket");
let timeout = Duration::from_secs(self.config.timeout);
let mut buf = vec![0u8; 1500]; let my_xid = *self.xid.lock().await;
dhcp_info!("Waiting for DHCP OFFER (timeout: {} seconds)", self.config.timeout);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
match socket.recv(&mut buf, Duration::from_secs(1)) {
Ok(size) => {
dhcp_debug!("DEBUG: Received packet of {} bytes", size);
match parse_dhcp_packet(&buf[..size]) {
Ok((msg_type, xid, dhcp_msg)) => {
dhcp_debug!("DEBUG: Parsed DHCP packet: type={}, xid=0x{:08x}", msg_type, xid);
if xid != my_xid {
dhcp_debug!("DEBUG: XID mismatch (expected 0x{:08x}, got 0x{:08x}), ignoring", my_xid, xid);
continue;
}
if msg_type != 2 {
dhcp_debug!("DEBUG: Not an OFFER (type={}), ignoring", msg_type);
continue;
}
match (extract_server_id(&dhcp_msg), extract_offered_ip(&dhcp_msg)) {
(Ok(server_id), Ok(offered_ip)) => {
dhcp_info!("Received DHCP OFFER from {} offering {}", server_id, offered_ip);
if let Err(e) = self.security.validate_ipv4_address(&offered_ip) {
warn!("Invalid offered IP address: {}", e);
continue;
}
let mut server = DhcpServer::new(IpAddr::V4(server_id));
server.mark_success(start.elapsed());
*self.offered_ip.write().await = Some(offered_ip);
*self.selected_server.write().await = Some(server_id);
let mut servers = self.servers.write().await;
servers.push(server.clone());
dhcp_debug!("========================================");
return Ok(Some(server));
}
(Err(e1), Err(e2)) => {
warn!("Failed to extract server info: server_id={}, ip={}", e1, e2);
}
(Err(_e), _) => {
warn!("Failed to extract server info from offer");
}
(_, Err(_e)) => {
warn!("Failed to extract offered IP from offer");
}
}
}
Err(_e) => {
dhcp_debug!("DEBUG: Failed to parse packet: {}", _e);
}
}
}
Err(e) => {
if e.kind() != std::io::ErrorKind::TimedOut &&
e.kind() != std::io::ErrorKind::WouldBlock {
dhcp_debug!("DEBUG: Recv error: {}", e);
}
}
}
}
dhcp_info!("No DHCP OFFER received within timeout");
dhcp_debug!("========================================");
Ok(None)
}
async fn do_request(&self, server: &DhcpServer) -> Result<DhcpRawSocket> {
use crate::raw_socket::*;
dhcp_debug!("========================================");
dhcp_debug!("DEBUG: do_request() called");
dhcp_debug!(" interface: {}", self.interface);
dhcp_debug!(" server: {}", server.address);
dhcp_debug!("DEBUG: Checking rate limit...");
self.security.check_rate_limit(&self.interface).await?;
dhcp_debug!("DEBUG: Rate limit check passed");
dhcp_debug!("DEBUG: Validating server...");
self.security.validate_server(&server.address)?;
dhcp_debug!("DEBUG: Server validation passed");
let xid = *self.xid.lock().await;
let offered_ip = self.offered_ip.read().await
.ok_or_else(|| DhcpClientError::InvalidMessage("No offered IP stored".to_string()))?;
let server_id = self.selected_server.read().await
.ok_or_else(|| DhcpClientError::InvalidMessage("No server ID stored".to_string()))?;
dhcp_debug!("DEBUG: Requesting IP {} from server {}", offered_ip, server_id);
let hostname = self.config.hostname.as_deref();
dhcp_info!("Creating raw socket for DHCP REQUEST (send and receive)");
let socket = DhcpRawSocket::new(RawSocketType::LinkLayer, Some(&self.interface))
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create raw socket: {}", e)
)))?;
dhcp_info!("Building DHCP REQUEST packet");
let packet = build_dhcp_request_link_frame(
self.mac_address,
xid,
offered_ip,
server_id,
hostname
);
dhcp_info!("Sending DHCP REQUEST packet ({} bytes)", packet.len());
socket.send(&packet)
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to send REQUEST: {}", e)
)))?;
info!("DHCP REQUEST sent to server {} on {}", server_id, self.interface);
dhcp_debug!("========================================");
Ok(socket)
}
async fn wait_for_ack(&self, socket: DhcpRawSocket) -> Result<Option<Dhcpv4Lease>> {
use crate::raw_socket::*;
dhcp_debug!("========================================");
dhcp_debug!("DEBUG: wait_for_ack() called");
dhcp_debug!(" interface: {}", self.interface);
dhcp_info!("Waiting for DHCP ACK on existing socket");
let timeout = Duration::from_secs(self.config.timeout);
let mut buf = vec![0u8; 1500];
let my_xid = *self.xid.lock().await;
dhcp_info!("Waiting for DHCP ACK (timeout: {} seconds)", self.config.timeout);
let start = std::time::Instant::now();
while start.elapsed() < timeout {
match socket.recv(&mut buf, Duration::from_secs(1)) {
Ok(size) => {
dhcp_debug!("DEBUG: Received packet of {} bytes", size);
match parse_dhcp_packet(&buf[..size]) {
Ok((msg_type, xid, dhcp_msg)) => {
dhcp_debug!("DEBUG: Parsed DHCP packet: type={}, xid=0x{:08x}", msg_type, xid);
if xid != my_xid {
dhcp_debug!("DEBUG: XID mismatch, ignoring");
continue;
}
if msg_type == 6 {
warn!("Received DHCP NAK - server rejected our request");
return Ok(None);
}
if msg_type != 5 {
dhcp_debug!("DEBUG: Not an ACK (type={}), ignoring", msg_type);
continue;
}
match extract_lease_info(&dhcp_msg) {
Ok(lease_info) => {
dhcp_info!("Received DHCP ACK from {}", lease_info.server_id);
dhcp_info!(" IP address: {}", lease_info.ip_address);
dhcp_info!(" Subnet mask: {:?}", lease_info.subnet_mask);
dhcp_info!(" Router: {:?}", lease_info.router);
dhcp_info!(" DNS servers: {:?}", lease_info.dns_servers);
dhcp_info!(" Lease time: {} seconds", lease_info.lease_time);
self.security.validate_ipv4_address(&lease_info.ip_address)?;
self.security.validate_lease_time(lease_info.lease_time)?;
let lease = Dhcpv4Lease {
ip_address: lease_info.ip_address,
subnet_mask: lease_info.subnet_mask,
router: lease_info.router,
dns_servers: lease_info.dns_servers,
domain_name: lease_info.domain_name,
ntp_servers: lease_info.ntp_servers,
lease_time: Duration::from_secs(lease_info.lease_time as u64),
renewal_time: Duration::from_secs(
lease_info.renewal_time.unwrap_or(lease_info.lease_time / 2) as u64
),
rebinding_time: Duration::from_secs(
lease_info.rebinding_time.unwrap_or(lease_info.lease_time * 7 / 8) as u64
),
server_id: lease_info.server_id,
acquired_at: SystemTime::now(),
};
dhcp_debug!("========================================");
return Ok(Some(lease));
}
Err(_e) => {
warn!("Failed to extract lease information from ACK");
}
}
}
Err(_e) => {
dhcp_debug!("DEBUG: Failed to parse packet: {}", _e);
}
}
}
Err(e) => {
if e.kind() != std::io::ErrorKind::TimedOut &&
e.kind() != std::io::ErrorKind::WouldBlock {
dhcp_debug!("DEBUG: Recv error: {}", e);
}
}
}
}
dhcp_info!("No DHCP ACK received within timeout");
dhcp_debug!("========================================");
Ok(None)
}
pub async fn renew(&self) -> Result<()> {
info!("Renewing DHCP lease on {}", self.interface);
self.do_renew().await
}
async fn do_renew(&self) -> Result<()> {
use crate::raw_socket::*;
dhcp_info!("Renewing DHCP lease on {}", self.interface);
let lease = self.lease.read().await;
let lease = lease.as_ref().ok_or_else(|| DhcpClientError::NoLease)?;
let xid = *self.xid.lock().await;
let hostname = self.config.hostname.as_deref();
dhcp_debug!("DEBUG: Renewing with server {}, IP {}", lease.server_id, lease.ip_address);
let socket = DhcpRawSocket::new(RawSocketType::LinkLayer, Some(&self.interface))
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create raw socket: {}", e)
)))?;
let packet = build_dhcp_request_link_frame(
self.mac_address,
xid,
lease.ip_address,
lease.server_id,
hostname
);
socket.send(&packet)
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to send RENEW: {}", e)
)))?;
info!("DHCP RENEW sent on {}", self.interface);
let _ = lease; if let Some(new_lease) = self.wait_for_ack(socket).await? {
*self.lease.write().await = Some(new_lease.clone());
info!("DHCP lease renewed on {}", self.interface);
if let Err(e) = self.apply_lease(&new_lease).await {
warn!("Failed to apply renewed lease to {}: {}", self.interface, e);
}
Ok(())
} else {
Err(DhcpClientError::Timeout("No ACK received for renewal".to_string()))
}
}
async fn do_rebind(&self) -> Result<()> {
use crate::raw_socket::*;
dhcp_info!("Rebinding DHCP lease on {}", self.interface);
let lease = self.lease.read().await;
let lease = lease.as_ref().ok_or_else(|| DhcpClientError::NoLease)?;
let xid = *self.xid.lock().await;
let hostname = self.config.hostname.as_deref();
dhcp_debug!("DEBUG: Rebinding IP {} (broadcasting)", lease.ip_address);
let socket = DhcpRawSocket::new(RawSocketType::LinkLayer, Some(&self.interface))
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create raw socket: {}", e)
)))?;
let packet = build_dhcp_request_link_frame(
self.mac_address,
xid,
lease.ip_address,
lease.server_id, hostname
);
socket.send(&packet)
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to send REBIND: {}", e)
)))?;
info!("DHCP REBIND sent on {}", self.interface);
let _ = lease; if let Some(new_lease) = self.wait_for_ack(socket).await? {
*self.lease.write().await = Some(new_lease.clone());
info!("DHCP lease rebound on {}", self.interface);
if let Err(e) = self.apply_lease(&new_lease).await {
warn!("Failed to apply rebound lease to {}: {}", self.interface, e);
}
Ok(())
} else {
Err(DhcpClientError::Timeout("No ACK received for rebinding".to_string()))
}
}
async fn setup_wifi_connection(&self) -> Result<()> {
use crate::wifi::{WifiManager, WifiConfig, WifiSecurity};
let wifi_net_config = match &self.wifi_config {
Some(cfg) => cfg,
None => {
debug!("No WiFi configuration for {}, skipping WiFi setup", self.interface);
return Ok(());
}
};
let wifi_mgr = WifiManager::new(&self.interface)?;
if !wifi_mgr.is_wireless().await {
debug!("Interface {} is not wireless, skipping WiFi setup", self.interface);
return Ok(());
}
info!("Detected wireless interface {}", self.interface);
if wifi_mgr.is_connected().await? {
if let Some(current_ssid) = wifi_mgr.get_current_ssid().await? {
if current_ssid == wifi_net_config.ssid {
info!("✓ Already connected to WiFi network \"{}\"", current_ssid);
return Ok(());
} else {
info!("Connected to different network \"{}\", disconnecting...", current_ssid);
wifi_mgr.disconnect().await?;
}
}
}
let security = match wifi_net_config.security.to_lowercase().as_str() {
"open" => WifiSecurity::Open,
"wep" => {
if let Some(key) = &wifi_net_config.wep_key {
WifiSecurity::Wep { key: key.clone() }
} else {
return Err(DhcpClientError::InvalidConfig(
"WEP security requires wep_key".to_string()
));
}
}
"wpa" | "wpa2" | "psk" => {
if let Some(pass) = &wifi_net_config.passphrase {
WifiSecurity::WpaPsk { passphrase: pass.clone() }
} else {
return Err(DhcpClientError::InvalidConfig(
"WPA/WPA2 security requires passphrase".to_string()
));
}
}
"wpa3" | "sae" => {
if let Some(pass) = &wifi_net_config.passphrase {
WifiSecurity::Wpa3Sae { passphrase: pass.clone() }
} else {
return Err(DhcpClientError::InvalidConfig(
"WPA3 security requires passphrase".to_string()
));
}
}
"enterprise" => {
let identity = wifi_net_config.identity.as_ref()
.ok_or_else(|| DhcpClientError::InvalidConfig(
"Enterprise security requires identity".to_string()
))?.clone();
let password = wifi_net_config.password.as_ref()
.ok_or_else(|| DhcpClientError::InvalidConfig(
"Enterprise security requires password".to_string()
))?.clone();
WifiSecurity::WpaEnterprise {
identity,
password,
ca_cert: wifi_net_config.ca_cert.clone(),
}
}
_ => {
return Err(DhcpClientError::InvalidConfig(
format!("Unknown security type: {}", wifi_net_config.security)
));
}
};
let wifi_config = WifiConfig {
ssid: wifi_net_config.ssid.clone(),
security,
hidden: wifi_net_config.hidden,
timeout: wifi_net_config.timeout,
};
info!("Connecting to WiFi network \"{}\"...", wifi_config.ssid);
wifi_mgr.connect(&wifi_config).await?;
info!("✓ WiFi connection established on {}", self.interface);
Ok(())
}
async fn bring_up_interface(&self) -> Result<()> {
use tokio::process::Command;
info!("Bringing up interface {} before DHCP operations", self.interface);
dhcp_debug!("Executing: ip link set {} up", self.interface);
let output = tokio::time::timeout(
std::time::Duration::from_secs(5),
Command::new("ip")
.args(&["link", "set", &self.interface, "up"])
.output()
).await;
match output {
Ok(Ok(result)) => {
if result.status.success() {
info!("✓ Interface {} brought up successfully", self.interface);
let carrier_path = format!("/sys/class/net/{}/carrier", self.interface);
let max_wait = std::time::Duration::from_secs(10);
let poll_interval = std::time::Duration::from_millis(100);
let start = std::time::Instant::now();
dhcp_debug!("Waiting for carrier on {} (max {} seconds)", self.interface, max_wait.as_secs());
loop {
if let Ok(carrier) = tokio::fs::read_to_string(&carrier_path).await {
if carrier.trim() == "1" {
info!("✓ Carrier detected on {} after {:?}", self.interface, start.elapsed());
break;
}
}
if start.elapsed() >= max_wait {
warn!("Timeout waiting for carrier on {} (continuing anyway)", self.interface);
break;
}
tokio::time::sleep(poll_interval).await;
}
Ok(())
} else {
let stderr = String::from_utf8_lossy(&result.stderr);
warn!("Failed to bring up interface {}: {}", self.interface, stderr);
Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to bring up interface: {}", stderr)
)))
}
}
Ok(Err(e)) => {
warn!("Failed to execute ip link set up: {}", e);
Err(DhcpClientError::Network(e))
}
Err(_) => {
warn!("Timeout bringing up interface {} (5 seconds)", self.interface);
Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"ip link set up command timed out"
)))
}
}
}
async fn apply_lease(&self, lease: &Dhcpv4Lease) -> Result<()> {
use tokio::process::Command;
dhcp_info!("Applying lease configuration to interface {}", self.interface);
dhcp_info!(" IP: {}", lease.ip_address);
dhcp_info!(" Netmask: {:?}", lease.subnet_mask);
dhcp_info!(" Gateway: {:?}", lease.router);
dhcp_info!(" DNS: {:?}", lease.dns_servers);
let prefix_len = if let Some(mask) = lease.subnet_mask {
mask.octets().iter().map(|b| b.count_ones()).sum::<u32>()
} else {
24 };
dhcp_info!("Setting IP address {}/{} on {}", lease.ip_address, prefix_len, self.interface);
dhcp_debug!("Executing: ip addr add {}/{} dev {}", lease.ip_address, prefix_len, self.interface);
let addr_future = Command::new("ip")
.args(&[
"addr",
"add",
&format!("{}/{}", lease.ip_address, prefix_len),
"dev",
&self.interface,
])
.output();
let addr_output = match tokio::time::timeout(
std::time::Duration::from_secs(5),
addr_future
).await {
Ok(Ok(output)) => {
dhcp_debug!("IP address add command completed, status: {}", output.status);
output
}
Ok(Err(e)) => {
error!("Failed to execute ip addr add: {}", e);
return Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to execute ip addr add: {}", e)
)));
}
Err(_) => {
error!("Timeout waiting for ip addr add command (5 seconds)");
return Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"ip addr add command timed out after 5 seconds"
)));
}
};
if !addr_output.status.success() {
let stderr = String::from_utf8_lossy(&addr_output.stderr);
dhcp_debug!("IP add failed: {}", stderr);
if stderr.contains("File exists") || stderr.contains("RTNETLINK answers: File exists") {
dhcp_info!("Address already exists, removing old IPv4 addresses and retrying");
let list_output = Command::new("ip")
.args(&["-4", "addr", "show", "dev", &self.interface])
.output()
.await
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to list IP addresses: {}", e)
)))?;
if list_output.status.success() {
let output_str = String::from_utf8_lossy(&list_output.stdout);
dhcp_debug!("Current addresses:\n{}", output_str);
for line in output_str.lines() {
if line.trim().starts_with("inet ") {
if let Some(addr_part) = line.trim().split_whitespace().nth(1) {
dhcp_info!("Removing old address: {}", addr_part);
let _ = Command::new("ip")
.args(&["addr", "del", addr_part, "dev", &self.interface])
.output()
.await;
}
}
}
}
dhcp_info!("Retrying: ip addr add {}/{} dev {}", lease.ip_address, prefix_len, self.interface);
let retry_output = Command::new("ip")
.args(&[
"addr",
"add",
&format!("{}/{}", lease.ip_address, prefix_len),
"dev",
&self.interface,
])
.output()
.await
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to execute ip addr add (retry): {}", e)
)))?;
if !retry_output.status.success() {
let retry_stderr = String::from_utf8_lossy(&retry_output.stderr);
error!("Failed to set IP address after retry. stderr: {}", retry_stderr);
return Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to set IP address: {}", retry_stderr)
)));
}
} else {
error!("Failed to set IP address. stderr: {}", stderr);
return Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to set IP address: {}", stderr)
)));
}
}
dhcp_info!("✓ IP address {}/{} set successfully on {}", lease.ip_address, prefix_len, self.interface);
dhcp_info!("Bringing interface {} up", self.interface);
dhcp_debug!("Executing: ip link set {} up", self.interface);
let up_future = Command::new("ip")
.args(&["link", "set", &self.interface, "up"])
.output();
let up_output = match tokio::time::timeout(
std::time::Duration::from_secs(5),
up_future
).await {
Ok(Ok(output)) => {
dhcp_debug!("Interface up command completed, status: {}", output.status);
output
}
Ok(Err(e)) => {
error!("Failed to execute ip link set up: {}", e);
return Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to bring interface up: {}", e)
)));
}
Err(_) => {
error!("Timeout waiting for ip link set up command (5 seconds)");
return Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"ip link set up command timed out after 5 seconds"
)));
}
};
if !up_output.status.success() {
let stderr = String::from_utf8_lossy(&up_output.stderr);
error!("Failed to bring interface up: {}", stderr);
return Err(DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to bring interface up: {}", stderr)
)));
}
dhcp_info!("✓ Interface {} is up", self.interface);
if let Some(gateway) = lease.router {
dhcp_info!("Setting default gateway to {}", gateway);
let _ = Command::new("ip")
.args(&["route", "del", "default"])
.output()
.await;
let route_output = Command::new("ip")
.args(&[
"route",
"add",
"default",
"via",
&gateway.to_string(),
"dev",
&self.interface,
])
.output()
.await
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to set default route: {}", e)
)))?;
if !route_output.status.success() {
let stderr = String::from_utf8_lossy(&route_output.stderr);
warn!("Failed to set default route (non-fatal): {}", stderr);
}
}
if !lease.dns_servers.is_empty() {
dhcp_info!("Updating DNS servers in /etc/resolv.conf");
if let Err(e) = self.update_resolv_conf(lease).await {
warn!("Failed to update /etc/resolv.conf (non-fatal): {}", e);
}
}
info!("✓ Successfully applied lease configuration to {}", self.interface);
Ok(())
}
async fn update_resolv_conf(&self, lease: &Dhcpv4Lease) -> Result<()> {
use tokio::fs;
use crate::security::{sanitize_domain_name, sanitize_resolv_conf_value};
let mut content = String::new();
content.push_str(&format!("# Generated by crdhcpc for {}\n", self.interface));
if let Some(domain) = &lease.domain_name {
if let Some(safe_domain) = sanitize_domain_name(domain) {
content.push_str(&format!("domain {}\n", safe_domain));
content.push_str(&format!("search {}\n", safe_domain));
} else {
warn!("Rejecting invalid domain name from DHCP: {:?}",
sanitize_resolv_conf_value(domain).unwrap_or_else(|| "[unprintable]".to_string()));
}
}
for dns in &lease.dns_servers {
content.push_str(&format!("nameserver {}\n", dns));
}
if lease.dns_servers.is_empty() {
warn!("No valid DNS servers to write to resolv.conf");
return Ok(());
}
let temp_path = "/etc/resolv.conf.crdhcpc.tmp";
dhcp_debug!("Writing DNS configuration to /etc/resolv.conf");
fs::write(temp_path, content.as_bytes())
.await
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to write temp resolv.conf: {}", e)
)))?;
fs::rename(temp_path, "/etc/resolv.conf")
.await
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to rename resolv.conf: {}", e)
)))?;
info!("✓ Updated /etc/resolv.conf with {} DNS servers", lease.dns_servers.len());
Ok(())
}
pub async fn release(&self) -> Result<()> {
use crate::raw_socket::*;
info!("Releasing DHCP lease on {}", self.interface);
let lease = self.lease.read().await;
if let Some(lease) = lease.as_ref() {
dhcp_info!("Sending DHCP RELEASE for IP {} to server {}",
lease.ip_address, lease.server_id);
let xid = *self.xid.lock().await;
let socket = DhcpRawSocket::new(RawSocketType::LinkLayer, Some(&self.interface))
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create raw socket: {}", e)
)))?;
let packet = build_dhcp_release_link_frame(
self.mac_address,
xid,
lease.ip_address,
lease.server_id
);
if let Err(e) = socket.send(&packet) {
warn!("Failed to send DHCP RELEASE: {}", e);
} else {
info!("DHCP RELEASE sent for {} on {}", lease.ip_address, self.interface);
}
}
drop(lease);
*self.lease.write().await = None;
*self.state.write().await = Dhcpv4State::Init;
*self.offered_ip.write().await = None;
*self.selected_server.write().await = None;
Ok(())
}
pub async fn decline(&self, declined_ip: Ipv4Addr, server_id: Ipv4Addr) -> Result<()> {
use crate::raw_socket::*;
info!("Declining DHCP offer {} from server {} on {}",
declined_ip, server_id, self.interface);
let xid = *self.xid.lock().await;
let socket = DhcpRawSocket::new(RawSocketType::LinkLayer, Some(&self.interface))
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to create raw socket: {}", e)
)))?;
let packet = build_dhcp_decline_link_frame(
self.mac_address,
xid,
declined_ip,
server_id
);
socket.send(&packet)
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to send DHCP DECLINE: {}", e)
)))?;
info!("DHCP DECLINE sent for {} on {}", declined_ip, self.interface);
*self.offered_ip.write().await = None;
*self.selected_server.write().await = None;
*self.state.write().await = Dhcpv4State::Init;
dhcp_info!("Waiting 10 seconds before restarting DHCP process (RFC 2131)");
sleep(Duration::from_secs(10)).await;
Ok(())
}
pub async fn arp_probe(&self, ip_address: Ipv4Addr) -> Result<bool> {
use crate::raw_socket::arp_probe_address;
dhcp_info!("Performing ARP probe for {} on {}", ip_address, self.interface);
let probe_count = 3;
let probe_wait_ms = 500; let timeout_ms = 1000;
let interface = self.interface.clone();
let mac = self.mac_address;
let result = tokio::task::spawn_blocking(move || {
arp_probe_address(&interface, mac, ip_address, probe_count, probe_wait_ms, timeout_ms)
}).await
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("ARP probe task failed: {}", e)
)))?
.map_err(|e| DhcpClientError::Network(std::io::Error::new(
std::io::ErrorKind::Other,
format!("ARP probe failed: {}", e)
)))?;
if result {
warn!("ARP conflict detected for {} on {}", ip_address, self.interface);
} else {
dhcp_info!("No ARP conflict for {} on {}", ip_address, self.interface);
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dhcpv4_state_display() {
assert_eq!(Dhcpv4State::Init.to_string(), "INIT");
assert_eq!(Dhcpv4State::Bound.to_string(), "BOUND");
}
#[test]
fn test_lease_times() {
let lease = Dhcpv4Lease {
ip_address: Ipv4Addr::new(192, 168, 1, 100),
subnet_mask: Some(Ipv4Addr::new(255, 255, 255, 0)),
router: Some(Ipv4Addr::new(192, 168, 1, 1)),
dns_servers: vec![],
domain_name: None,
ntp_servers: vec![],
lease_time: Duration::from_secs(3600),
renewal_time: Duration::from_secs(1800),
rebinding_time: Duration::from_secs(3150),
server_id: Ipv4Addr::new(192, 168, 1, 1),
acquired_at: SystemTime::now() - Duration::from_secs(3700),
};
assert!(lease.is_expired());
assert!(lease.needs_renewal());
assert!(lease.needs_rebinding());
}
}