2023-06-20 02:27:23 -06:00
|
|
|
--[[
|
|
|
|
|
|
|
|
Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
|
|
|
|
Copyright (C) 2023 Tim Wilkinson
|
|
|
|
See Contributors file for additional contributors
|
|
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation version 3 of the License.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
Additional Terms:
|
|
|
|
|
|
|
|
Additional use restrictions exist on the AREDN(TM) trademark and logo.
|
|
|
|
See AREDNLicense.txt for more info.
|
|
|
|
|
|
|
|
Attributions to the AREDN Project must be retained in the source code.
|
|
|
|
If importing this code into a new or existing project attribution
|
|
|
|
to the AREDN project must be added to the source code.
|
|
|
|
|
|
|
|
You must not misrepresent the origin of the material contained within.
|
|
|
|
|
|
|
|
Modified versions must be modified to attribute to the original source
|
|
|
|
and be marked in reasonable ways as differentiate it from the original
|
|
|
|
version
|
|
|
|
|
|
|
|
--]]
|
|
|
|
|
|
|
|
local ip = require("luci.ip")
|
|
|
|
|
|
|
|
local IW = "/usr/sbin/iw"
|
|
|
|
local ARPING = "/usr/sbin/arping"
|
|
|
|
|
|
|
|
local M = {}
|
|
|
|
|
|
|
|
local wifi
|
|
|
|
local phy
|
|
|
|
local chipset
|
|
|
|
local frequency
|
|
|
|
local ssid
|
|
|
|
|
|
|
|
local action_limits = {
|
|
|
|
unresponsive_report = 3,
|
|
|
|
unresponsive_trigger1 = 5,
|
|
|
|
unresponsive_trigger2 = 10,
|
|
|
|
zero_trigger1 = 10 * 60, -- 10 minutes
|
|
|
|
zero_trigger2 = 30 * 60 -- 30 minutes
|
|
|
|
}
|
|
|
|
-- Start action state assuming the node is active and no actions are pending
|
|
|
|
local action_state = {
|
2023-06-20 21:51:18 -06:00
|
|
|
scan1 = true,
|
|
|
|
scan2 = true,
|
|
|
|
rejoin1 = true,
|
|
|
|
rejoin2 = true
|
2023-06-20 02:27:23 -06:00
|
|
|
}
|
|
|
|
local unresponsive = {
|
|
|
|
max = 0,
|
|
|
|
ignore = 15,
|
|
|
|
stations = {}
|
|
|
|
}
|
|
|
|
local station_count = {
|
|
|
|
first_zero = 0,
|
|
|
|
first_nonzero = 0,
|
|
|
|
last_zero = 0,
|
|
|
|
last_nonzero = 0,
|
|
|
|
history = {},
|
|
|
|
history_limit = 120 -- 2 hours
|
|
|
|
}
|
|
|
|
|
|
|
|
-- Detect Mikrotik AC which requires special handling
|
|
|
|
local mikrotik_ac = false
|
|
|
|
local boardid = aredn.hardware.get_board_id():lower()
|
|
|
|
if boardid:match("mikrotik") and boardid:match("ac") then
|
|
|
|
mikrotik_ac = true
|
|
|
|
end
|
|
|
|
|
|
|
|
local logfile = "/tmp/wireless_monitor.log"
|
|
|
|
if not file_exists(logfile) then
|
|
|
|
io.open(logfile, "w+"):close()
|
|
|
|
end
|
|
|
|
local log = aredn.log.open(logfile, 8000)
|
|
|
|
|
|
|
|
-- Various forms of network resets --
|
|
|
|
|
|
|
|
function M.reset_network(mode)
|
|
|
|
log:write("reset_network: " .. mode)
|
|
|
|
if mode == "rejoin" then
|
|
|
|
-- Only observered on Mikrotik AC devices
|
|
|
|
if mikrotik_ac then
|
|
|
|
os.execute(IW .. " " .. wifi .. " ibss leave > /dev/null 2>&1")
|
|
|
|
os.execute(IW .. " " .. wifi .. " ibss join " .. ssid .. " " .. frequency .. " fixed-freq > /dev/null 2>&1")
|
|
|
|
else
|
|
|
|
log:write("-- ignoring (mikrotik ac only)")
|
|
|
|
end
|
|
|
|
elseif mode == "scan-quick" then
|
2023-06-21 14:57:21 -06:00
|
|
|
os.execute(IW .. " " .. wifi .. " scan freq " .. frequency .. " > /dev/null 2>&1")
|
2023-06-20 02:27:23 -06:00
|
|
|
elseif mode == "scan-all" then
|
2023-06-21 14:57:21 -06:00
|
|
|
os.execute(IW .. " " .. wifi .. " scan > /dev/null 2>&1")
|
|
|
|
os.execute(IW .. " " .. wifi .. " scan passive > /dev/null 2>&1")
|
2023-06-20 02:27:23 -06:00
|
|
|
else
|
|
|
|
log:write("-- unknown")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Monitor stations and detect if they become unresponsive --
|
|
|
|
|
|
|
|
function M.monitor_unresponsive_stations()
|
|
|
|
|
|
|
|
local old = unresponsive.stations
|
|
|
|
unresponsive.stations = {}
|
|
|
|
unresponsive.max = 0
|
|
|
|
|
|
|
|
local now = nixio.sysinfo().uptime
|
2023-06-25 00:37:48 -06:00
|
|
|
local arp = {}
|
2023-06-20 02:27:23 -06:00
|
|
|
arptable(
|
|
|
|
function (entry)
|
2023-06-25 00:37:48 -06:00
|
|
|
if entry.Device == wifi and entry["Flags"] ~= "0x0" then
|
2023-06-20 02:27:23 -06:00
|
|
|
local ipaddr = entry["IP address"]
|
2023-06-25 00:37:48 -06:00
|
|
|
local mac = entry["HW address"]
|
|
|
|
if mac and ipaddr then
|
|
|
|
arp[mac:upper()] = ipaddr
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)
|
|
|
|
for mac, _ in pairs(iwinfo.nl80211.assoclist(wifi))
|
|
|
|
do
|
|
|
|
local ipaddr = arp[mac:upper()]
|
|
|
|
if ipaddr then
|
|
|
|
unresponsive.stations[ipaddr] = -1
|
|
|
|
local rt = ip.route(ipaddr)
|
|
|
|
if rt and tostring(rt.gw) == ipaddr then
|
|
|
|
unresponsive.stations[ipaddr] = 0
|
|
|
|
-- The first ping is broadcast, the rest unicast
|
|
|
|
for line in io.popen(ARPING .. " -w 5 -I " .. wifi .. " " .. ipaddr):lines()
|
|
|
|
do
|
|
|
|
-- If we see exactly one response then broadcast works and unicast doesnt.
|
|
|
|
-- We neeed to force the station to reassociate
|
|
|
|
if line:match("^Received 1 response") then
|
|
|
|
local val = (old[ipaddr] or 0) + 1
|
|
|
|
unresponsive.stations[ipaddr] = val
|
|
|
|
if val < unresponsive.ignore then
|
|
|
|
if val > action_limits.unresponsive_report then
|
|
|
|
log:write("Possible unresponsive node: " .. ipaddr .. " [" .. mac .. "]")
|
|
|
|
end
|
|
|
|
if val > unresponsive.max then
|
|
|
|
unresponsive.max = val
|
2023-06-20 02:27:23 -06:00
|
|
|
end
|
|
|
|
end
|
2023-06-25 00:37:48 -06:00
|
|
|
break
|
2023-06-20 02:27:23 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2023-06-25 00:37:48 -06:00
|
|
|
end
|
2023-06-20 02:27:23 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
-- Monitor number of connected stations --
|
|
|
|
|
|
|
|
function M.monitor_station_count()
|
|
|
|
local count = 0
|
|
|
|
for mac, station in pairs(iwinfo.nl80211.assoclist(wifi))
|
|
|
|
do
|
|
|
|
count = count + 1
|
|
|
|
end
|
|
|
|
table.insert(station_count.history, 1, count)
|
|
|
|
while #station_count.history > station_count.history_limit
|
|
|
|
do
|
|
|
|
station_count.history[#station_count.history] = nil
|
|
|
|
end
|
|
|
|
local now = nixio.sysinfo().uptime
|
|
|
|
if count == 0 then
|
|
|
|
station_count.last_zero = now
|
|
|
|
if station_count.first_zero <= station_count.first_nonzero then
|
|
|
|
station_count.first_zero = now
|
|
|
|
end
|
|
|
|
else
|
|
|
|
station_count.last_nonzero = now
|
|
|
|
if station_count.first_nonzero <= station_count.first_zero then
|
|
|
|
station_count.first_nonzero = now
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Take action depending on the monitor state
|
|
|
|
|
|
|
|
function M.run_actions()
|
|
|
|
-- No action if we have stations and they're responsive
|
|
|
|
if station_count.last_nonzero > station_count.last_zero and unresponsive.max < action_limits.unresponsive_trigger1 then
|
2023-06-20 21:51:18 -06:00
|
|
|
for k, _ in pairs(action_state)
|
|
|
|
do
|
|
|
|
action_state[k] = false
|
|
|
|
end
|
2023-06-20 02:27:23 -06:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Otherwise ...
|
|
|
|
|
|
|
|
-- If network stations falls to zero when it was previously non-zero
|
|
|
|
if station_count.first_zero > station_count.first_nonzero then
|
2023-06-20 21:51:18 -06:00
|
|
|
if not action_state.scan1 and station_count.last_zero - station_count.first_zero > action_limits.zero_trigger1 then
|
2023-06-20 02:27:23 -06:00
|
|
|
M.reset_network("scan-quick")
|
2023-06-20 21:51:18 -06:00
|
|
|
action_state.scan1 = true
|
2023-06-20 02:27:23 -06:00
|
|
|
return
|
2023-06-20 21:51:18 -06:00
|
|
|
elseif not action_state.scan2 and station_count.last_zero - station_count.first_zero > action_limits.zero_trigger2 then
|
2023-06-20 02:27:23 -06:00
|
|
|
M.reset_network("scan-all")
|
2023-06-20 21:51:18 -06:00
|
|
|
action_state.scan2 = true
|
2023-06-20 02:27:23 -06:00
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- We are failing to ping stations we are associated with
|
2023-06-20 21:51:18 -06:00
|
|
|
if unresponsive.max >= action_limits.unresponsive_trigger1 and not action_state.rejoin1 then
|
2023-06-20 02:27:23 -06:00
|
|
|
M.reset_network("rejoin")
|
2023-06-20 21:51:18 -06:00
|
|
|
action_state.rejoin1 = true
|
2023-06-20 02:27:23 -06:00
|
|
|
return
|
2023-06-20 21:51:18 -06:00
|
|
|
elseif unresponsive.max >= action_limits.unresponsive_trigger2 and not action_state.rejoin2 then
|
2023-06-20 02:27:23 -06:00
|
|
|
M.reset_network("rejoin")
|
2023-06-20 21:51:18 -06:00
|
|
|
action_state.rejoin2 = true
|
2023-06-20 02:27:23 -06:00
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function M.run_monitors()
|
|
|
|
M.monitor_unresponsive_stations()
|
|
|
|
M.monitor_station_count()
|
|
|
|
end
|
|
|
|
|
|
|
|
function M.save()
|
|
|
|
local f = io.open("/tmp/wireless_monitor.info", "w")
|
|
|
|
if f then
|
|
|
|
f:write(luci.jsonc.stringify({
|
|
|
|
now = nixio.sysinfo().uptime,
|
|
|
|
unresponsive = unresponsive,
|
|
|
|
station_count = station_count,
|
|
|
|
action_state = action_state
|
|
|
|
}, true))
|
|
|
|
f:close()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
function M.start_monitor()
|
|
|
|
if not string.match(get_ifname("wifi"), "^wlan") then
|
|
|
|
exit_app()
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2023-06-20 21:51:18 -06:00
|
|
|
-- No stations when we start
|
|
|
|
local now = nixio.sysinfo().uptime
|
|
|
|
station_count.first_zero = now
|
|
|
|
station_count.last_zero = now
|
|
|
|
|
2023-06-20 02:27:23 -06:00
|
|
|
wait_for_ticks(math.max(1, 120 - nixio.sysinfo().uptime))
|
|
|
|
|
|
|
|
-- Extract all the necessary wifi parameters
|
|
|
|
wifi = get_ifname("wifi")
|
|
|
|
phy = iwinfo.nl80211.phyname(wifi)
|
|
|
|
frequency = iwinfo.nl80211.frequency(wifi)
|
|
|
|
ssid = iwinfo.nl80211.ssid(wifi)
|
|
|
|
if not (phy and frequency and ssid) then
|
2023-06-20 21:51:18 -06:00
|
|
|
log:write("Startup failed")
|
|
|
|
log:flush()
|
2023-06-20 02:27:23 -06:00
|
|
|
exit_app()
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
-- Select chipset
|
|
|
|
if nixio.fs.stat("/sys/kernel/debug/ieee80211/" .. phy .. "/ath9k") then
|
|
|
|
chipset = "ath9k"
|
|
|
|
elseif nixio.fs.stat("/sys/kernel/debug/ieee80211/" .. phy .. "/ath10k") then
|
|
|
|
chipset = "ath10k"
|
|
|
|
else
|
|
|
|
exit_app()
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
log:write("Monitoring wireless chipset: " .. chipset)
|
|
|
|
|
|
|
|
M.reset_network("rejoin")
|
|
|
|
|
|
|
|
while true
|
|
|
|
do
|
|
|
|
M.run_monitors()
|
|
|
|
M.run_actions()
|
|
|
|
M.save()
|
|
|
|
log:flush()
|
|
|
|
wait_for_ticks(60) -- 1 minute
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return M.start_monitor
|