#!/usr/bin/lua --[[ Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks Copyright (C) 2021 Tim Wilkinson Original Perl Copyright (C) 2015 Conrad Lara 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 . 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 --]] require("nixio") require("aredn.hardware") require("aredn.http") require("aredn.utils") require("aredn.html") require("uci") aredn.info = require("aredn.info") aredn.olsr = require("aredn.olsr") require("iwinfo") local html = aredn.html local rateL = { ["1.0M"] = 1, -- CCP LP ["2.0M"] = 2, -- CCP LP ["5.5M"] = 5.5, -- CCP LP MCS0 = 6.5, -- HT20 LGI ["11.0M"] = 11, -- CCP LP MCS1 = 13, -- HT20 LGI... MCS2 = 19.5, MCS3 = 26, MCS4 = 39, MCS5 = 52, MCS6 = 58.5, MCS7 = 65, MCS8 = 13, MCS9 = 26, MCS10 = 39, MCS11 = 52, MCS12 = 78, MCS13 = 104, MCS14 = 117, MCS15 = 130 } local rateS = { MCS0 = 7.2, MCS1 = 14.4, MCS2 = 21.7, MCS3 = 28.9, MCS4 = 43.3, MCS5 = 57.8, MCS6 = 65, MCS7 = 72.2, MCS8 = 14.4, MCS9 = 28.9, MCS10 = 43.3, MCS11 = 57.8, MCS12 = 86.7, MCS13 = 115.6, MCS14 = 130, MCS15 = 144.4 } local node = aredn.info.get_nvram("node") if node == "" then node = "NOCALL" end local tactical = aredn.info.get_nvram("tactical") local config = aredn.info.get_nvram("config") if config == "" or nixio.fs.stat("/etc/config.mesh", "type") ~= "dir" then config = "not set" end local wifiif = aredn.hardware.get_iface_name("wifi") local my_ip = aredn.hardware.get_interface_ip4(wifiif) if not my_ip then my_ip = "none" end local phy = iwinfo.nl80211.phyname(wifiif) if not phy then phy = 0 end local chanbw = 1 local cb = "/sys/kernel/debug/ieee80211/" .. phy .. "/ath9k/chanbw" if nixio.fs.stat(cb) then for line in io.lines(cb) do if line == "0x00000005" then chanbw = 4 elseif line == "0x0000000a" then chanbw = 2 end break end end if not nixio.fs.stat("/tmp/web") then nixio.fs.mkdir("/tmp/web") end -- post data if os.getenv("REQUEST_METHOD") == "POST" then require('luci.http') require('luci.sys') local request = luci.http.Request(luci.sys.getenv(), function() local v = io.read(1024) if not v then io.close() end return v end ) if request:formvalue("auto") then io.open("/tmp/web/automesh", "w"):close() end if request:formvalue("stop") then os.remove("/tmp/web/automesh") end end local cursor = uci.cursor() local node_desc = cursor:get("system", "@system[0]", "description") local lat_lon = "Location Not Available" local lat = cursor:get("aredn", "@location[0]", "lat") local lon = cursor:get("aredn", "@location[0]", "lon") if lat and lon then lat_lon = string.format("
Location: %s %s
", lat, lon) end -- low memory mitigation local lowmemory = cursor:get("aredn", "@meshstatus[0]", "lowmem") if not lowmemory then lowmemory = 10000 else lowmemory = tonumber(lowmemory) end local lowroutes = cursor:get("aredn", "@meshstatus[0]", "lowroutes") if not lowroutes then lowroutes = 1000 else lowroutes = tonumber(lowroutes) end local routes = {} local links = {} local neighbor = {} local wangateway = {} local ipalias = {} local localhosts = {} local dtd = {} local midcount = {} local hosts = {} local services = {} local history = {} local olsr_total = 0 local olsr_nodes = 0 local olsr_routes = 0 for i, node in ipairs(aredn.olsr.getOLSRRoutes()) do if node.genmask ~= 0 then -- don't count default route olsr_total = olsr_total + 1 if node.genmask ~= 32 then olsr_nodes = olsr_nodes + 1 end if node.etx <= 50 then routes[node.destination] = { etx = node.etx } olsr_routes = olsr_routes + 1 end end end -- low memory route reduction if olsr_routes > lowroutes and nixio.sysinfo().freeram < lowmemory then local list = {} for k,v in pairs(routes) do list[#list + 1] = { key = k, etx = v.etx } end table.sort(list, function (a, b) return a.etx < b.etx end) for i = lowroutes,olsr_routes do table.remove(routes, list[i].key) end end -- load up arpcache local arpcache = {} arptable(function(a) arpcache[a["IP address"]] = a end) local prefix = "/sys/kernel/debug/ieee80211/" .. phy .. "/netdev:" .. wifiif .. "/stations/" for i, node in ipairs(aredn.olsr.getOLSRLinks()) do links[node.remoteIP] = { lq = node.linkQuality, nlq = node.neighborLinkQuality, mbps = "" } neighbor[node.remoteIP] = true local mac = arpcache[node.remoteIP] if mac then local f = io.open(prefix .. mac["HW address"] .. "/rc_stats_csv", "r") if f then for line in f:lines() do local gi, rate, ewma = line:match("^[^,]*,([^,]*),[^,]*,A[^,]*,([^,%s]*)%s*,[^,]*,[^,]*,[^,]*,[^,]*,([^,]*),") ewma = tonumber(ewma) if gi and ewma then -- 802.11b/n if gi == "SGI" then links[node.remoteIP].mbps = string.format("%.1f", rateS[rate] * ewma / 100 / chanbw) else links[node.remoteIP].mbps = string.format("%.1f", rateL[rate] * ewma / 100 / chanbw) end else rate, ewma = line:match("^A[^,]*,([^,]*),[^,]*,[^,]*,([^,]*,)") rate = tonumber(rate) ewma = tonumber(ewma) if rate and ewma then -- 802.11a/b/g links[node.remoteIP].mbps = string.format("%.1f", rate * ewma / 100 / chanbw) end end end f:close() end end end -- discard arpcache = nil for i, node in ipairs(aredn.olsr.getOLSRHNA()) do if node.destination == "0.0.0.0" then wangateway[node.gateway] = true end end for i, node in ipairs(aredn.olsr.getOLSRMid()) do local ip = node.main.ipAddress for _, alias in ipairs(node.aliases) do local aip = alias.ipAddress ipalias[aip] = ip neighbor[aip] = true if links[aip] then neighbor[ip] = true end end end -- load the local hosts file for line in io.lines("/etc/hosts") do if line:match("^10%.") then local ip, name = line:match("([%d%.]+)%s+(%S+)") if name then local name9 = name:sub(1, 9) if name9 ~= "localhost" and name9 ~= "localnode" then local name7 = name:sub(1, 7) if name7 ~= "localap" and name7 ~= "dtdlink" then if not name:match("%.") then name = name .. ".local.mesh" end local tac = line:match("[%d%.]+%s+%S+%s+(%S+)") if not tac then tac = "" end if not localhosts[my_ip] then localhosts[my_ip] = { hosts = {}, noprops = {}, aliases = {}, name = name, tactical = tac } end local host = localhosts[my_ip] if ip == my_ip then host.tactical = tac host.name = name else host.hosts[#host.hosts + 1] = name end if tac == "#NOPROP" then host.noprops[#host.noprops + 1] = name end if tac == "#ALIAS" then host.aliases[#host.aliases + 1] = name end end end end end end -- load the olsr hosts file for line in io.lines("/var/run/hosts_olsr") do local ip, name, originator = line:match("^([%d%.]+)%s+(%S+)%s+%S+%s+(%S+)") if ip and originator and originator ~= "myself" and (routes[ip] or routes[originator]) then local etx = routes[ip] if not etx then etx = routes[originator] end etx = etx.etx if not name:match("%.") or name:match("^mid%.[^%.]*$") then name = name .. ".local.mesh" end if ip == originator then if not hosts[originator] then hosts[originator] = { hosts = {} } end local host = hosts[originator] if host.name then host.tactical = name else host.name = name host.etx = etx end elseif name:match("^dtdlink%.") then dtd[originator] = true if links[ip] then links[ip].dtd = true end elseif name:match("^mid%d+%.") then if not midcount[originator] then midcount[originator] = 1 else midcount[originator] = midcount[originator] + 1 end if links[ip] then links[ip].tun = true end else if not hosts[originator] then hosts[originator] = { hosts = {} } end local host = hosts[originator] host.hosts[#host.hosts + 1] = name end end end -- discard routes = nil for line in io.lines("/var/run/services_olsr") do if line:match("^%w") then local url, name = line:match("^(.*)|.*|(.*)$") if name then local protocol, host, port, path = url:match("^([%w][%w%+%-%.]+)%://(.+):(%d+)/(.*)") if path then local name, originator = name:match("(.*%S)%s*#(.*)") if originator == " my own service" or (hosts[originator] and hosts[originator].name) then if not host:match("%.") then host = host .. ".local.mesh" end if not services[host] then services[host] = {} end if not services[host][name] then if port ~= "0" then services[host][name] = "" .. name .. "" else services[host][name] = name end end end end end end end -- load the node history if nixio.fs.stat("/tmp/node.history") then for line in io.lines("/tmp/node.history") do local ip, age, host = line:match("^(%S+) (%d+) (%S+)") if ip and age and host then history[ip] = { age = age, host = host:gsub("/", " / ") } end end end http_header() html.header(node .. " mesh status", false) local automesh = nixio.fs.stat("/tmp/web/automesh"); if automesh then html.print("") end html.print("") html.print("
") html.print("") html.print("
") html.alert_banner() html.print("

