Files
wlan-ucentral-schema/system/state.uc
Lannar Dean 1784072a0b [state.uc] Prevent crash when devstats is null
In some (unknown) circumstances, devstats can be null, and attempting to reference devstats[port] causes a crash of state.uc

prevent unsafe access by checking if devstats is null

Signed-off-by: Lannar Dean <moracca@gmail.com>
2026-01-18 01:49:22 -05:00

1137 lines
30 KiB
Ucode
Executable File

#!/usr/bin/ucode
push(REQUIRE_SEARCH_PATH, '/usr/share/ucentral/*.uc');
let fs = require("fs");
let uci = require("uci");
let ubus = require("ubus");
let cfgfile = fs.open("/etc/ucentral/ucentral.active", "r");
let cfg = json(cfgfile.read("all"));
let capabfile = fs.open("/etc/ucentral/capabilities.json", "r");
let capab = json(capabfile.read("all"));
let vsifile = fs.open("/tmp/udhcpc-vsi.json", "r");
let vsi = vsifile ? json(vsifile.read("all")) : null;
let now = time();
/* set up basic functionality */
if (!cursor)
cursor = uci.cursor();
if (!ctx)
ctx = ubus.connect();
let state = {
unit: { memory: {} },
radios: [],
interfaces: []
};
function discover_ports() {
let roles = {};
/* Derive ethernet port names and roles from default config */
for (let role, spec in capab.network) {
for (let i, ifname in spec) {
role = uc(role);
push(roles[role] = roles[role] || [], {
netdev: ifname,
index: i
});
}
}
/* Sort ports in each role group according to their index, then normalize
* names into uppercase role name with 1-based index suffix in case of multiple
* ports or just uppercase role name in case of single ports */
let rv = {};
for (let role, ports in roles) {
switch (length(ports)) {
case 0:
break;
case 1:
rv[role] = ports[0];
break;
default:
map(sort(ports, (a, b) => (a.index - b.index)), (port, i) => {
rv[role + (i + 1)] = port;
});
}
}
return rv;
}
let select_ports = discover_ports();
function lookup_port(netdev) {
for (let k, v in select_ports)
if (v.netdev == netdev)
return k;
return 'unknown';
}
/* find out what telemetry we should gather */
let stats;
cursor.load("state");
cursor.load("event");
if (!length(stats)) {
stats = cursor.get_all("state", "stats");
}
let delta = 1;
if (telemetry)
delta = 0;
global.tid_stats = (index(stats.types, 'tid-stats') > 0);
/* load state data */
let ipv6leases = ctx.call("dhcp", "ipv6leases");
let topology = ctx.call("topology", "mac");
let wifistatus = ctx.call("network.wireless", "status");
let wifiphy = require('wifi.phy');
let wifiiface = require('wifi.iface');
let stations = require('wifi.station');
//Scan for WDS STA dynamic interfaces (e.g., wlan2.sta1)
let wds_sta_interfaces = {};
if (length(wifistatus)) {
for (let radio, data in wifistatus) {
for (let k, vap in data.interfaces) {
let base_ifname = vap.ifname;
// Look for dynamic WDS STA interfaces using glob pattern
let wds_pattern = "/sys/class/net/" + base_ifname + ".sta*";
let wds_paths = fs.glob(wds_pattern);
for (let wds_path in wds_paths) {
let wds_ifname = split(wds_path, "/")[-1];
if (!wds_sta_interfaces[base_ifname])
wds_sta_interfaces[base_ifname] = [];
push(wds_sta_interfaces[base_ifname], wds_ifname);
}
}
}
}
// Collect all stations including WDS STA interfaces
let all_stations = { ...stations };
for (let base_ifname, wds_list in wds_sta_interfaces) {
for (let wds_ifname in wds_list) {
try {
// Try to get WDS STA association info via ubus
let wds_stations = ctx.call("hostapd." + wds_ifname, "get_clients");
if (wds_stations && length(wds_stations)) {
all_stations[wds_ifname] = wds_stations;
}
} catch (e) {
// Fallback: try alternative method to get WDS STA info
// This might need to be adjusted based on your system's WDS implementation
}
}
}
let survey = require('wifi.survey');
let mesh = require('wifi.mesh');
let ports = ctx.call("topology", "port", { delta });
let poe = ctx.call("poe", "info");
let gps = ctx.call("gps", "info");
let ieee8021x = ctx.call("ieee8021x", "dump");
let devstats = ctx.call('udevstats', 'dump');
let devices = ctx.call("network.device", "status");
let previous = json(fs.readfile('/tmp/' + (telemetry ? 'telemetry.json' : 'state.json')) || '{ "ports": {}, "devstats": {}, "stations": {}, "devstats": {} }');
let finger_config = cursor.get_all("state", "fingerprint");
let finger_state = json(fs.readfile('/tmp/finger.state') || '{}');
let fingerprint;
if (finger_config?.mode != 'polled')
fingerprint = ctx.call("fingerprint", "fingerprint", { age: +(finger_config?.min_age || 0), raw: (finger_config?.mode == 'raw') }) || {};
let finger_wan = [];
if (!+finger_config?.allow_wan)
for (let k in cursor.get("event", "config", "wan_port"))
push(finger_wan, lookup_port(k));
//Use all_stations instead of stations to include WDS STA data
let stations_lookup = {};
for (let k, v in all_stations) {
stations_lookup[k] = {};
for (let assoc in v)
stations_lookup[k][assoc.station] = assoc;
}
fs.writefile('/tmp/' + (telemetry ? 'telemetry.json' : 'state.json'), { ports, devstats, stations: stations_lookup, devstats });
if (fingerprint) {
for (let mac in fingerprint) {
if (!finger_state[mac])
finger_state[mac] = { reported: 0 };
finger_state[mac].seen = now;
}
for (let mac, data in finger_state)
if ((now - data.seen) > (+finger_config?.max_age || 600))
delete finger_state[mac];
}
//printf('%.J\n', previous);
let lldp = [];
let wireless = cursor.get_all("wireless");
let snoop = ctx.call("dhcpsnoop", "dump");
let captive = ctx.call("spotfilter", "client_list", { "interface": "hotspot"});
function generate_deltas(counters, prev_counters) {
let ret = {};
for (let k in [ "rx_packets", "tx_packets", "rx_bytes", "tx_bytes", "tx_retries", "tx_failed", "rx_errors", "tx_errors", "rx_dropped", "tx_dropped", "multicast", "collisions" ]) {
if (prev_counters[k] < 0)
prev_counters[k] = 0;
ret[k] = counters[k] - prev_counters[k];
if (ret[k] < 0) {
if (counters[k] > 0)
ret[k] = counters[k];
else
ret[k] = 0;
}
}
return ret;
}
function ports_deltas(port) {
if (!ports[port]?.counters || !previous.ports[port]?.counters)
return {};
return generate_deltas(ports[port].counters, previous.ports[port].counters);
}
function stations_deltas(assoc, iface) {
let ret = {};
// Search for the station in the dynamic iface
if (assoc.dynamic_vlan)
iface = iface + "-v" + assoc.dynamic_vlan;
if (!previous.stations[iface] || !previous.stations[iface][assoc.station])
return ret;
for (let k in [ "rx_packets", "tx_packets", "rx_bytes", "tx_bytes", "tx_retries", "tx_failed" ]) {
if (assoc["connected_time"] < previous.stations[iface][assoc.station]["connected_time"]) {
ret[k] = assoc[k];
} else {
ret[k] = assoc[k] - (previous.stations[iface][assoc.station][k] || 0);
}
// Stil negative?
if (ret[k] < 0 && assoc[k] > 0) {
ret[k] = assoc[k];
} else if (ret[k] < 0) {
ret[k] = 0;
}
}
return ret;
}
/* prepare dhcp leases cache */
let ip4leases = {};
try {
let fd = fs.open("/tmp/dhcp.leases");
if (fd) {
let line;
while (line = fd.read("line")) {
let tokens = split(line, " ");
if (length(tokens) < 4)
continue;
ip4leases[tokens[1]] = {
assigned: tokens[0],
mac: tokens[1],
address: tokens[2],
hostname: tokens[3]
};
}
fd.close();
}
}
catch(e) {
printf("Failed to parse dhcp leases cache: %s\n%s\n", e, e.stacktrace[0].context);
}
/* prepare lldp cache */
try {
let stdout = fs.popen("lldpcli -f json show neighbors");
let tmp;
if (stdout) {
tmp = json(stdout.read("all")).lldp[0].interface;
stdout.close();
} else {
printf("LLDP cli command failed: %s", fs.error());
}
for (let key, iface in tmp) {
let peer = { };
for (let host, chassis in iface.chassis) {
if (!length(chassis.id) ||
!length(chassis.descr))
continue;
peer.mac = chassis.id[0].value;
peer.ifname = iface.name;
peer.description = chassis.descr[0].value;
if (iface?.port[0]?.id[0]?.value && iface?.port[0]?.descr[0]?.value) {
peer.port_id = iface.port[0].id[0].value;
peer.port_descr = iface.port[0].descr[0].value;
}
if (length(chassis.name))
peer.name = chassis.name[0].value;
if (length(chassis['mgmt-ip'])) {
let ipaddr = [];
for (let ip in chassis["mgmt-ip"])
push(ipaddr, ip.value);
peer.management_ips = ipaddr;
}
if (length(chassis.capability)) {
let cap = [];
for (let c in chassis.capability) {
if (!c.enabled)
continue;
push(cap, c.type);
}
peer.capability = cap;
}
}
if (!length(peer))
continue;
push(lldp, peer);
}
}
catch(e) {
printf("Failed to parse LLDP cli output: %s\n%s\n", e, e.stacktrace[0].context);
}
/* system state */
let sysinfo = ctx.call("system", "info");
state.unit.localtime = sysinfo.localtime;
state.unit.uptime = sysinfo.uptime;
state.unit.load = sysinfo.load;
state.unit.memory.total = sysinfo.memory.total;
state.unit.memory.free = sysinfo.memory.free;
state.unit.memory.cached = sysinfo.memory.cached;
state.unit.memory.buffered = sysinfo.memory.buffered;
for (let l = 0; l < 3; l++)
state.unit.load[l] /= 65535.0;
let thermal = fs.glob('/sys/class/thermal/thermal_zone*/temp');
if (length(thermal) > 0) {
let temps = [];
for (let t in thermal) {
let file = fs.open(t, 'r');
if (!file)
continue;
let temp = +file.read('all');
if (temp > 1000)
temp /= 1000;
file.close();
// skip non-connected thermal zones
if (temp < 200)
push(temps, temp);
}
if (length(temps) > 0) {
let avg = 0;
temps = sort(temps);
for (let t in temps)
avg += t;
avg /= length(temps);
state.unit.temperature = [ avg, temps[-1] ];
}
}
/* cpu load */
let fs = require('fs');
function sum(arr) {
let rv = 0;
for (let val in arr)
rv += +val;
return rv;
}
function cpu_stats() {
let proc = fs.open('/proc/stat', 'r');
let stats;
if (proc) {
let line;
stats = [];
while (line = proc.read('line')) {
let cols = split(replace(trim(line), ' ', ' '), ' ');
if (!wildcard(cols[0], 'cpu*'))
continue;
shift(cols);
push(stats, [ sum(cols), +cols[3] ]);
}
proc.close();
}
return stats;
}
let last;
let file = fs.open('/tmp/cpu_load', 'r');
if (file) {
last = json(file.read('all'));
file.close();
}
let now = cpu_stats();
if (now && last) {
state.unit.cpu_load = [];
for (let i = 0; i < length(now); i++)
//printf('CPU%s %3d\%\n', i ? i : ' ', 100 * (now[i][1] - last[i][1]) / (now[i][0] - last[i][0]));
push(state.unit.cpu_load, 100 - (100 * (now[i][1] - last[i][1]) / (now[i][0] - last[i][0])));
}
file = fs.open('/tmp/cpu_load', 'w');
if (file) {
file.write(now);
file.close();
}
// mapping 5G to S1G(HaLow)
function map5GToS1G() {
// format: 5G channel -> { s1g_channel, s1g_freq, s1g_bw }
let mappingTable = {
// 1MHz bandwidth
'132': { s1g_channel: 1, s1g_freq: 902, s1g_bw: 1 },
'136': { s1g_channel: 3, s1g_freq: 903, s1g_bw: 1 },
'36': { s1g_channel: 5, s1g_freq: 904, s1g_bw: 1 },
'40': { s1g_channel: 7, s1g_freq: 905, s1g_bw: 1 },
'44': { s1g_channel: 9, s1g_freq: 906, s1g_bw: 1 },
'48': { s1g_channel: 11, s1g_freq: 907, s1g_bw: 1 },
'52': { s1g_channel: 13, s1g_freq: 908, s1g_bw: 1 },
'56': { s1g_channel: 15, s1g_freq: 909, s1g_bw: 1 },
'60': { s1g_channel: 17, s1g_freq: 910, s1g_bw: 1 },
'64': { s1g_channel: 19, s1g_freq: 911, s1g_bw: 1 },
'100': { s1g_channel: 21, s1g_freq: 912, s1g_bw: 1 },
'104': { s1g_channel: 23, s1g_freq: 913, s1g_bw: 1 },
'108': { s1g_channel: 25, s1g_freq: 914, s1g_bw: 1 },
'112': { s1g_channel: 27, s1g_freq: 915, s1g_bw: 1 },
'116': { s1g_channel: 29, s1g_freq: 916, s1g_bw: 1 },
'120': { s1g_channel: 31, s1g_freq: 917, s1g_bw: 1 },
'124': { s1g_channel: 33, s1g_freq: 918, s1g_bw: 1 },
'128': { s1g_channel: 35, s1g_freq: 919, s1g_bw: 1 },
'149': { s1g_channel: 37, s1g_freq: 920, s1g_bw: 1 },
'153': { s1g_channel: 39, s1g_freq: 921, s1g_bw: 1 },
'157': { s1g_channel: 41, s1g_freq: 922, s1g_bw: 1 },
'161': { s1g_channel: 43, s1g_freq: 923, s1g_bw: 1 },
'165': { s1g_channel: 45, s1g_freq: 924, s1g_bw: 1 },
'169': { s1g_channel: 47, s1g_freq: 925, s1g_bw: 1 },
'173': { s1g_channel: 49, s1g_freq: 926, s1g_bw: 1 },
'177': { s1g_channel: 51, s1g_freq: 927, s1g_bw: 1 },
// 2MHz bandwidth
'134': { s1g_channel: 2, s1g_freq: 903, s1g_bw: 2 },
'38': { s1g_channel: 6, s1g_freq: 905, s1g_bw: 2 },
'46': { s1g_channel: 10, s1g_freq: 907, s1g_bw: 2 },
'54': { s1g_channel: 14, s1g_freq: 909, s1g_bw: 2 },
'62': { s1g_channel: 18, s1g_freq: 911, s1g_bw: 2 },
'102': { s1g_channel: 22, s1g_freq: 913, s1g_bw: 2 },
'110': { s1g_channel: 26, s1g_freq: 915, s1g_bw: 2 },
'118': { s1g_channel: 30, s1g_freq: 917, s1g_bw: 2 },
'126': { s1g_channel: 34, s1g_freq: 919, s1g_bw: 2 },
'151': { s1g_channel: 38, s1g_freq: 921, s1g_bw: 2 },
'159': { s1g_channel: 42, s1g_freq: 923, s1g_bw: 2 },
'167': { s1g_channel: 46, s1g_freq: 925, s1g_bw: 2 },
'175': { s1g_channel: 50, s1g_freq: 927, s1g_bw: 2 },
// 4MHz bandwidth
'42': { s1g_channel: 8, s1g_freq: 906, s1g_bw: 4 },
'58': { s1g_channel: 16, s1g_freq: 910, s1g_bw: 4 },
'106': { s1g_channel: 24, s1g_freq: 914, s1g_bw: 4 },
'122': { s1g_channel: 32, s1g_freq: 918, s1g_bw: 4 },
'155': { s1g_channel: 40, s1g_freq: 922, s1g_bw: 4 },
'171': { s1g_channel: 48, s1g_freq: 926, s1g_bw: 4 },
// 8MHz bandwidth
'50': { s1g_channel: 12, s1g_freq: 908, s1g_bw: 8 },
'114': { s1g_channel: 28, s1g_freq: 916, s1g_bw: 8 },
'163': { s1g_channel: 44, s1g_freq: 924, s1g_bw: 8 }
};
return mappingTable;
}
/* wifi radios */
for (let radio, data in wifistatus) {
if (!length(data.interfaces))
continue;
let vap = wifiiface[data.interfaces[0].ifname];
if (!length(vap))
continue;
let radio = {};
radio.channel = vap.channel[0];
radio.channels = uniq(vap.channel);
radio.frequency = uniq(vap.frequency);
radio.channel_width = +vap.ch_width;
radio.tx_power = vap.tx_power;
let path = data.config.path;
if (exists(data.config, 'radio'))
path += ':' + uc(data.config.band);
radio.phy = path;
if (wifiphy[path] && wifiphy[path].temperature)
radio.temperature = wifiphy[path].temperature;
radio.band = wifiphy[path].band;
radio.chanUtil = 0;
let radio_band = lc(radio.band[0]);
let _chanUtil = int(fs.readfile('/tmp/chanutil_phy' + radio_band) || 0);
if (_chanUtil > 0)
radio.chanUtil = _chanUtil;
// Check if this is a HaLow device
if (index(radio.band, 'HaLow') != -1) {
// Get mapping table
let mapping_table = map5GToS1G();
let orig_channels = radio.channels;
let orig_frequency = radio.frequency;
// Use the second channel in the list if available
let selected_channel = null;
let selected_channel_idx = -1;
// Check if we have at least two channels in the list
if (length(orig_channels) >= 2) {
// Take the second channel
selected_channel = '' + orig_channels[1]; // Convert to string
selected_channel_idx = 1;
} else {
// Fallback to using the smallest channel if only one channel available
for (let i = 0; i < length(orig_channels); i++) {
let ch = '' + orig_channels[i]; // Convert to string
if (mapping_table[ch] && (selected_channel === null || +ch < +selected_channel)) {
selected_channel = ch;
selected_channel_idx = i;
}
}
}
// If we found a valid channel to map
if (selected_channel !== null && mapping_table[selected_channel]) {
// Map to S1G
let s1g_info = mapping_table[selected_channel];
radio.channel = s1g_info.s1g_channel;
// Set S1G bandwidth
let bandwidth = s1g_info.s1g_bw;
radio.channel_width = bandwidth;
// Calculate lower and upper edge frequencies
// For S1G channels, lower edge = center_freq - bandwidth/2
let center_freq = s1g_info.s1g_freq;
let lower_freq = center_freq - bandwidth / 2;
let upper_freq = center_freq + bandwidth / 2;
// Update frequencies to include both lower and upper edge
radio.frequency = [lower_freq, upper_freq];
// Convert all channels
let s1g_channels = [];
for (let i = 0; i < length(orig_channels); i++) {
let ch = '' + orig_channels[i];
if (mapping_table[ch]) {
push(s1g_channels, mapping_table[ch].s1g_channel);
}
}
if (length(s1g_channels)) {
radio.channels = uniq(s1g_channels);
}
// Update survey frequencies to match the S1G center frequency in MHz
let s1g_center_freq_khz = center_freq;
// Collect survey items
radio.survey = [];
for (let k, v in survey.survey) {
if (v.frequency in orig_frequency) {
// Make a copy of the survey item and update frequency
let survey_item = { ...v };
survey_item.frequency = s1g_center_freq_khz;
push(radio.survey, survey_item);
}
}
}
} else {
// Non-HaLow device, process survey normally
radio.survey = [];
for (let k, v in survey.survey)
if (v.frequency in radio.frequency)
push(radio.survey, v);
}
delete radio.in_use;
push(state.radios, radio);
}
if (!length(state.radios))
delete state.radios;
function iface_add_counters(iface, vlan, port) {
if (!devstats || !devstats[port])
return;
for (let k, vid in devstats[port]) {
if (vid.vid != vlan)
continue;
iface.counters.tx_bytes += vid.tx?.bytes || 0;
iface.counters.tx_packets += vid.tx?.packets || 0;
iface.counters.rx_bytes += vid.rx?.bytes || 0;
iface.counters.rx_packets += vid.rx?.packets || 0;
if (previous.devstats[port] && previous.devstats[port][k]) {
iface.delta_counters = {};
for (let v in [ 'tx_bytes', 'tx_packets', 'rx_bytes', 'rx_packets' ])
iface.delta_counters[v] = +iface.counters[v] - (+previous.devstats[port][k] || 0);
}
}
}
function is_mesh(net, wif) {
if (!net.batman)
return false;
if (wif.mode != 'mesh')
return false;
return true;
}
function get_fingerprint(mac, ports) {
if (finger_config?.mode != 'final')
return null;
if (!fingerprint[mac])
return null;
if (ports)
for (let port in finger_wan)
if (port in ports)
return 0;
if ((time() - finger_state[mac].reported || 0) < (+finger_config.period || 0))
return null;
finger_state[mac].reported = time();
return fingerprint[mac];
}
let idx = 0;
let dyn_vlans = {};
let dyn_vids = [];
if (devices?.up) for (let k, vlan in devices.up['bridge-vlans']) {
if (vlan.id >= 4000)
continue;
let wlan = [];
for (let port in vlan.ports) {
let dev = split(port, '-v');
if (+dev[1] != vlan.id)
continue;
if (!dyn_vlans[dev[0]])
dyn_vlans[dev[0]] = [];
push(dyn_vlans[dev[0]], port);
}
}
/* interfaces */
cursor.load("network");
cursor.foreach("network", "interface", function(d) {
let name = d[".name"];
if (name == "loopback")
return;
if (index(name, "_") >= 0)
return;
if (!d.ucentral_path)
return;
let role = split(name, /[[:digit:]]/)[0];
let vlan = split(name, 'v')[1];
let iface_port;
let iface = { name, location: d.ucentral_path, ipv4:{}, ipv6:{} };
let ipv4leases = [];
push(state.interfaces, iface);
let status = ctx.call(sprintf("network.interface.%s", name) , "status");
if (!length(status))
return;
if (devices && devices[role] && length(devices[role]["bridge-members"]))
iface_ports = devices[role]["bridge-members"];
iface.uptime = status.uptime || 0;
if (length(status["ipv4-address"])) {
let ipv4 = [];
for (let a in status["ipv4-address"])
push(ipv4, sprintf("%s/%d", a.address, a.mask));
iface.ipv4.addresses = ipv4;
if (vsi && name in vsi)
iface.ipv4.dhcp_vsi = vsi[name];
}
if (length(status["ipv6-address"])) {
iface.ipv6.addresses = status["ipv6-address"];
for (let key, addr in iface.ipv6.addresses) {
if (!addr.mask)
continue;
addr.address = sprintf("%s/%s", addr.address, addr.mask);
delete addr.mask;
}
}
if (length(status["dns-server"]))
iface.dns_servers = status["dns-server"];
if (length(status.data) && status.data.ntpserver)
iface.ntp_server = status.data.ntpserver;
if (length(status.data) && status.data.leasetime && status.proto == "dhcp") {
iface.ipv4.leasetime = status.data.leasetime;
iface.ipv4.dhcp_server = status.data.dhcpserver;
}
if (length(ipv6leases) &&
length(ipv6leases.device) &&
length(ipv6leases.device[status.device]) &&
length(ipv6leases.device[status.device].leases)) {
let leases = [];
for (let l in ipv6leases.device[status.device].leases) {
let lease = {};
lease.hostname = l.hostname;
lease.addresses = [];
for (let addr in l["ipv6-addr"])
push(lease.addresses, addr.address);
push(leases, lease);
}
if (length(leases))
iface.ipv6.leases = leases
}
let macs = [];
if (length(topology)) {
let clients = [];
for (let mac, topo in topology) {
if (topo.interface != d[".name"] ||
!length(topo.fdb) ||
(!length(topo["ipv4"]) && !length(topo["ipv6"])))
continue;
let client = {};
if (length(ip4leases[mac]))
push(ipv4leases, ip4leases[mac]);
client.mac = mac;
if (length(topo["ipv4"]))
client.ipv4_addresses = topo["ipv4"];
else if (snoop && snoop[mac])
client.ipv4_addresses = [ snoop[mac] ];
if (length(topo["ipv6"]))
client.ipv6_addresses = topo["ipv6"];
client.ports = [];
for (let k in topo.fdb)
push(client.ports, lookup_port(k));
let fp = get_fingerprint(mac, client.ports);
if (fp)
client.fingerprint = fp;
client.last_seen = topo.last_seen;
if (index(stats.types, 'clients') >= 0) {
push(clients, client);
push(macs, mac);
}
}
if (length(ipv4leases))
iface.ipv4.leases = ipv4leases;
if (length(clients))
iface.clients = clients;
}
if (index(stats.types, 'ssids') >= 0 && length(wifistatus)) {
let ssids = [];
let counter = 0;
for (let radio, data in wifistatus) {
for (let k, vap in data.interfaces) {
if (!length(vap.config) ||
!length(vap.config.network) ||
!wifiiface[vap.ifname])
continue;
let wif = wifiiface[vap.ifname];
if (!(name in vap.config.network) && !is_mesh(d, wif))
continue;
let ssid = {
radio:{"$ref": sprintf("#/radios/%d", counter)},
phy: data.config.path,
band: uc(data.config.band)
};
ssid.location = wireless[vap.section]?.ucentral_path || '';
ssid.ssid = wif.ssid || vap.config.mesh_id;
ssid.mode = wif.mode;
ssid.bssid = wif.bssid;
ssid.frequency = uniq(wif.frequency);
//Collect associations from both regular and WDS STA interfaces
for (let k, v in all_stations) {
let vlan = split(k, '-v');
let sta_match = split(k, '.sta');
// Handle regular VAP interfaces (including VLAN tagged)
if (vlan[0] == vap.ifname) {
if (vlan[1])
for (let k, assoc in v)
assoc.dynamic_vlan = +vlan[1];
ssid.associations = [ ...(ssid.associations || []), ...v ];
}
// Handle WDS STA interfaces (e.g., wlan2.sta1)
else if (sta_match[0] == vap.ifname && sta_match[1]) {
for (let assoc in v) {
assoc.wds_interface = k; // Mark as WDS interface connection
assoc.connection_type = "WDS-STA";
}
ssid.associations = [ ...(ssid.associations || []), ...v ];
}
}
for (let assoc in ssid.associations) {
let fp = get_fingerprint(assoc.station);
if (fp)
assoc.fingerprint = fp;
if (length(ip4leases[assoc.station]))
assoc.ipaddr_v4 = ip4leases[assoc.station];
else if (snoop && snoop[assoc.station]) {
assoc.ipaddr_v4 = snoop[assoc.station];
if (!(assoc.station in macs)) {
if (!iface.clients)
iface.clients = [];
let client = {
"mac": assoc.station,
"ipv4_addresses": [ snoop[assoc.station] ],
"ports": [ vap.ifname ]
};
push(iface.clients, client);
}
}
//Handle delta calculation for both regular and WDS STA interfaces
let delta_iface = vap.ifname;
if (assoc.wds_interface) {
delta_iface = assoc.wds_interface;
}
assoc.delta_counters = stations_deltas(assoc, delta_iface);
}
ssid.iface = vap.ifname;
if (ports && ports[vap.ifname]?.counters) {
ssid.counters = ports[vap.ifname].counters || {};
ssid.delta_counters = ports_deltas(vap.ifname);
}
if (is_mesh(d, wif)) {
ssid.counters = ports['batman_mesh'].counters;
ssid['mesh-path'] = mesh[vap.ifname];
}
if (dyn_vlans[vap.ifname]) {
ssid.vlan_ifaces = [];
for (let vlan in dyn_vlans[vap.ifname]) {
let vid = +split(vlan, '-v')[1];
push(dyn_vids, vid);
push(ssid.vlan_ifaces, { ...(ports[vlan]?.counters || {}), ...{ vid} });
}
}
push(ssids, ssid);
}
counter++;
}
if (length(ssids))
iface.ssids = ssids;
}
if (ports && ports[name]?.counters)
iface.counters = ports[name].counters;
else
iface.counters = {};
if (role == 'up') {
iface.counters.rx_bytes = 0;
iface.counters.tx_bytes = 0;
iface.counters.rx_packets = 0;
iface.counters.tx_packets = 0;
for (let port in iface_ports)
iface_add_counters(iface, vlan, port);
} else {
iface.delta_counters = ports_deltas(name);
}
if (!length(iface.ipv4))
delete iface.ipv4;
if (!length(iface.ipv6))
delete iface.ipv6;
});
dyn_vids = uniq(dyn_vids);
if (length(dyn_vids)) {
state.dynamic_vlans = [];
for (let id in dyn_vids) {
let dyn = {
vid: id,
tx_bytes: 0,
tx_packets: 0,
rx_bytes: 0,
rx_packets: 0
};
for (let k, dev in devstats) {
for (let k, vid in dev) {
if (vid.vid != id)
continue;
dyn.tx_bytes += vid.tx.bytes;
dyn.tx_packets += vid.tx.packets;
dyn.rx_bytes += vid.rx.bytes;
dyn.rx_packets += vid.rx.packets;
}
}
push(state.dynamic_vlans, dyn);
}
}
if (length(poe)) {
state.poe = {};
state.poe.consumption = poe.consumption;
state.poe.ports = [];
for (let k, v in poe.ports) {
let port = {
id: replace(k, 'lan', ''),
status: v.status
};
if (v.consumption)
port.consumption = v.consumption;
push(state.poe.ports, port);
}
}
if (length(gps) && gps.latitude)
state.gps = {
latitude: gps.latitude,
longitude: gps.longitude,
elevation: gps.elevation
};
if (length(captive)) {
let res = {};
let t = time();
for (let c, val in captive) {
res[c] = {
status: val.state ? 'Authenticated' : 'Garden',
idle: val.idle || 0,
time: val.data.connect ? t - val.data.connect : 0,
ip4addr: val.ip4addr || '',
ip6addr: val.ip6addr || '',
packets_ul: val.packets_ul || 0,
bytes_ul: val.bytes_ul || 0,
packets_dl: val.packets_dl || 0,
bytes_dl: val.bytes_dl || 0,
username: val?.data?.radius?.request?.username || '',
};
}
state.captive = res;
}
function sysfs_net(iface, prop) {
let f = fs.open(sprintf("/sys/class/net/%s/%s", iface, prop), "r");
let val = 0;
if (f) {
val = replace(f.read("all"), '\n', '');
f.close();
}
if (val === null)
val = 0;
return val;
}
function link_state_name(name) {
let names = {
'wan': 'upstream',
'lan': 'downstream'
};
return names[name] || name;
}
function get_vlan_id_for_port(sw_port, switch_name) {
let current_vlan;
let swconfig_cmd = "swconfig dev " + switch_name + " show";
let sw_status = fs.popen(swconfig_cmd);
for (let line; (line = sw_status.read("line")) != null; ){
line = trim(line);
let vlan_info = match(line, /^VLAN\s+(\d+):$/);
if (vlan_info) {
current_vlan = vlan_info[1];
continue;
}
let port_info = match(line, /^ports:\s+(.+)$/);
if (port_info && current_vlan) {
let ports = split(port_info[1], /\s+/);
for (let port_num in ports) {
if (port_num == sw_port) {
sw_status.close();
return current_vlan;
}
}
}
}
sw_status.close();
return null;
}
function get_sw_port_status(sw_port, switch_name, prop) {
let speed_regexp, duplex_regexp, val;
let swconfig_cmd = "swconfig dev " + switch_name + " port " + sw_port + " show";
let sw_status = fs.popen(swconfig_cmd);
for (let line; (line = sw_status.read("line")) != null; ){
line = trim(line);
switch (prop) {
case 'carrier':
if (match(line, /link:up/))
val = 1;
else if (match(line, /link:down/))
val = 0;
break;
case 'speed':
speed_regexp = match(line, /speed:([0-9]+)/);
if (speed_regexp)
val = speed_regexp[1];
break;
case 'duplex':
duplex_regexp = match(line, /(full|half)-duplex/);
if (duplex_regexp)
val = duplex_regexp[1];
break;
}
}
sw_status.close();
return val;
}
if (length(capab.network)) {
let link = {};
let lldp_peers = {};
let vlan_id;
for (let name, net in capab.network) {
let link_name = link_state_name(name);
link[link_name] = {};
lldp_peers[link_name] = {};
for (let iface in capab.network[name]) {
let state = {};
let lldp_state = {};
let name = lookup_port(iface);
state.carrier = +sysfs_net(iface, "carrier");
if (state.carrier) {
state.speed = +sysfs_net(iface, "speed");
state.duplex = sysfs_net(iface, "duplex");
}
if (length(capab.switch_ports)) {
for (let eth_port, sw_port_info in capab.switch_ports) {
let sw_iface = split(iface, ":");
if (sw_iface[0] == eth_port) {
vlan_id = get_vlan_id_for_port(sw_iface[1], sw_port_info['name']);
if (vlan_id) {
iface = eth_port + "." + vlan_id;
}
state.carrier = get_sw_port_status(sw_iface[1], sw_port_info['name'], "carrier");
if (state.carrier) {
state.speed = get_sw_port_status(sw_iface[1], sw_port_info['name'], "speed");
state.duplex = get_sw_port_status(sw_iface[1], sw_port_info['name'], "duplex");
}
}
}
}
if (ports && ports[iface]?.counters) {
state.counters = ports[iface].counters;
state.delta_counters = ports_deltas(iface);
}
link[link_name][name] = state;
let lldp_neigh = [];
for (let l in lldp)
if (l.ifname == iface) {
delete l.ifname;
push(lldp_neigh, l);
}
if (length(lldp_neigh))
lldp_peers[link_name][name] = lldp_neigh;
}
}
state["link-state"] = link;
if (index(stats.types, 'lldp') >= 0)
state["lldp-peers"] = lldp_peers;
}
if (ieee8021x)
state.ieee8021x = {};
if (fingerprint) {
switch(finger_config?.mode) {
case 'raw':
state.fingerprint = fingerprint;
break;
case 'final':
fs.writefile('/tmp/finger.state', finger_state);
break;
}
}
state.version = 1;
printf("%.J\n", state);
let msg = {
uuid: cfg.uuid || 1,
serial: cursor.get("ucentral", "config", "serial"),
state
};
if (telemetry) {
ctx.call("ucentral", "telemetry", { "event": "state", "payload": msg });
let f = fs.open("/tmp/ucentral.telemetry", "w");
if (f) {
f.write(msg);
f.close();
}
return;
}
ctx.call("ucentral", "stats", msg);
let f = fs.open("/tmp/ucentral.state", "w");
if (f) {
f.write(msg);
f.close();
}
else {
printf("Unable to open %s for writing: %s", statefile_path, fs.error());
}