use super::{
error::{ProtocolError, ProtocolResult},
};
use serde_json::{json, Value};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
struct MockState {
hostname: String,
uptime_seconds: u64,
interfaces: Vec<Value>,
firewall_rules: Vec<Value>,
nftables_tables: Vec<Value>,
nftables_running: bool,
dns_config: Value,
ntp_config: Value,
devices: Vec<Value>,
ksz_ports: Vec<Value>,
cpu_usage: f64,
memory_total: u64,
memory_used: u64,
nm_connections: Vec<Value>,
nm_devices: Vec<Value>,
dhcp_running: bool,
dhcp_leases: Vec<Value>,
dhcp_static_leases: Vec<Value>,
dhcp_config: Value,
}
impl Default for MockState {
fn default() -> Self {
Self {
hostname: "crrouter-dev".to_string(),
uptime_seconds: 12345,
interfaces: vec![
json!({
"name": "eth0",
"status": "up",
"ip": "192.168.1.100",
"mac": "b8:27:eb:12:34:56",
"speed": 1000,
"rx_bytes": 1234567890,
"tx_bytes": 987654321,
}),
json!({
"name": "wlan0",
"status": "up",
"ip": "192.168.2.1",
"mac": "b8:27:eb:ab:cd:ef",
"speed": 300,
"rx_bytes": 123456789,
"tx_bytes": 98765432,
}),
],
firewall_rules: vec![
json!({
"id": 1,
"chain": "input",
"action": "accept",
"protocol": "tcp",
"port": 22,
"comment": "Allow SSH",
}),
json!({
"id": 2,
"chain": "input",
"action": "accept",
"protocol": "tcp",
"port": 80,
"comment": "Allow HTTP",
}),
],
nftables_tables: vec![
json!({
"family": "inet",
"name": "filter",
"chains": [
{
"name": "input",
"type": "filter",
"hook": "input",
"priority": 0,
"policy": "accept",
"rules": [
{"handle": 1, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "iifname"}}, "right": "lo"}}, {"accept": null}]},
{"handle": 2, "expr": [{"match": {"op": "==", "left": {"ct": {"key": "state"}}, "right": ["established", "related"]}}, {"accept": null}]},
{"handle": 3, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "l4proto"}}, "right": "tcp"}}, {"match": {"op": "==", "left": {"tcp": {"dport": 22}}, "right": 22}}, {"accept": null}]},
]
},
{
"name": "forward",
"type": "filter",
"hook": "forward",
"priority": 0,
"policy": "drop"
},
{
"name": "output",
"type": "filter",
"hook": "output",
"priority": 0,
"policy": "accept"
}
]
}),
json!({
"family": "inet",
"name": "nat",
"chains": [
{
"name": "prerouting",
"type": "nat",
"hook": "prerouting",
"priority": -100,
"policy": "accept"
},
{
"name": "postrouting",
"type": "nat",
"hook": "postrouting",
"priority": 100,
"policy": "accept",
"rules": [
{"handle": 10, "expr": [{"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": "eth0"}}, {"masquerade": null}]}
]
}
]
})
],
nftables_running: true,
dns_config: json!({
"upstream_servers": ["1.1.1.1", "8.8.8.8"],
"listen_address": "0.0.0.0",
"port": 53,
"local_zones": {
"local.lan": [
{"record_type": "A", "value": "192.168.1.1"}
]
},
"dnssec_enabled": true,
}),
ntp_config: json!({
"upstream_servers": ["time.google.com", "time.cloudflare.com"],
"pool": "pool.ntp.org",
"local_stratum": 10,
"rtc_sync": true,
}),
devices: vec![
json!({
"id": 1,
"mac": "aa:bb:cc:dd:ee:01",
"ip": "192.168.1.101",
"hostname": "laptop-1",
"vendor": "Apple",
"first_seen": "2025-11-09T10:00:00Z",
"last_seen": "2025-11-10T15:30:00Z",
}),
json!({
"id": 2,
"mac": "aa:bb:cc:dd:ee:02",
"ip": "192.168.1.102",
"hostname": "phone-1",
"vendor": "Samsung",
"first_seen": "2025-11-09T12:00:00Z",
"last_seen": "2025-11-10T15:25:00Z",
}),
],
ksz_ports: vec![
json!({"port": 1, "link": "up", "speed": 1000, "duplex": "full"}),
json!({"port": 2, "link": "up", "speed": 1000, "duplex": "full"}),
json!({"port": 3, "link": "down", "speed": 0, "duplex": "none"}),
json!({"port": 4, "link": "down", "speed": 0, "duplex": "none"}),
],
cpu_usage: 25.5,
memory_total: 8_589_934_592, memory_used: 4_294_967_296,
nm_connections: vec![
json!({
"uuid": "12345678-1234-1234-1234-123456789012",
"id": "Wired connection 1",
"type": "802-3-ethernet",
"device": "eth0",
}),
],
nm_devices: vec![
json!({
"device": "eth0",
"type": "ethernet",
"state": "activated",
"ip4_address": "192.168.1.100/24",
}),
],
dhcp_running: true,
dhcp_leases: vec![
json!({
"mac": "aa:bb:cc:dd:ee:01",
"ip": "192.168.1.101",
"hostname": "laptop-1",
"expires": "2025-11-12T10:00:00Z",
"lease_time": 86400,
}),
json!({
"mac": "aa:bb:cc:dd:ee:02",
"ip": "192.168.1.102",
"hostname": "phone-1",
"expires": "2025-11-12T11:00:00Z",
"lease_time": 86400,
}),
],
dhcp_static_leases: vec![
json!({
"mac": "aa:bb:cc:dd:ee:ff",
"ip": "192.168.1.50",
"hostname": "server-1",
}),
],
dhcp_config: json!({
"interfaces": ["br0"],
"ranges": [
{
"start": "192.168.1.100",
"end": "192.168.1.200",
"interface": "br0",
}
],
"dns_servers": ["1.1.1.1", "8.8.8.8"],
"gateway": "192.168.1.1",
"lease_time": 86400,
"domain": "local.lan",
}),
}
}
}
pub struct MockDaemonClient {
state: Arc<RwLock<MockState>>,
}
impl MockDaemonClient {
pub fn new() -> Self {
Self {
state: Arc::new(RwLock::new(MockState::default())),
}
}
pub async fn call(
&self,
method: impl Into<String>,
params: Option<Value>,
) -> ProtocolResult<Value> {
let method = method.into();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
match method.as_str() {
"system.getInfo" => self.get_system_info().await,
"system.getHostname" => self.get_hostname().await,
"system.setHostname" => self.set_hostname(params).await,
"system.getUptime" => self.get_uptime().await,
"system.getMemory" => self.get_memory().await,
"system.getCpu" => self.get_cpu().await,
"system.getLoad" => self.get_load().await,
"system.reboot" => self.reboot(params).await,
"system.changePassword" => self.change_password(params).await,
"system.getDaemonStatus" => self.get_daemon_status().await,
"network.getInterfaces" => self.get_interfaces().await,
"network.getInterface" => self.get_interface(params).await,
"network.setInterface" => self.set_interface(params).await,
"firewall.getRules" => self.get_firewall_rules().await,
"firewall.addRule" => self.add_firewall_rule(params).await,
"firewall.deleteRule" => self.delete_firewall_rule(params).await,
"nftables.listTables" | "firewall.listTables" => self.nftables_list_tables().await,
"nftables.listRules" | "firewall.listRules" => self.nftables_list_rules(params).await,
"nftables.listAllRules" | "firewall.listAllRules" => self.nftables_list_all_rules().await,
"nftables.addRule" => self.nftables_add_rule(params).await,
"nftables.deleteRule" => self.nftables_delete_rule(params).await,
"nftables.flushChain" => self.nftables_flush_chain(params).await,
"nftables.getStatus" => self.nftables_get_status().await,
"nftables.reload" => self.nftables_reload().await,
"dns.getConfig" => self.get_dns_config().await,
"dns.setConfig" => self.set_dns_config(params).await,
"dns.getStats" => self.get_dns_stats().await,
"dns.flush" => self.dns_flush().await,
"ntp.getConfig" => self.get_ntp_config().await,
"ntp.setConfig" => self.set_ntp_config(params).await,
"ntp.getStatus" => self.get_ntp_status().await,
"ntp.sync" => self.ntp_sync().await,
"chrony.tracking" | "ntp.status" => self.chrony_tracking().await,
"chrony.sources" | "ntp.sources" => self.chrony_sources().await,
"chrony.sourcestats" => self.chrony_sourcestats().await,
"chrony.serverstats" | "ntp.stats" => self.chrony_serverstats().await,
"chrony.activity" => self.chrony_activity().await,
"chrony.makestep" => self.chrony_makestep().await,
"chrony.addServer" => self.chrony_add_server(params).await,
"chrony.deleteServer" => self.chrony_delete_server(params).await,
"chrony.burst" => self.chrony_burst(params).await,
"chrony.version" => self.chrony_version().await,
"devices.list" => self.list_devices().await,
"devices.get" => self.get_device(params).await,
"ksz.getPorts" => self.get_ksz_ports().await,
"ksz.getPort" => self.get_ksz_port(params).await,
"ksz.setPort" => self.set_ksz_port(params).await,
"nm.listConnections" => self.nm_list_connections().await,
"nm.listDevices" => self.nm_list_devices().await,
"nm.getConnection" => self.nm_get_connection(params).await,
"nm.activateConnection" => self.nm_activate_connection(params).await,
"networkmanager.getState" => self.networkmanager_get_state().await,
"networkmanager.listDevices" => self.networkmanager_list_devices().await,
"networkmanager.getDevice" => self.networkmanager_get_device(params).await,
"networkmanager.listConnections" => self.networkmanager_list_connections().await,
"networkmanager.listActiveConnections" => self.networkmanager_list_active_connections().await,
"networkmanager.activateConnection" => self.networkmanager_activate_connection(params).await,
"networkmanager.deactivateConnection" => self.networkmanager_deactivate_connection(params).await,
"networkmanager.enableNetworking" => self.networkmanager_enable_networking(params).await,
"networkmanager.enableWireless" => self.networkmanager_enable_wireless(params).await,
"networkmanager.scanWifi" => self.networkmanager_scan_wifi(params).await,
"networkmanager.createAccessPoint" => self.networkmanager_create_access_point(params).await,
"networkmanager.activateAccessPoint" => self.networkmanager_activate_access_point(params).await,
"networkmanager.stopAccessPoint" => self.networkmanager_stop_access_point(params).await,
"networkmanager.addConnection" => self.networkmanager_add_connection(params).await,
"networkmanager.deleteConnection" => self.networkmanager_delete_connection(params).await,
"networkmanager.updateConnection" => self.networkmanager_update_connection(params).await,
"dhcp.getStatus" => self.dhcp_get_status().await,
"dhcp.getStats" => self.dhcp_get_stats().await,
"dhcp.getLeases" => self.dhcp_get_leases().await,
"dhcp.start" => self.dhcp_start().await,
"dhcp.stop" => self.dhcp_stop().await,
"dhcp.restart" => self.dhcp_restart().await,
"dhcp.addStaticLease" => self.dhcp_add_static_lease(params).await,
"dhcp.removeStaticLease" => self.dhcp_remove_static_lease(params).await,
"dhcp.getConfig" => self.dhcp_get_config().await,
"dhcp.setConfig" => self.dhcp_set_config(params).await,
_ => Err(ProtocolError::MethodNotFound(method)),
}
}
async fn get_system_info(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"hostname": state.hostname,
"uptime": state.uptime_seconds,
"model": "Raspberry Pi 5",
"kernel": "6.12.47+rpt-rpi-2712",
"architecture": "aarch64",
"load_avg": [0.5, 0.4, 0.3],
"cpu_usage": state.cpu_usage,
"memory_total": state.memory_total,
"memory_used": state.memory_used,
"memory_percent": (state.memory_used as f64 / state.memory_total as f64) * 100.0,
}))
}
async fn get_hostname(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "hostname": state.hostname }))
}
async fn set_hostname(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let hostname = params.get("hostname")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing hostname".to_string()))?;
let mut state = self.state.write().await;
state.hostname = hostname.to_string();
Ok(json!({ "success": true, "hostname": hostname }))
}
async fn get_uptime(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "uptime_seconds": state.uptime_seconds }))
}
async fn get_memory(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"total": state.memory_total,
"used": state.memory_used,
"free": state.memory_total - state.memory_used,
"percent": (state.memory_used as f64 / state.memory_total as f64) * 100.0,
}))
}
async fn get_cpu(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"usage_percent": state.cpu_usage,
"cores": 4,
"model": "ARM Cortex-A76",
}))
}
async fn get_load(&self) -> ProtocolResult<Value> {
Ok(json!({
"load_1": 0.5,
"load_5": 0.4,
"load_15": 0.3,
}))
}
async fn reboot(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true, "message": "Reboot scheduled (mock)" }))
}
async fn change_password(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn get_daemon_status(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"running": true,
"socket_path": "/var/run/crrouterd.sock",
"plugins": [
"system",
"network",
"firewall",
"unbound",
"chrony",
"networkmanager",
"nftables_proxy",
"dhcp"
],
"uptime": state.uptime_seconds,
}))
}
async fn get_interfaces(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "interfaces": state.interfaces }))
}
async fn get_interface(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let name = params.get("name")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing name".to_string()))?;
let state = self.state.read().await;
let interface = state.interfaces.iter()
.find(|i| i.get("name").and_then(|v| v.as_str()) == Some(name))
.ok_or(ProtocolError::Internal(format!("Interface {} not found", name)))?;
Ok(interface.clone())
}
async fn set_interface(&self, params: Option<Value>) -> ProtocolResult<Value> {
let _params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
Ok(json!({ "success": true }))
}
async fn get_firewall_rules(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "rules": state.firewall_rules }))
}
async fn add_firewall_rule(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let mut state = self.state.write().await;
let new_id = state.firewall_rules.len() as i64 + 1;
let mut rule = params.clone();
if let Some(obj) = rule.as_object_mut() {
obj.insert("id".to_string(), json!(new_id));
}
state.firewall_rules.push(rule.clone());
Ok(json!({ "success": true, "rule": rule }))
}
async fn delete_firewall_rule(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let id = params.get("id")
.and_then(|v| v.as_i64())
.ok_or(ProtocolError::InvalidParams("Missing id".to_string()))?;
let mut state = self.state.write().await;
state.firewall_rules.retain(|r| r.get("id").and_then(|v| v.as_i64()) != Some(id));
Ok(json!({ "success": true }))
}
async fn get_dns_config(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(state.dns_config.clone())
}
async fn set_dns_config(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let mut state = self.state.write().await;
state.dns_config = params;
Ok(json!({ "success": true }))
}
async fn get_dns_stats(&self) -> ProtocolResult<Value> {
Ok(json!({
"queries_total": 12345,
"queries_cached": 8900,
"cache_hit_rate": 72.1,
"upstream_queries": 3445,
}))
}
async fn dns_flush(&self) -> ProtocolResult<Value> {
Ok(json!({ "success": true, "message": "DNS cache flushed (mock)" }))
}
async fn get_ntp_config(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(state.ntp_config.clone())
}
async fn set_ntp_config(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let mut state = self.state.write().await;
state.ntp_config = params;
Ok(json!({ "success": true }))
}
async fn get_ntp_status(&self) -> ProtocolResult<Value> {
Ok(json!({
"synchronized": true,
"stratum": 3,
"offset": 0.00123,
"jitter": 0.00045,
"sources": [
{
"name": "time.google.com",
"reachable": true,
"offset": 0.00123,
"jitter": 0.00045,
}
]
}))
}
async fn ntp_sync(&self) -> ProtocolResult<Value> {
Ok(json!({ "success": true, "message": "Time sync initiated (mock)" }))
}
async fn chrony_tracking(&self) -> ProtocolResult<Value> {
Ok(json!({
"reference_id": "185.125.190.56",
"stratum": 3,
"ref_time": "Mon Nov 11 00:00:00 2025",
"system_time": 0.000123,
"last_offset": 0.000234,
"rms_offset": 0.000345,
"frequency": 12.345,
"residual_freq": 0.012,
"skew": 0.123,
"root_delay": 0.012345,
"root_dispersion": 0.001234,
"update_interval": 64.0,
"leap_status": "Normal"
}))
}
async fn chrony_sources(&self) -> ProtocolResult<Value> {
Ok(json!([
{
"mode": "^",
"state": "*",
"name": "time.cloudflare.com",
"stratum": 2,
"poll": 10,
"reach": 377,
"last_rx": "59",
"last_sample": "+123us +456us",
"offset": 0.000123,
"offset_error": 0.000012
},
{
"mode": "^",
"state": "+",
"name": "time.google.com",
"stratum": 2,
"poll": 10,
"reach": 377,
"last_rx": "120",
"last_sample": "+234us +567us",
"offset": 0.000234,
"offset_error": 0.000023
}
]))
}
async fn chrony_sourcestats(&self) -> ProtocolResult<Value> {
Ok(json!([
{
"name": "time.cloudflare.com",
"np": 64,
"nr": 32,
"span": 1024,
"frequency": 12.345,
"freq_skew": 0.123,
"offset": 0.000123,
"std_dev": 0.000456
},
{
"name": "time.google.com",
"np": 64,
"nr": 32,
"span": 1024,
"frequency": 12.345,
"freq_skew": 0.123,
"offset": 0.000234,
"std_dev": 0.000567
}
]))
}
async fn chrony_serverstats(&self) -> ProtocolResult<Value> {
Ok(json!({
"ntp_hits": 1234,
"nke_hits": 0,
"cmd_hits": 45,
"ntp_drops": 12,
"nke_drops": 0,
"cmd_drops": 0,
"log_drops": 0
}))
}
async fn chrony_activity(&self) -> ProtocolResult<Value> {
Ok(json!({
"online": 2,
"offline": 0,
"burst_online": 0,
"burst_offline": 0,
"unresolved": 0
}))
}
async fn chrony_makestep(&self) -> ProtocolResult<Value> {
Ok(json!({
"success": true,
"message": "Time step completed (mock)"
}))
}
async fn chrony_add_server(&self, params: Option<Value>) -> ProtocolResult<Value> {
let hostname = params
.and_then(|p| p.get("hostname").cloned())
.and_then(|h| h.as_str().map(String::from))
.ok_or(ProtocolError::InvalidParams("Missing hostname".to_string()))?;
Ok(json!({
"success": true,
"message": format!("Server {} added (mock)", hostname)
}))
}
async fn chrony_delete_server(&self, params: Option<Value>) -> ProtocolResult<Value> {
let hostname = params
.and_then(|p| p.get("hostname").cloned())
.and_then(|h| h.as_str().map(String::from))
.ok_or(ProtocolError::InvalidParams("Missing hostname".to_string()))?;
Ok(json!({
"success": true,
"message": format!("Server {} deleted (mock)", hostname)
}))
}
async fn chrony_burst(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({
"success": true,
"message": "Burst poll initiated (mock)"
}))
}
async fn chrony_version(&self) -> ProtocolResult<Value> {
Ok(json!({
"version": "chrony version 4.5 (mock)"
}))
}
async fn list_devices(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "devices": state.devices }))
}
async fn get_device(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let id = params.get("id")
.and_then(|v| v.as_i64())
.ok_or(ProtocolError::InvalidParams("Missing id".to_string()))?;
let state = self.state.read().await;
let device = state.devices.iter()
.find(|d| d.get("id").and_then(|v| v.as_i64()) == Some(id))
.ok_or(ProtocolError::Internal(format!("Device {} not found", id)))?;
Ok(device.clone())
}
async fn get_ksz_ports(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "ports": state.ksz_ports }))
}
async fn get_ksz_port(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let port = params.get("port")
.and_then(|v| v.as_i64())
.ok_or(ProtocolError::InvalidParams("Missing port".to_string()))?;
let state = self.state.read().await;
let port_data = state.ksz_ports.iter()
.find(|p| p.get("port").and_then(|v| v.as_i64()) == Some(port))
.ok_or(ProtocolError::Internal(format!("Port {} not found", port)))?;
Ok(port_data.clone())
}
async fn set_ksz_port(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn nm_list_connections(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "connections": state.nm_connections }))
}
async fn nm_list_devices(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({ "devices": state.nm_devices }))
}
async fn nm_get_connection(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let uuid = params.get("uuid")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing uuid".to_string()))?;
let state = self.state.read().await;
let conn = state.nm_connections.iter()
.find(|c| c.get("uuid").and_then(|v| v.as_str()) == Some(uuid))
.ok_or(ProtocolError::Internal(format!("Connection {} not found", uuid)))?;
Ok(conn.clone())
}
async fn nm_activate_connection(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true, "message": "Connection activated (mock)" }))
}
async fn networkmanager_get_state(&self) -> ProtocolResult<Value> {
Ok(json!({
"state": "connectedglobal",
"networking_enabled": true,
"wireless_enabled": true,
"wireless_hw_enabled": true,
"version": "1.0.0-mock"
}))
}
async fn networkmanager_list_devices(&self) -> ProtocolResult<Value> {
Ok(json!([
{
"path": "/org/freedesktop/NetworkManager/Devices/1",
"interface": "eth0",
"device_type": "ethernet",
"state": "activated",
"ip4_config": {
"addresses": ["192.168.1.100/24"],
"gateway": "192.168.1.1",
"nameservers": ["8.8.8.8"],
"domains": [],
"routes": []
},
"ip6_config": null,
"driver": "bcmgenet",
"hw_address": "b8:27:eb:12:34:56"
},
{
"path": "/org/freedesktop/NetworkManager/Devices/2",
"interface": "wlan0",
"device_type": "wifi",
"state": "disconnected",
"ip4_config": null,
"ip6_config": null,
"driver": "brcmfmac",
"hw_address": "b8:27:eb:ab:cd:ef"
}
]))
}
async fn networkmanager_get_device(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let interface = params.get("interface")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing interface".to_string()))?;
if interface == "eth0" {
Ok(json!({
"path": "/org/freedesktop/NetworkManager/Devices/1",
"interface": "eth0",
"device_type": "ethernet",
"state": "activated",
"ip4_config": {
"addresses": ["192.168.1.100/24"],
"gateway": "192.168.1.1",
"nameservers": ["8.8.8.8"],
"domains": [],
"routes": []
},
"ip6_config": null,
"driver": "bcmgenet",
"hw_address": "b8:27:eb:12:34:56"
}))
} else if interface == "wlan0" {
Ok(json!({
"path": "/org/freedesktop/NetworkManager/Devices/2",
"interface": "wlan0",
"device_type": "wifi",
"state": "disconnected",
"ip4_config": null,
"ip6_config": null,
"driver": "brcmfmac",
"hw_address": "b8:27:eb:ab:cd:ef"
}))
} else {
Err(ProtocolError::Internal(format!("Device {} not found", interface)))
}
}
async fn networkmanager_list_connections(&self) -> ProtocolResult<Value> {
Ok(json!([
{
"uuid": "12345678-1234-1234-1234-123456789012",
"id": "Wired connection 1",
"connection_type": "802-3-ethernet",
"interface_name": "eth0",
"is_active": true,
"settings": {}
},
{
"uuid": "87654321-4321-4321-4321-210987654321",
"id": "WiFi Home",
"connection_type": "802-11-wireless",
"interface_name": "wlan0",
"is_active": false,
"settings": {}
}
]))
}
async fn networkmanager_list_active_connections(&self) -> ProtocolResult<Value> {
Ok(json!([
{
"uuid": "12345678-1234-1234-1234-123456789012",
"id": "Wired connection 1",
"connection_type": "802-3-ethernet",
"state": "activated",
"devices": ["eth0"],
"is_default": true,
"is_vpn": false
}
]))
}
async fn networkmanager_activate_connection(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let uuid = params.get("uuid")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing uuid".to_string()))?;
Ok(json!({
"active_connection_path": format!("/org/freedesktop/NetworkManager/ActiveConnection/{}", uuid),
"success": true
}))
}
async fn networkmanager_deactivate_connection(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn networkmanager_enable_networking(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn networkmanager_enable_wireless(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn networkmanager_scan_wifi(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!([
{
"ssid": "HomeNetwork",
"hw_address": "AA:BB:CC:DD:EE:FF",
"strength": 85,
"frequency": 2437,
"flags": 0,
"wpa_flags": 0,
"rsn_flags": 256
},
{
"ssid": "NeighborWiFi",
"hw_address": "11:22:33:44:55:66",
"strength": 45,
"frequency": 5180,
"flags": 0,
"wpa_flags": 0,
"rsn_flags": 256
}
]))
}
async fn networkmanager_create_access_point(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let ssid = params.get("ssid")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing ssid".to_string()))?;
let uuid = format!("ap-{}-{}", ssid, "12345678-1234-1234-1234-123456789012");
Ok(json!({
"uuid": uuid,
"success": true
}))
}
async fn networkmanager_activate_access_point(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let uuid = params.get("uuid")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing uuid".to_string()))?;
Ok(json!({
"active_connection": format!("/org/freedesktop/NetworkManager/ActiveConnection/{}", uuid),
"success": true
}))
}
async fn networkmanager_stop_access_point(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn networkmanager_add_connection(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({
"connection_path": "/org/freedesktop/NetworkManager/Settings/1",
"success": true
}))
}
async fn networkmanager_delete_connection(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn networkmanager_update_connection(&self, _params: Option<Value>) -> ProtocolResult<Value> {
Ok(json!({ "success": true }))
}
async fn dhcp_get_status(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"running": state.dhcp_running,
"service": "dora",
"uptime": 3600,
}))
}
async fn dhcp_get_stats(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"total_leases": state.dhcp_leases.len(),
"static_leases": state.dhcp_static_leases.len(),
"available_ips": 100 - state.dhcp_leases.len(),
}))
}
async fn dhcp_get_leases(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"leases": state.dhcp_leases,
"static_leases": state.dhcp_static_leases,
}))
}
async fn dhcp_start(&self) -> ProtocolResult<Value> {
let mut state = self.state.write().await;
state.dhcp_running = true;
Ok(json!({
"success": true,
"message": "DHCP server started (mock)",
}))
}
async fn dhcp_stop(&self) -> ProtocolResult<Value> {
let mut state = self.state.write().await;
state.dhcp_running = false;
Ok(json!({
"success": true,
"message": "DHCP server stopped (mock)",
}))
}
async fn dhcp_restart(&self) -> ProtocolResult<Value> {
Ok(json!({
"success": true,
"message": "DHCP server restarted (mock)",
}))
}
async fn dhcp_add_static_lease(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let mac = params.get("mac")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing mac".to_string()))?;
let ip = params.get("ip")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing ip".to_string()))?;
let hostname = params.get("hostname")
.and_then(|v| v.as_str());
let mut state = self.state.write().await;
let lease = json!({
"mac": mac,
"ip": ip,
"hostname": hostname.unwrap_or(""),
});
state.dhcp_static_leases.push(lease.clone());
Ok(json!({
"success": true,
"lease": lease,
}))
}
async fn dhcp_remove_static_lease(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let mac = params.get("mac")
.and_then(|v| v.as_str())
.ok_or(ProtocolError::InvalidParams("Missing mac".to_string()))?;
let mut state = self.state.write().await;
state.dhcp_static_leases.retain(|lease| {
lease.get("mac").and_then(|v| v.as_str()) != Some(mac)
});
Ok(json!({
"success": true,
"message": format!("Static lease for {} removed (mock)", mac),
}))
}
async fn dhcp_get_config(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(state.dhcp_config.clone())
}
async fn dhcp_set_config(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let mut state = self.state.write().await;
state.dhcp_config = params;
Ok(json!({
"success": true,
"message": "DHCP configuration updated (mock)",
}))
}
async fn nftables_list_tables(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"tables": state.nftables_tables.clone(),
}))
}
async fn nftables_list_rules(&self, params: Option<Value>) -> ProtocolResult<Value> {
let params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
let family = params.get("family").and_then(|v| v.as_str()).unwrap_or("inet");
let table = params.get("table").and_then(|v| v.as_str()).unwrap_or("filter");
let state = self.state.read().await;
let table_data = state.nftables_tables.iter()
.find(|t| {
t.get("family").and_then(|v| v.as_str()) == Some(family) &&
t.get("name").and_then(|v| v.as_str()) == Some(table)
})
.cloned()
.unwrap_or_else(|| json!({}));
Ok(json!({
"table": table_data,
}))
}
async fn nftables_list_all_rules(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"ruleset": state.nftables_tables.clone(),
}))
}
async fn nftables_add_rule(&self, params: Option<Value>) -> ProtocolResult<Value> {
let _params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
Ok(json!({
"success": true,
"message": "Rule added (mock)",
"handle": 100,
}))
}
async fn nftables_delete_rule(&self, params: Option<Value>) -> ProtocolResult<Value> {
let _params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
Ok(json!({
"success": true,
"message": "Rule deleted (mock)",
}))
}
async fn nftables_flush_chain(&self, params: Option<Value>) -> ProtocolResult<Value> {
let _params = params.ok_or(ProtocolError::InvalidParams("Missing params".to_string()))?;
Ok(json!({
"success": true,
"message": "Chain flushed (mock)",
}))
}
async fn nftables_get_status(&self) -> ProtocolResult<Value> {
let state = self.state.read().await;
Ok(json!({
"running": state.nftables_running,
"service": "nftables",
"table_count": state.nftables_tables.len(),
}))
}
async fn nftables_reload(&self) -> ProtocolResult<Value> {
Ok(json!({
"success": true,
"message": "Nftables reloaded (mock)",
}))
}
}
impl Default for MockDaemonClient {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_mock_get_system_info() {
let client = MockDaemonClient::new();
let result = client.call("system.getInfo", None).await.unwrap();
assert!(result.get("hostname").is_some());
assert!(result.get("uptime").is_some());
assert_eq!(result.get("model").unwrap().as_str().unwrap(), "Raspberry Pi 5");
}
#[tokio::test]
async fn test_mock_set_hostname() {
let client = MockDaemonClient::new();
let params = json!({ "hostname": "test-router" });
let result = client.call("system.setHostname", Some(params)).await.unwrap();
assert_eq!(result.get("success").unwrap().as_bool().unwrap(), true);
let info = client.call("system.getHostname", None).await.unwrap();
assert_eq!(info.get("hostname").unwrap().as_str().unwrap(), "test-router");
}
#[tokio::test]
async fn test_mock_get_interfaces() {
let client = MockDaemonClient::new();
let result = client.call("network.getInterfaces", None).await.unwrap();
let interfaces = result.get("interfaces").unwrap().as_array().unwrap();
assert_eq!(interfaces.len(), 2);
}
#[tokio::test]
async fn test_mock_firewall_add_delete() {
let client = MockDaemonClient::new();
let rule = json!({
"chain": "input",
"action": "drop",
"protocol": "tcp",
"port": 23,
"comment": "Block telnet",
});
let add_result = client.call("firewall.addRule", Some(rule)).await.unwrap();
assert_eq!(add_result.get("success").unwrap().as_bool().unwrap(), true);
let added_rule = add_result.get("rule").unwrap();
let rule_id = added_rule.get("id").unwrap().as_i64().unwrap();
let delete_params = json!({ "id": rule_id });
let delete_result = client.call("firewall.deleteRule", Some(delete_params)).await.unwrap();
assert_eq!(delete_result.get("success").unwrap().as_bool().unwrap(), true);
}
#[tokio::test]
async fn test_mock_method_not_found() {
let client = MockDaemonClient::new();
let result = client.call("nonexistent.method", None).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ProtocolError::MethodNotFound(_)));
}
}