use crate::capabilities::{render, View};
use crate::cli::{Cli, NetworkAction};
use crate::error::{FezError, Result};
use crate::protocol::client::BridgeClient;
use crate::transport;
use serde_json::{json, Value};
const NM_NAME: &str = "org.freedesktop.NetworkManager";
const NM_MGR_PATH: &str = "/org/freedesktop/NetworkManager";
const NM_MGR_IFACE: &str = "org.freedesktop.NetworkManager";
const PROPS_IFACE: &str = "org.freedesktop.DBus.Properties";
const DEVICE_IFACE: &str = "org.freedesktop.NetworkManager.Device";
const IP4_IFACE: &str = "org.freedesktop.NetworkManager.IP4Config";
const IP6_IFACE: &str = "org.freedesktop.NetworkManager.IP6Config";
const ACTIVE_IFACE: &str = "org.freedesktop.NetworkManager.Connection.Active";
const DHCP4_IFACE: &str = "org.freedesktop.NetworkManager.DHCP4Config";
pub fn dispatch(cli: &Cli, action: &NetworkAction) -> i32 {
let result = run(cli, action);
render(cli, result)
}
fn run(cli: &Cli, action: &NetworkAction) -> Result<View> {
let transport = transport::from_host(cli.host.as_deref());
let mut client = BridgeClient::connect(transport.as_ref())?;
let host = client.host().to_string();
let channel = client.dbus_open(NM_NAME)?;
match action {
NetworkAction::List { all } => list(&mut client, &channel, host, *all),
NetworkAction::Show { device } => show(&mut client, &channel, host, device),
}
}
fn device_type_str(n: u64) -> String {
match n {
0 => "unknown",
1 => "ethernet",
2 => "wifi",
5 => "bluetooth",
6 => "olpc-mesh",
7 => "wimax",
8 => "modem",
9 => "infiniband",
10 => "bond",
11 => "vlan",
12 => "adsl",
13 => "bridge",
14 => "generic",
15 => "team",
16 => "tun",
17 => "ip-tunnel",
18 => "macvlan",
19 => "vxlan",
20 => "veth",
32 => "loopback",
_ => return format!("type-{n}"),
}
.to_string()
}
fn device_state_str(n: u64) -> String {
match n {
0 => "unknown",
10 => "unmanaged",
20 => "unavailable",
30 => "disconnected",
40 => "prepare",
50 => "config",
60 => "need-auth",
70 => "ip-config",
80 => "ip-check",
90 => "secondaries",
100 => "activated",
110 => "deactivating",
120 => "failed",
_ => return format!("state-{n}"),
}
.to_string()
}
const PHYSICAL_TYPES: [u64; 8] = [
1, 2, 9, 10, 11, 13, 15, 32, ];
fn keep_device(device_type: u64, managed: bool) -> bool {
managed || PHYSICAL_TYPES.contains(&device_type)
}
fn unwrap_variant(v: &Value) -> &Value {
v.get("v").unwrap_or(v)
}
fn prop_str(props: &Value, key: &str) -> String {
props
.get(key)
.map(unwrap_variant)
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
}
fn prop_u64(props: &Value, key: &str) -> u64 {
props
.get(key)
.map(unwrap_variant)
.and_then(Value::as_u64)
.unwrap_or(0)
}
fn prop_bool(props: &Value, key: &str) -> bool {
props
.get(key)
.map(unwrap_variant)
.and_then(Value::as_bool)
.unwrap_or(false)
}
fn prop_path(props: &Value, key: &str) -> Option<String> {
let p = prop_str(props, key);
if p.is_empty() || p == "/" {
None
} else {
Some(p)
}
}
fn get_all(client: &mut BridgeClient, channel: &str, path: &str, iface: &str) -> Result<Value> {
let out = client.dbus_call(channel, path, PROPS_IFACE, "GetAll", json!([iface]))?;
out.get(0)
.cloned()
.ok_or_else(|| FezError::Problem(format!("GetAll({iface}) returned no value")))
}
fn address_entry(entry: &Value) -> Option<String> {
let addr = entry.get("address").map(unwrap_variant)?.as_str()?;
let prefix = entry
.get("prefix")
.map(unwrap_variant)
.and_then(Value::as_u64);
Some(match prefix {
Some(p) => format!("{addr}/{p}"),
None => addr.to_string(),
})
}
fn addresses(ip_props: &Value) -> Vec<String> {
ip_props
.get("AddressData")
.map(unwrap_variant)
.and_then(Value::as_array)
.map(|arr| arr.iter().filter_map(address_entry).collect())
.unwrap_or_default()
}
fn primary_address(ip_props: &Value) -> String {
ip_props
.get("AddressData")
.map(unwrap_variant)
.and_then(Value::as_array)
.and_then(|arr| arr.first())
.and_then(|e| e.get("address"))
.map(unwrap_variant)
.and_then(Value::as_str)
.unwrap_or("")
.to_string()
}
fn device_paths(client: &mut BridgeClient, channel: &str) -> Result<Vec<String>> {
let out = client.dbus_call(channel, NM_MGR_PATH, NM_MGR_IFACE, "GetDevices", json!([]))?;
let arr = out
.get(0)
.and_then(Value::as_array)
.ok_or_else(|| FezError::Problem("GetDevices returned a non-array response".into()))?;
Ok(arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect())
}
fn list(client: &mut BridgeClient, channel: &str, host: String, all: bool) -> Result<View> {
let paths = device_paths(client, channel)?;
let mut devices = Vec::new();
for path in &paths {
let props = get_all(client, channel, path, DEVICE_IFACE)?;
let device_type = prop_u64(&props, "DeviceType");
let managed = prop_bool(&props, "Managed");
if !all && !keep_device(device_type, managed) {
continue;
}
let ip4 = match prop_path(&props, "Ip4Config") {
Some(p) => primary_address(&get_all(client, channel, &p, IP4_IFACE)?),
None => String::new(),
};
let ip6 = match prop_path(&props, "Ip6Config") {
Some(p) => primary_address(&get_all(client, channel, &p, IP6_IFACE)?),
None => String::new(),
};
devices.push(json!({
"interface": prop_str(&props, "Interface"),
"type": device_type_str(device_type),
"state": device_state_str(prop_u64(&props, "State")),
"ip4": ip4,
"ip6": ip6,
"mac": prop_str(&props, "HwAddress"),
}));
}
let mut human = format!(
"{:<14} {:<10} {:<13} {:<20} {}\n",
"DEVICE", "TYPE", "STATE", "IPV4", "MAC"
);
for d in &devices {
human.push_str(&format!(
"{:<14} {:<10} {:<13} {:<20} {}\n",
ds(d, "interface"),
ds(d, "type"),
ds(d, "state"),
ds(d, "ip4"),
ds(d, "mac"),
));
}
let columns = ["interface", "type", "state", "ip4", "ip6", "mac"];
let rows: Vec<Value> = devices
.iter()
.map(|d| Value::Array(columns.iter().map(|c| d[*c].clone()).collect()))
.collect();
Ok(View::new(
"NetworkDeviceList",
host,
crate::envelope::table_data(&columns, rows),
human,
))
}
fn show(client: &mut BridgeClient, channel: &str, host: String, device: &str) -> Result<View> {
let paths = device_paths(client, channel)?;
let mut found: Option<Value> = None;
for path in &paths {
let props = get_all(client, channel, path, DEVICE_IFACE)?;
if prop_str(&props, "Interface") == device {
found = Some(props);
break;
}
}
let props = found.ok_or_else(|| FezError::NotFound(format!("network device {device}")))?;
let device_type = prop_u64(&props, "DeviceType");
let state = device_state_str(prop_u64(&props, "State"));
let (mut ip4_addrs, mut gateway, mut dns, mut domains) =
(Vec::new(), String::new(), Vec::new(), Vec::new());
if let Some(p) = prop_path(&props, "Ip4Config") {
let ip = get_all(client, channel, &p, IP4_IFACE)?;
ip4_addrs = addresses(&ip);
gateway = prop_str(&ip, "Gateway");
dns = nameservers(&ip);
domains = domains_list(&ip);
}
let mut ip6_addrs = Vec::new();
if let Some(p) = prop_path(&props, "Ip6Config") {
let ip = get_all(client, channel, &p, IP6_IFACE)?;
ip6_addrs = addresses(&ip);
}
let connection = match prop_path(&props, "ActiveConnection") {
Some(p) => {
let ac = get_all(client, channel, &p, ACTIVE_IFACE)?;
Some(json!({
"id": prop_str(&ac, "Id"),
"type": prop_str(&ac, "Type"),
"default": prop_bool(&ac, "Default"),
}))
}
None => None,
};
let dhcp = match prop_path(&props, "Dhcp4Config") {
Some(p) => {
let d = get_all(client, channel, &p, DHCP4_IFACE)?;
d.get("Options")
.map(unwrap_variant)
.and_then(flatten_options)
}
None => None,
};
let data = json!({
"interface": prop_str(&props, "Interface"),
"type": device_type_str(device_type),
"state": state,
"mac": prop_str(&props, "HwAddress"),
"mtu": prop_u64(&props, "Mtu"),
"ipv4": { "addresses": ip4_addrs, "gateway": gateway, "dns": dns, "domains": domains },
"ipv6": { "addresses": ip6_addrs },
"connection": connection,
"dhcp4": dhcp,
});
let human = render_show_human(&data);
Ok(View::new("NetworkDeviceDetail", host, data, human))
}
fn flatten_options(opts: &Value) -> Option<Value> {
let obj = opts.as_object()?;
let flat: serde_json::Map<String, Value> = obj
.iter()
.map(|(k, v)| (k.clone(), unwrap_variant(v).clone()))
.collect();
Some(Value::Object(flat))
}
fn nameservers(ip_props: &Value) -> Vec<String> {
ip_props
.get("NameserverData")
.map(unwrap_variant)
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|e| {
e.get("address")
.map(unwrap_variant)
.and_then(Value::as_str)
.map(str::to_string)
})
.collect()
})
.unwrap_or_default()
}
fn domains_list(ip_props: &Value) -> Vec<String> {
ip_props
.get("Domains")
.map(unwrap_variant)
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default()
}
fn render_show_human(d: &Value) -> String {
let mut out = String::new();
out.push_str(&format!("Device: {}\n", json_str(d, "interface")));
out.push_str(&format!("Type: {}\n", json_str(d, "type")));
out.push_str(&format!("State: {}\n", json_str(d, "state")));
out.push_str(&format!("MAC: {}\n", json_str(d, "mac")));
out.push_str(&format!(
"MTU: {}\n",
d.get("mtu").and_then(Value::as_u64).unwrap_or(0)
));
let ip4 = &d["ipv4"];
out.push_str(&format!("IPv4: {}\n", join_arr(ip4, "addresses")));
out.push_str(&format!("Gateway: {}\n", json_str(ip4, "gateway")));
out.push_str(&format!("DNS: {}\n", join_arr(ip4, "dns")));
out.push_str(&format!("Domains: {}\n", join_arr(ip4, "domains")));
out.push_str(&format!(
"IPv6: {}\n",
join_arr(&d["ipv6"], "addresses")
));
if let Some(conn) = d.get("connection").filter(|c| !c.is_null()) {
out.push_str(&format!(
"Connection: {} ({})\n",
json_str(conn, "id"),
json_str(conn, "type")
));
}
out
}
fn json_str(v: &Value, key: &str) -> String {
v.get(key).and_then(Value::as_str).unwrap_or("").to_string()
}
fn join_arr(v: &Value, key: &str) -> String {
v.get(key)
.and_then(Value::as_array)
.map(|arr| {
arr.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_default()
}
fn ds(v: &Value, key: &str) -> String {
v.get(key).and_then(Value::as_str).unwrap_or("").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn device_type_decodes_known_and_unknown() {
assert_eq!(device_type_str(1), "ethernet");
assert_eq!(device_type_str(2), "wifi");
assert_eq!(device_type_str(32), "loopback");
assert_eq!(device_type_str(20), "veth");
assert_eq!(device_type_str(999), "type-999");
}
#[test]
fn device_state_decodes_known_and_unknown() {
assert_eq!(device_state_str(100), "activated");
assert_eq!(device_state_str(20), "unavailable");
assert_eq!(device_state_str(10), "unmanaged");
assert_eq!(device_state_str(777), "state-777");
}
#[test]
fn filter_keeps_managed_and_physical_drops_unmanaged_virtual() {
assert!(keep_device(1, true));
assert!(keep_device(32, false));
assert!(!keep_device(20, false));
assert!(keep_device(20, true));
}
#[test]
fn unwrap_variant_handles_wrapped_and_flat() {
assert_eq!(unwrap_variant(&json!({"t":"s","v":"x"})), &json!("x"));
assert_eq!(unwrap_variant(&json!("x")), &json!("x"));
}
#[test]
fn address_entry_projects_address_and_prefix() {
let e = json!({"address":{"t":"s","v":"10.0.0.5"},"prefix":{"t":"u","v":24}});
assert_eq!(address_entry(&e).as_deref(), Some("10.0.0.5/24"));
let e = json!({"address":{"t":"s","v":"10.0.0.5"}});
assert_eq!(address_entry(&e).as_deref(), Some("10.0.0.5"));
assert_eq!(address_entry(&json!({})), None);
}
#[test]
fn flatten_options_unwraps_variant_values() {
let opts = json!({
"routers": {"t":"s","v":"192.168.10.1"},
"lease_time": {"t":"s","v":"3600"},
});
let flat = flatten_options(&opts).unwrap();
assert_eq!(flat["routers"], json!("192.168.10.1"));
assert_eq!(flat["lease_time"], json!("3600"));
assert_eq!(flatten_options(&json!("x")), None);
}
#[test]
fn prop_path_treats_null_path_as_absent() {
let props = json!({"Ip4Config":{"t":"o","v":"/"},"Ok":{"t":"o","v":"/x/1"}});
assert_eq!(prop_path(&props, "Ip4Config"), None);
assert_eq!(prop_path(&props, "Ok").as_deref(), Some("/x/1"));
assert_eq!(prop_path(&props, "Missing"), None);
}
}