" .. node .. " mesh status

") html.print(lat_lon) if node_desc then html.print("
" .. node_desc .. "
") end html.print("
") if nixio.fs.stat("/tmp/web/automesh") then html.print("") else html.print("") html.print("  ") html.print("") end html.print("  ") html.print("") html.print("

") if not next(localhosts) and not next(links) then html.print("No other nodes are available.") html.print("
") html.footer() html.print("") os.exit(0) end html.print("
") -- show local hosts html.print("") html.print("") if next(localhosts) then local rows = {} for ip, host in pairs(localhosts) do local localpart = host.name:match("([^.]*)%.") local tactical = "" if host.tactical ~= "" then tactical = " / " .. host.tactical end local row = "" -- add locally advertised dmz hosts for i, dmzhost in ipairs(host.hosts) do local nopropd = false local aliased = false for _, v in ipairs(host.noprops) do if v == dmzhost then nopropd = true; break end end for _, v in ipairs(host.aliases) do if v == dmzhost then aliased = true; break end end local localpart = dmzhost:match("(.*)%.local%.mesh") if not nopropd and not aliased then row = row .. "" elseif aliased then row = row .. "" else row = row .. "" end if services[dmzhost] then row = row .. "" end rows[#rows + 1] = { key = host.name, row = row } end table.sort(rows, function(a,b) return a.key < b.key end) for _, row in ipairs(rows) do html.print(row.row) end -- discard rows = nil else html.print("") end -- discard localhosts = nil -- show remote nodes html.print("") html.print("") html.print("") local rows = {} for ip, host in pairs(hosts) do if not neighbor[ip] and host.name then local localpart = host.name:match("(.*)%.local%.mesh") local tactical = "" if host.tactical then tactical = " / " .. host.tactical end local etx = string.format("%.2f", host.etx) local row = "" -- add locally advertised dmz hosts for _, dmzhost in ipairs(host.hosts) do local localpart = dmzhost:match("(.*)%.local%.mesh") row = row .. "" row = row .. "" end rows[#rows + 1] = { key = host.etx, row = row } end end if #rows > 0 then table.sort(rows, function(a,b) return a.key < b.key end) for _, row in ipairs(rows) do html.print(row.row) end -- discard rows = nil else html.print("") end -- discard neighbor = nil dtd = nil midcount = nil html.print("
Local HostsServices

