use crdhcpc::*;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::signal;
use tokio::sync::RwLock;
use tracing::{info, error, warn};
use tracing_subscriber::EnvFilter;
#[derive(Parser, Debug)]
#[command(author, version, about = "Standalone DHCP Client for Linux", long_about = None)]
struct Args {
#[arg(short, long, default_value = "/etc/dhcp-client.toml")]
config: PathBuf,
#[arg(short, long)]
verbose: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand, Debug)]
enum Commands {
Daemon {
#[arg(short, long)]
foreground: bool,
},
Start {
interface: String,
#[arg(short, long)]
foreground: bool,
},
Stop {
interface: String,
},
Renew {
interface: String,
},
Release {
interface: String,
},
Status {
interface: Option<String>,
},
Check {
#[arg(long)]
generate_example: bool,
},
}
#[tokio::main]
async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let log_level = if args.verbose { "debug" } else { "info" };
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new(log_level))
.init();
#[cfg(feature = "dhcp-debug")]
if args.verbose {
info!("========================================");
info!("VERBOSE MODE ENABLED - Debug logging active");
info!("Log level: {}", log_level);
info!("========================================");
}
match args.command {
Commands::Check { generate_example } => {
check_config(&args.config, generate_example)?;
}
_ => {
let config = load_config(&args.config)?;
match args.command {
Commands::Daemon { foreground } => {
run_daemon(config, foreground).await?;
}
Commands::Start { interface, foreground } => {
start_interface(&interface, config, foreground).await?;
}
Commands::Stop { interface } => {
stop_interface(&interface).await?;
}
Commands::Renew { interface } => {
renew_interface(&interface).await?;
}
Commands::Release { interface } => {
release_interface(&interface).await?;
}
Commands::Status { interface } => {
show_status(interface).await?;
}
Commands::Check { .. } => unreachable!(), }
}
}
Ok(())
}
fn load_config(path: &PathBuf) -> std::result::Result<DhcpClientConfig, Box<dyn std::error::Error>> {
use std::fs;
info!("Loading configuration from: {}", path.display());
if !path.exists() {
return Err(format!("Configuration file not found: {}", path.display()).into());
}
let config_str = fs::read_to_string(path)?;
let config: DhcpClientConfig = toml::from_str(&config_str)?;
info!("Configuration loaded successfully");
Ok(config)
}
fn check_config(path: &PathBuf, generate_example: bool) -> std::result::Result<(), Box<dyn std::error::Error>> {
use std::fs;
use std::os::unix::fs::PermissionsExt;
if generate_example {
println!("{}", generate_example_config());
return Ok(());
}
println!("🔍 Checking configuration file: {}", path.display());
println!();
if !path.exists() {
eprintln!("❌ ERROR: Configuration file not found: {}", path.display());
eprintln!(" Generate an example with: crdhcpc check --generate-example > /etc/dhcp-client.toml");
std::process::exit(1);
}
let metadata = fs::metadata(path)?;
let permissions = metadata.permissions();
let mode = permissions.mode();
println!("📄 File information:");
println!(" Path: {}", path.display());
println!(" Permissions: {:o}", mode & 0o777);
if mode & 0o004 != 0 {
println!(" ⚠ WARNING: Config file is world-readable (others can read sensitive info)");
println!(" Recommended: chmod 640 {}", path.display());
} else {
println!(" ✓ Permissions are appropriate");
}
println!();
let config = match load_config(path) {
Ok(c) => c,
Err(e) => {
eprintln!("❌ ERROR: Failed to parse configuration:");
eprintln!(" {}", e);
eprintln!();
eprintln!(" Check the TOML syntax and ensure all required fields are present.");
eprintln!(" Generate an example with: crdhcpc check --generate-example");
std::process::exit(1);
}
};
println!("✅ Configuration file is valid TOML");
println!();
println!("📊 Configuration summary:");
println!(" Global enabled: {}", if config.enabled { "✓ yes" } else { "✗ no" });
println!(" Interfaces: {}", if config.interfaces.is_empty() {
"⚠ NONE (daemon will not manage any interfaces)".to_string()
} else {
format!("{:?}", config.interfaces)
});
println!();
println!("🌐 Protocol settings:");
println!(" DHCPv4: {}", if config.dhcpv4.enabled { "✓ enabled" } else { "✗ disabled" });
if config.dhcpv4.enabled {
println!(" Timeout: {}s", config.dhcpv4.timeout);
}
println!(" DHCPv6: {}", if config.dhcpv6.enabled { "✓ enabled" } else { "✗ disabled" });
if config.dhcpv6.enabled {
println!(" Timeout: {}s", config.dhcpv6.timeout);
}
if !config.dhcpv4.enabled && !config.dhcpv6.enabled {
println!(" ⚠ WARNING: Both DHCPv4 and DHCPv6 are disabled!");
}
println!();
println!("🔧 Additional features:");
println!(" PXE Boot: {}", if config.pxe.enabled { "✓ enabled" } else { "✗ disabled" });
println!(" Dynamic DNS: {}", if config.ddns.enabled { "✓ enabled" } else { "✗ disabled" });
println!(" Failover: {}", if config.failover.enabled { "✓ enabled" } else { "✗ disabled" });
println!();
println!("🔒 Security settings:");
let mut warnings = 0;
if config.security.allowed_servers.is_empty() {
println!(" ⚠ WARNING: No allowed DHCP servers configured!");
println!(" This accepts DHCP responses from ANY server (security risk)");
println!(" Recommended: Set security.allowed_servers = [\"192.168.1.1\"]");
warnings += 1;
} else {
println!(" ✓ Allowed servers: {:?}", config.security.allowed_servers);
}
println!(" Message validation: {}", if config.security.validate_messages { "✓ enabled" } else { "⚠ disabled" });
if !config.security.validate_messages {
println!(" WARNING: Message validation is disabled (security risk)");
warnings += 1;
}
println!(" Option validation: {}", if config.security.validate_options { "✓ enabled" } else { "⚠ disabled" });
if !config.security.validate_options {
println!(" WARNING: Option validation is disabled (security risk)");
warnings += 1;
}
println!(" Max request rate: {} req/s", config.security.max_request_rate);
if config.security.max_request_rate == 0 {
println!(" ⚠ WARNING: Rate limiting is disabled");
warnings += 1;
}
println!();
println!("🔌 Interface validation:");
if config.interfaces.is_empty() {
println!(" ⚠ No interfaces configured");
} else {
for iface in &config.interfaces {
let iface_path = format!("/sys/class/net/{}", iface);
if std::path::Path::new(&iface_path).exists() {
println!(" ✓ {} (exists)", iface);
} else {
println!(" ⚠ {} (not found - may not exist yet)", iface);
}
}
}
println!();
if warnings > 0 {
println!("⚠️ Configuration is valid but has {} security warning(s)", warnings);
println!(" Review the warnings above and update your configuration for better security.");
} else {
println!("✅ Configuration is valid and secure!");
}
println!();
println!("💡 Next steps:");
println!(" • Start daemon: sudo systemctl start dhcp-client-standalone.service");
println!(" • Check status: sudo crdhcpc status");
println!(" • View logs: sudo journalctl -u dhcp-client-standalone.service -f");
Ok(())
}
fn generate_example_config() -> String {
r#"# DHCP Client Configuration
# This file configures the crdhcpc standalone DHCP client daemon
# Location: /etc/dhcp-client.toml
# Enable the DHCP client
enabled = true
# Mock mode (for testing only - simulates DHCP without network operations)
mock_mode = false
# List of network interfaces to manage
# The daemon will start DHCP on all listed interfaces
interfaces = ["eth0"]
# DHCPv4 configuration
[dhcpv4]
enabled = true
send_hostname = true
timeout = 10 # seconds
retry_count = 3
request_options = [1, 3, 6, 15, 28, 42] # subnet mask, router, DNS, domain, broadcast, NTP
# hostname = "my-hostname" # Optional: override system hostname
# vendor_class = "crrouter-web" # Optional: vendor class identifier
# DHCPv6 configuration
[dhcpv6]
enabled = true
prefix_delegation = true
rapid_commit = false
timeout = 10 # seconds
retry_count = 3
request_options = [23, 24] # DNS servers, domain search list
# Security settings
[security]
# Validate DHCP message format and checksums
validate_messages = true
# Validate DHCP options for correctness
validate_options = true
# Validate DHCP server (check against allowed_servers list)
validate_server = false
# List of allowed DHCP server IP addresses
# Empty list = accept responses from any server (if validate_server = true, this must not be empty)
# Example: allowed_servers = ["192.168.1.1", "10.0.0.1"]
allowed_servers = []
# Maximum DHCP requests per second (rate limiting)
# Set to 0 to disable rate limiting (not recommended)
max_request_rate = 10
# Enable DHCP snooping (additional security layer)
enable_dhcp_snooping = false
# Minimum acceptable lease time (seconds)
min_lease_time = 300 # 5 minutes
# Maximum acceptable lease time (seconds)
max_lease_time = 86400 # 24 hours
# PXE (Preboot Execution Environment) support
[pxe]
enabled = false
vendor_class = "PXEClient"
architecture = 7 # 0 = x86 BIOS, 7 = x64 UEFI, 9 = x64 UEFI HTTP
# Dynamic DNS (DDNS) updates
[ddns]
enabled = false
update_forward = true
update_reverse = true
ttl = 3600 # seconds
# server = "ns1.example.com" # Optional: DNS server for updates
# tsig_key_name = "dhcp-update" # Optional: TSIG key name for authentication
# tsig_key = "base64-encoded-secret" # Optional: TSIG key for authentication
# TFTP client for PXE boot files
[tftp]
enabled = false
timeout = 5 # seconds
max_retries = 3
block_size = 1468 # Maximum for Ethernet without fragmentation
# Failover and high availability
[failover]
enabled = false
server_timeout = 5 # seconds
health_check_interval = 30 # seconds
allowed_servers = [] # List of allowed DHCP servers for failover
prefer_previous_server = true # Prefer previously used server if healthy
"#.to_string()
}
async fn run_daemon(
config: DhcpClientConfig,
foreground: bool,
) -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("Starting DHCP client daemon");
if !foreground {
warn!("Daemonization not yet implemented, running in foreground");
}
let manager = Arc::new(RwLock::new(DhcpClientManager::new(config.clone())));
for interface in &config.interfaces {
info!("Starting DHCP client on interface: {}", interface);
match manager.read().await.start_interface(interface).await {
Ok(_) => info!("✓ Started DHCP client on {}", interface),
Err(e) => error!("✗ Failed to start DHCP client on {}: {}", interface, e),
}
}
let control_handler = DhcpControlHandler::new(manager.clone());
let socket_path = "/var/run/crdhcpc.sock";
let server_config = ServerConfig {
socket_path: socket_path.to_string(),
socket_mode: 0o666, max_connections: 10,
allowed_uids: None,
require_root: false,
};
info!("Starting control interface on {}", socket_path);
let server = Arc::new(UnixServer::new(server_config, control_handler));
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
if let Err(e) = server_clone.run().await {
error!("Control interface error: {}", e);
}
});
info!("DHCP client daemon running. Control interface: {}", socket_path);
info!("Press Ctrl+C to stop, or send SIGTERM to gracefully shutdown");
#[cfg(unix)]
info!("Send SIGHUP to reload config, SIGUSR1 to log status");
#[cfg(unix)]
{
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate())
.expect("Failed to setup SIGTERM handler");
let mut sighup = signal(SignalKind::hangup())
.expect("Failed to setup SIGHUP handler");
let mut sigusr1 = signal(SignalKind::user_defined1())
.expect("Failed to setup SIGUSR1 handler");
loop {
tokio::select! {
_ = signal::ctrl_c() => {
info!("Received SIGINT (Ctrl+C), stopping DHCP client...");
break;
}
_ = sigterm.recv() => {
info!("Received SIGTERM, stopping DHCP client...");
break;
}
_ = sighup.recv() => {
info!("Received SIGHUP - reloading configuration");
warn!("Configuration reload not yet implemented");
}
_ = sigusr1.recv() => {
info!("Received SIGUSR1 - logging status");
let status = manager.read().await.get_status().await;
info!("=== DHCP Client Status ===");
info!("Running: {}", status.running);
info!("DHCPv4 clients: {}", status.v4_clients.len());
for client in &status.v4_clients {
info!(" - {} (state: {})", client.interface, client.state);
}
info!("DHCPv6 clients: {}", status.v6_clients.len());
for client in &status.v6_clients {
info!(" - {} (state: {})", client.interface, client.state);
}
info!("==========================");
}
}
}
}
#[cfg(not(unix))]
{
match signal::ctrl_c().await {
Ok(()) => {
info!("Received SIGINT (Ctrl+C), stopping DHCP client...");
}
Err(err) => {
error!("Error waiting for shutdown signal: {}", err);
}
}
}
server_task.abort();
for interface in &config.interfaces {
info!("Stopping DHCP client on interface: {}", interface);
match manager.read().await.stop_interface(interface).await {
Ok(_) => info!("✓ Stopped DHCP client on {}", interface),
Err(e) => error!("✗ Failed to stop DHCP client on {}: {}", interface, e),
}
}
if std::path::Path::new(socket_path).exists() {
if let Err(e) = std::fs::remove_file(socket_path) {
warn!("Failed to remove socket file: {}", e);
}
}
info!("DHCP client daemon stopped");
Ok(())
}
async fn start_interface(
interface: &str,
config: DhcpClientConfig,
foreground: bool,
) -> std::result::Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "dhcp-debug")]
{
info!("========================================");
info!("DEBUG: start_interface() called");
info!(" interface: {}", interface);
info!(" foreground: {}", foreground);
info!("========================================");
}
let manager = DhcpClientManager::new(config);
#[cfg(feature = "dhcp-debug")]
info!("DEBUG: Calling manager.start_interface({})", interface);
manager.start_interface(interface).await?;
info!("✓ DHCP client started on {}", interface);
if foreground {
info!("Running in foreground with status updates, press Ctrl+C to stop");
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(5));
loop {
tokio::select! {
_ = signal::ctrl_c() => {
info!("Received shutdown signal");
break;
}
_ = interval.tick() => {
let status = manager.get_status().await;
println!("\nStatus: running={}", status.running);
for client in &status.v4_clients {
if client.interface == interface {
println!(" DHCPv4 on {}: state={}", client.interface, client.state);
if let Some(lease) = &client.lease {
println!(" IP: {}", lease.ip_address);
println!(" Subnet: {}", lease.subnet_mask);
if let Some(router) = lease.router {
println!(" Gateway: {}", router);
}
println!(" DNS: {:?}", lease.dns_servers);
}
}
}
for client in &status.v6_clients {
if client.interface == interface {
println!(" DHCPv6 on {}: state={}", client.interface, client.state);
}
}
}
}
}
} else {
info!("Running in background, press Ctrl+C to stop");
match signal::ctrl_c().await {
Ok(()) => {
info!("Received shutdown signal");
}
Err(err) => {
error!("Error waiting for shutdown signal: {}", err);
}
}
}
manager.stop_interface(interface).await?;
info!("✓ DHCP client stopped on {}", interface);
Ok(())
}
async fn stop_interface(interface: &str) -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("Stopping DHCP client on interface: {}", interface);
let client = DaemonClient::real("/var/run/crdhcpc.sock");
match client.connect().await {
Ok(_) => {
info!("Connected to crdhcpc daemon");
let params = serde_json::json!({
"interface": interface
});
match client.call("dhcpc.stop", Some(params)).await {
Ok(result) => {
info!("✓ Stopped DHCP client on {}", interface);
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
Err(e) => {
error!("Failed to stop DHCP client: {}", e);
Err(format!("Failed to stop DHCP client: {}", e).into())
}
}
}
Err(e) => {
error!("Failed to connect to daemon: {}", e);
error!("Make sure crdhcpc daemon is running: crdhcpc daemon");
Err(format!("Failed to connect to daemon: {}", e).into())
}
}
}
async fn renew_interface(interface: &str) -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("Renewing DHCP lease on interface: {}", interface);
let client = DaemonClient::real("/var/run/crdhcpc.sock");
match client.connect().await {
Ok(_) => {
info!("Connected to crdhcpc daemon");
let params = serde_json::json!({
"interface": interface
});
match client.call("dhcpc.renew", Some(params)).await {
Ok(result) => {
info!("✓ Renewed DHCP lease on {}", interface);
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
Err(e) => {
error!("Failed to renew DHCP lease: {}", e);
Err(format!("Failed to renew DHCP lease: {}", e).into())
}
}
}
Err(e) => {
error!("Failed to connect to daemon: {}", e);
error!("Make sure crdhcpc daemon is running: crdhcpc daemon");
Err(format!("Failed to connect to daemon: {}", e).into())
}
}
}
async fn release_interface(interface: &str) -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("Releasing DHCP lease on interface: {}", interface);
let client = DaemonClient::real("/var/run/crdhcpc.sock");
match client.connect().await {
Ok(_) => {
info!("Connected to crdhcpc daemon");
let params = serde_json::json!({
"interface": interface
});
match client.call("dhcpc.release", Some(params)).await {
Ok(result) => {
info!("✓ Released DHCP lease on {}", interface);
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
Err(e) => {
error!("Failed to release DHCP lease: {}", e);
Err(format!("Failed to release DHCP lease: {}", e).into())
}
}
}
Err(e) => {
error!("Failed to connect to daemon: {}", e);
error!("Make sure crdhcpc daemon is running: crdhcpc daemon");
Err(format!("Failed to connect to daemon: {}", e).into())
}
}
}
async fn show_status(interface: Option<String>) -> std::result::Result<(), Box<dyn std::error::Error>> {
info!("Showing DHCP client status");
let client = DaemonClient::real("/var/run/crdhcpc.sock");
match client.connect().await {
Ok(_) => {
info!("Connected to crdhcpc daemon");
let params = if let Some(iface) = &interface {
Some(serde_json::json!({
"interface": iface
}))
} else {
None
};
match client.call("dhcpc.status", params).await {
Ok(result) => {
if let Some(iface) = interface {
println!("\nStatus for interface: {}", iface);
} else {
println!("\nStatus for all interfaces:");
}
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}
Err(e) => {
error!("Failed to get status: {}", e);
Err(format!("Failed to get status: {}", e).into())
}
}
}
Err(e) => {
error!("Failed to connect to daemon: {}", e);
error!("Make sure crdhcpc daemon is running: crdhcpc daemon");
Err(format!("Failed to connect to daemon: {}", e).into())
}
}
}