#![allow(unused_imports, unused_variables, unused_mut)]
pub mod backend;
pub mod datapath;
pub mod dhcp;
pub use splicetcp::direct_rx;
pub mod dns;
pub mod error;
pub use arcbox_packet::ethernet;
#[cfg(target_os = "linux")]
pub mod linux;
pub mod mdns;
pub mod mdns_protocol;
pub mod nat;
pub mod nat_engine;
pub mod port_forward;
pub mod timer_wheel;
#[cfg(target_os = "macos")]
pub mod darwin;
pub use error::{NetError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum NetworkMode {
#[default]
Nat,
Bridge,
HostOnly,
None,
}
#[derive(Debug, Clone)]
pub struct NetConfig {
pub mode: NetworkMode,
pub mac: Option<[u8; 6]>,
pub mtu: u16,
pub bridge: Option<String>,
pub multiqueue: bool,
pub num_queues: u32,
}
impl Default for NetConfig {
fn default() -> Self {
Self {
mode: NetworkMode::Nat,
mac: None,
mtu: 1500,
bridge: None,
multiqueue: false,
num_queues: 1,
}
}
}
use std::net::IpAddr;
use std::sync::RwLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetworkState {
Stopped,
Starting,
Running,
Stopping,
}
pub struct NetworkManager {
config: NetConfig,
state: RwLock<NetworkState>,
ip_allocator: RwLock<Option<nat::IpAllocator>>,
dns_forwarder: RwLock<dns::DnsForwarder>,
}
impl NetworkManager {
#[must_use]
pub fn new(config: NetConfig) -> Self {
Self {
config,
state: RwLock::new(NetworkState::Stopped),
ip_allocator: RwLock::new(None),
dns_forwarder: RwLock::new(dns::DnsForwarder::new(dns::DnsConfig::default())),
}
}
pub fn set_dns_domain(&self, domain: &str) {
if let Ok(mut forwarder) = self.dns_forwarder.write() {
if let Some(ref old_domain) = forwarder.config().local_domain {
let suffix = format!(".{old_domain}");
let table = forwarder.local_hosts_table();
if let Ok(mut hosts) = table.write() {
hosts.retain(|k, _| !k.ends_with(&suffix) && k != old_domain);
}
}
let mut cfg = forwarder.config().clone();
cfg.local_domain = Some(domain.to_string());
let shared_table = forwarder.local_hosts_table();
*forwarder = dns::DnsForwarder::with_shared_hosts(cfg, shared_table);
}
}
#[must_use]
pub fn config(&self) -> &NetConfig {
&self.config
}
#[must_use]
pub fn state(&self) -> NetworkState {
*self.state.read().unwrap_or_else(|p| p.into_inner())
}
pub fn start(&self) -> Result<()> {
{
let mut state = self
.state
.write()
.map_err(|_| NetError::config("lock poisoned".to_string()))?;
if *state != NetworkState::Stopped {
return Ok(());
}
*state = NetworkState::Starting;
}
let result = self.do_start();
{
let mut state = self
.state
.write()
.map_err(|_| NetError::config("lock poisoned".to_string()))?;
*state = if result.is_ok() {
NetworkState::Running
} else {
NetworkState::Stopped
};
}
result
}
fn do_start(&self) -> Result<()> {
match self.config.mode {
NetworkMode::Nat => {
let allocator = nat::IpAllocator::new(
std::net::Ipv4Addr::new(192, 168, 64, 2),
std::net::Ipv4Addr::new(192, 168, 64, 254),
);
let mut ip_alloc = self
.ip_allocator
.write()
.map_err(|_| NetError::config("lock poisoned".to_string()))?;
*ip_alloc = Some(allocator);
tracing::info!("Network manager started in NAT mode");
}
NetworkMode::Bridge => {
if self.config.bridge.is_none() {
return Err(NetError::config(
"bridge mode requires bridge interface name".to_string(),
));
}
tracing::info!(
"Network manager started in bridge mode ({})",
self.config.bridge.as_deref().unwrap_or("unknown")
);
}
NetworkMode::HostOnly => {
let allocator = nat::IpAllocator::new(
std::net::Ipv4Addr::new(10, 0, 0, 2),
std::net::Ipv4Addr::new(10, 0, 0, 254),
);
let mut ip_alloc = self
.ip_allocator
.write()
.map_err(|_| NetError::config("lock poisoned".to_string()))?;
*ip_alloc = Some(allocator);
tracing::info!("Network manager started in host-only mode");
}
NetworkMode::None => {
tracing::info!("Network manager started with no networking");
}
}
Ok(())
}
pub fn stop(&self) -> Result<()> {
{
let mut state = self
.state
.write()
.map_err(|_| NetError::config("lock poisoned".to_string()))?;
if *state != NetworkState::Running {
return Ok(());
}
*state = NetworkState::Stopping;
}
let result = self.do_stop();
{
let mut state = self
.state
.write()
.map_err(|_| NetError::config("lock poisoned".to_string()))?;
*state = NetworkState::Stopped;
}
result
}
#[allow(clippy::unnecessary_wraps)] fn do_stop(&self) -> Result<()> {
if let Ok(mut ip_alloc) = self.ip_allocator.write() {
*ip_alloc = None;
}
tracing::info!("Network manager stopped");
Ok(())
}
pub fn allocate_ip(&self) -> Option<std::net::Ipv4Addr> {
let mut ip_alloc = self.ip_allocator.write().ok()?;
ip_alloc.as_mut()?.allocate()
}
pub fn release_ip(&self, ip: std::net::Ipv4Addr) {
if let Ok(mut ip_alloc) = self.ip_allocator.write() {
if let Some(allocator) = ip_alloc.as_mut() {
allocator.release(ip);
}
}
}
#[must_use]
pub fn available_ips(&self) -> usize {
self.ip_allocator
.read()
.ok()
.and_then(|guard| guard.as_ref().map(nat::IpAllocator::available_count))
.unwrap_or(0)
}
pub fn local_hosts_table(&self) -> std::sync::Arc<arcbox_dns::LocalHostsTable> {
self.dns_forwarder
.read()
.expect("dns forwarder lock")
.local_hosts_table()
}
pub fn register_dns(&self, hostname: &str, ip: IpAddr) {
if let Ok(forwarder) = self.dns_forwarder.read() {
forwarder.add_local_host(hostname, ip);
}
}
pub fn deregister_dns(&self, hostname: &str) {
if let Ok(forwarder) = self.dns_forwarder.read() {
forwarder.remove_local_host(hostname);
}
}
pub fn try_resolve_dns_or_nxdomain(&self, query: &[u8]) -> Option<Vec<u8>> {
let forwarder = self.dns_forwarder.read().ok()?;
forwarder.try_resolve_locally_or_nxdomain(query)
}
pub fn handle_dns_query(&self, query: &[u8]) -> Result<Vec<u8>> {
let mut forwarder = self
.dns_forwarder
.write()
.map_err(|_| NetError::config("dns forwarder lock poisoned".to_string()))?;
forwarder.handle_query(query)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_network_manager_lifecycle() {
let manager = NetworkManager::new(NetConfig::default());
assert_eq!(manager.state(), NetworkState::Stopped);
manager.start().unwrap();
assert_eq!(manager.state(), NetworkState::Running);
let ip = manager.allocate_ip();
assert!(ip.is_some());
manager.stop().unwrap();
assert_eq!(manager.state(), NetworkState::Stopped);
}
#[test]
fn test_network_manager_ip_allocation() {
let manager = NetworkManager::new(NetConfig::default());
manager.start().unwrap();
let ip1 = manager.allocate_ip().unwrap();
let ip2 = manager.allocate_ip().unwrap();
assert_ne!(ip1, ip2);
manager.release_ip(ip1);
let initial_available = manager.available_ips();
let _ip3 = manager.allocate_ip().unwrap();
assert!(manager.available_ips() < initial_available || initial_available == 253);
}
#[test]
fn test_set_dns_domain_switches_nxdomain_scope() {
let manager = NetworkManager::new(NetConfig::default());
let ip = IpAddr::V4(std::net::Ipv4Addr::new(172, 17, 0, 2));
manager.register_dns("web", ip);
let build_query = |name: &str| -> Vec<u8> {
let mut pkt = vec![0xAB, 0xCD, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00];
pkt.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
for label in name.split('.') {
pkt.push(label.len() as u8);
pkt.extend_from_slice(label.as_bytes());
}
pkt.push(0x00);
pkt.extend_from_slice(&[0x00, 0x01, 0x00, 0x01]);
pkt
};
let q = build_query("web.arcbox.local");
let resp = manager.try_resolve_dns_or_nxdomain(&q).unwrap();
assert_eq!(resp[3] & 0x0F, 0, "should resolve under default domain");
manager.set_dns_domain("custom.test");
manager.register_dns("web", ip);
let q = build_query("web.custom.test");
let resp = manager.try_resolve_dns_or_nxdomain(&q).unwrap();
assert_eq!(resp[3] & 0x0F, 0, "should resolve under custom domain");
let q = build_query("nope.custom.test");
let resp = manager.try_resolve_dns_or_nxdomain(&q).unwrap();
assert_eq!(
resp[3] & 0x0F,
3,
"NXDOMAIN for unregistered custom-domain host"
);
let q = build_query("web.arcbox.local");
assert!(
manager.try_resolve_dns_or_nxdomain(&q).is_none(),
"old domain queries should not match after set_dns_domain"
);
}
#[test]
fn test_network_manager_no_network_mode() {
let config = NetConfig {
mode: NetworkMode::None,
..Default::default()
};
let manager = NetworkManager::new(config);
manager.start().unwrap();
assert!(manager.allocate_ip().is_none());
}
}