" .. localpart .. tactical .. "" if wangateway[ip] then row = row .. "   (wan)" end row = row .. " " if services[host.name] then for n, v in pairs(services[host.name]) do row = row .. "" .. v .. "
" end end row = row .. "
 " .. localpart .. "
 " .. localpart .. "
 " .. localpart .." " for n, v in pairs(services[dmzhost]) do row = row .. "" .. v .. "
" end end row = row .. "
none
 
Remote Nodes  ETX  Services

" .. localpart .. tactical .. "" local nodeiface local mycount = 0 if midcount[ip] then mycount = midcount[ip] end if dtd[ip] then mycount = mycount - 1 end if hosts[ip].tactical then mycount = mycount - 1 end if mycount > 0 then nodeiface = "tun*" .. mycount end if wangateway[ip] then if nodeiface then nodeiface = nodeiface .. ",wan" else nodeiface = "wan" end end if nodeiface then row = row .. "   (" .. nodeiface .. ")" end row = row .. "" .. etx .. "" if services[host.name] then for _, v in pairs(services[host.name]) do row = row .. "" .. v .. "
" end end row = row .. "
 " .. localpart .. "" if services[dmzhost] then for _, v in pairs(services[dmzhost]) do row = row .. "" .. v .. "
" end end row = row .. "
none
 ") -- show current neighbors html.print("") html.print("") local rows = {} local neighservices = {} for ip, link in pairs(links) do local ipmain = ipalias[ip] if not ipmain then ipmain = ip end local name = ipmain local localpart = ipmain local tactical = "" local host = hosts[ipmain] if host then if host.name then name = host.name localpart = name:match("(.*)%.local%.mesh") if not localpart then localpart = name end end if host.tactical then tactical = " / " .. host.tactical end end if rows[name] then name = name .. " " -- avoid collision 2 links to same host {rf, dtd} end local no_space_host = name:match("(.*%S)%s*$") local row = "" -- add advertised dmz hosts if host then for _, dmzhost in ipairs(host.hosts) do local localpart = dmzhost:match("(.*)%.local%.mesh") row = row .. "" row = row .. "" end end end rows[#rows + 1] = { key = name, row = row } end if #rows > 0 then table.sort(rows, function(a,b) return a.key < b.key end) for _, row in ipairs(rows) do html.print(row.row) end -- discard rows = nil else html.print("") end -- discard wangateway = nil services = nil -- show previous neighbors html.print("") html.print("") html.print("") local rows = {} local uptime = nixio.sysinfo().uptime for ip, node in pairs(history) do if not (links[ip] or links[ipalias[ip]]) then local age = uptime - tonumber(node.age) local host = node.host if host == "" then host = ip else host = host:gsub("^mid%d+%.", ""):gsub("^dtdlink%.", "") end local row = "" end rows[#rows + 1] = { key = age, row = row } end end if #rows > 0 then table.sort(rows, function(a,b) return a.key < b.key end) for _, row in ipairs(rows) do html.print(row.row) end -- discard rows = nil else html.print("") end -- discard links = nil ipalias = nil hosts = nil history = nil -- footer html.print("") html.print("") html.print("") html.print("") html.print("") html.print("
Current Neighbors  LQNLQTxMbps  Services

" .. localpart .. tactical .. "" local nodeiface if ipmain ~= ip then if links[ip].dtd then nodeiface = "dtd" elseif links[ip].tun then nodeiface = "tun" else nodeiface = "?" end end if wangateway[ip] or wangateway[ipmain] then if nodeiface then nodeiface = nodeiface .. ",wan" else nodeiface = "wan" end end if nodeiface then row = row .. "   (" .. nodeiface .. ")" end row = row .. string.format("%.0f%%%.0f%%%s\n", 100 * link.lq, 100 * link.nlq, link.mbps) if not neighservices[name] then neighservices[name] = true if services[name] then for _, v in pairs(services[name]) do row = row .. "" .. v .. "
" end end row = row .. "
 " .. localpart .. "" if services[dmzhost] then for _, v in pairs(services[dmzhost]) do row = row .. v .. "
" end end row = row .. "
none
 
Previous NeighborsWhen

" .. host .. "" if hosts[ip] and hosts[ip].hosts then for _, v in ipairs(hosts[ip].hosts) do row = row .. "
" .. v .. "" end end row = row .. "
" if age < 3600 then local val = math.floor(age / 60) if val == 1 then row = row .. "1 minute ago" else row = row .. val .. " minutes ago" end else local val = string.format("%.1f", age / 3600) if val == "1.0" then row = row .. "1 hour ago" else row = row .. val .. " hours ago" end row = row .. "
none
 
OLSR Entries

Total " .. olsr_total .. "
Nodes " .. olsr_nodes .. "
") -- end html.print("") html.print("") html.footer(); html.print("") html.print("")