#!/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® 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") require("aredn.info") require("aredn.olsr") require("iwinfo") require('luci.jsonc') local html = aredn.html 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 not nixio.fs.stat(cb) then cb = "/sys/kernel/debug/ieee80211/" .. phy .. "/ath10k/chanbw" end 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 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 local routes = {} local links = {} local neighbor = {} local wangateway = {} local ipalias = {} local localhosts = {} local dtd = {} local midcount = {} local xlinkcount = {} local hosts = {} local services = {} local history = {} for i, node in ipairs(aredn.olsr.getOLSRRoutes()) do if node.genmask ~= 0 and node.etx <= 50 then routes[node.destination] = { etx = node.etx } end end -- low memory route reduction local lowmemory = 1024 * tonumber(cursor:get("aredn", "@meshstatus[0]", "lowmem") or 10000) local lowroutes = tonumber(cursor:get("aredn", "@meshstatus[0]", "lowroutes") or 1000) if #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, #list - 1 do routes[list[i].key] = nil end end -- load up arpcache local arpcache = {} arptable(function(a) if a["Flags"] ~= "0x0" and a["HW address"] ~= "00:00:00:00:00:00" then arpcache[a["IP address"]] = a end end) local iwrates for i, node in ipairs(aredn.olsr.getOLSRLinks()) do links[node.remoteIP] = { lq = node.linkQuality, nlq = node.neighborLinkQuality, mbps = "", weight = 65536 / node.lossMultiplier } neighbor[node.remoteIP] = true local mac = arpcache[node.remoteIP] if mac then if not iwrates then iwrates = {} if wifiif ~= "br-nomesh" then local station = {} for line in io.popen("iw " .. wifiif .. " station dump"):lines() do local mac = line:match("^Station (%S+) ") if mac then station = {} iwrates[mac] = station end local txbitrate = line:match("tx bitrate:%s+([%d%.]+) MBit/s") if txbitrate then station.txbitrate = txbitrate end end end end local station = iwrates[mac["HW address"]] if station then links[node.remoteIP].mbps = string.format("%.1f", tonumber(station.txbitrate) / chanbw) 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 aredn.olsr.getHostAsLines(2) 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("^xlink%d+%.") then if not xlinkcount[originator] then xlinkcount[originator] = 1 else xlinkcount[originator] = xlinkcount[originator] + 1 end if links[ip] then links[ip].xlink = 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 aredn.olsr.getServicesAsLines() 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("%.local%.mesh$") 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 local f = io.open("/tmp/node.history") if f then for line in f: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 function ac(a, b) if not a or a == "" then return b elseif not b or b == "" then return a else return a .. "," .. b end end ------------------ -- generate page ------------------ http_header() html.header(node .. " mesh status", false) local automesh = string.find((nixio.getenv("QUERY_STRING") or ""):lower(),"automesh=1") if automesh then html.print("") end html.print([[ ]]) 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("
") html.print("Help    ") if automesh then html.print("") else html.print("") html.print("  ") html.print([[]]) end if nixio.fs.stat("/tmp/dnsmasq.d/supernode.conf") then local ip = read_all("/tmp/dnsmasq.d/supernode.conf"):match("^#(%S+)") if ip then html.print("  ") html.print("") end end html.print("  ") html.print("") 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 -- show local node table html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") if next(localhosts) then local rows = {} for ip, host in pairs(localhosts) do local localpart = host.name:match("([^.]*)%.") if localpart then local tactical = "" if host.tactical ~= "" then tactical = " / " .. host.tactical end local c1 = localpart .. tactical if wangateway[ip] then c1 = c1 .. "   (wan)" end local tbody = "" if services[host.name] then local first = true; for _, v in pairs(services[host.name]) do if first then first = false tbody = tbody .. "" else tbody = tbody .. "" end end else tbody = tbody .. "" end for _, 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 localpart then if aliased then localpart = "" .. localpart .. "" elseif nopropd then localpart = "" .. localpart .. "" end if services[dmzhost] then local first = true for _, v in pairs(services[dmzhost]) do if first then first = false tbody = tbody .. "" else tbody = tbody .. "" end end else tbody = tbody .. "" end end end tbody = tbody .. "" rows[#rows + 1] = { key = host.name, row = tbody } end 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 -- end local node table html.print("
Node NameLAN HostnameService Name
" .. c1 .. "" .. v .. "
" .. v .. "
" .. c1 .. "
" .. localpart .. "" .. v .. "
" .. v .. "
" .. localpart .. "
none
") -- show current neighbors table html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") html.print("") local trackers = {} local l = io.open("/tmp/lqm.info") if l then local lqm = luci.jsonc.parse(l:read("*a")) l:close() for _, tracker in pairs(lqm.trackers) do if tracker.ip then trackers[tracker.ip] = tracker end end end local now = nixio.sysinfo().uptime local metric = true local language = os.getenv("HTTP_ACCEPT_LANGUAGE") or "en-US" if language:match("^..%-US") or language:match("^..%-GB") then metric = false end 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 c1 = "" .. localpart .. tactical .. "" local nodeiface local waniface local lqmstatus if ipmain ~= ip then if links[ip].dtd then nodeiface = "dtd" elseif links[ip].xlink then nodeiface = "xlink" elseif links[ip].tun then nodeiface = "tun" else nodeiface = "?" end end if wangateway[ip] or wangateway[ipmain] then waniface = "wan" end local c2 = "" local c3 = string.format("%.0f%%", math.min(100, math.ceil(100 * link.lq * link.weight))) local c4 = string.format("%.0f%%", math.min(100, math.ceil(100 * link.nlq * link.weight))) local c4b = "" local c4c = "" local c5 = string.format("%s", link.mbps) local c5b = "" local c6 = "" -- lqm info local track = trackers[ip] if track and not (track.hidden or track.blocked) then if not nodeiface and track.type == "RF" then nodeiface = "rf" end if nodeiface == "tun" and track.type == "Wireguard" then nodeiface = "wg" end if track.pending > now then lqmstatus = "pending" elseif track.routable then lqmstatus = "active" else lqmstatus = "idle" end if (track.blocks.signal or track.blocks.distance or track.blocks.quality) and ((track.leaf == "minor" and track.rev_leaf == "major") or (track.leaf == "major" and track.rev_leaf == "minor")) then lqmstatus = ac(lqmstatus, "leaf") end if track.snr then c4b = track.snr if track.rev_snr then c4b = c4b .. "/" .. track.rev_snr end end if track.quality then c4c = track.quality .. "%" end if track.distance then if not metric then local v = track.distance * 0.000621371 if v > 1 then c5b = math.ceil(v) .. " miles" elseif v > 0.5 then c5b = "1 mile" else c5b = "<1 mile" end else c5b = math.ceil(track.distance * 0.001) .. " km" end end end if nodeiface or waniface or lqmstatus then c1 = c1 .. " (" .. ac(ac(nodeiface, waniface), lqmstatus) .. ")" end -- print node services if any local tbody = "" if not neighservices[name] then neighservices[name] = true if services[name] then local first = true for _, v in pairs(services[name]) do if first then first = false tbody = tbody .. "" else tbody = tbody .. "" end end else tbody = tbody .. "" end if host then for _, dmzhost in ipairs(host.hosts) do local localpart = dmzhost:match("(.*)%.local%.mesh") if localpart then if services[dmzhost] then local first = true for _, v in pairs(services[dmzhost]) do if first then first = false tbody = tbody .. "" else tbody = tbody .. "" end end else tbody = tbody .. "" end end end end end tbody = tbody .. "" rows[#rows + 1] = { key = name:lower(), row = tbody } end for _, track in pairs(trackers) do if track.blocked then local name = track.ip if track.hostname then name = track.hostname end local c1 = "" .. name .. "" local c4c = "" local c4b = "" local c5b = "" local type = "" if track.type == "RF" then type = "rf," end if track.blocks.user then c1 = c1 .. " (" .. type .. "blocked user)" elseif track.blocks.dtd then c1 = c1 .. " (" .. type .. "blocked dtd)" elseif track.blocks.distance then c1 = c1 .. " (" .. type .. "blocked distance)" elseif track.blocks.signal then c1 = c1 .. " (" .. type .. "blocked signal)" elseif track.blocks.dup then c1 = c1 .. " (" .. type .. "blocked dup)" elseif track.blocks.quality then if track.ping_quality then if track.tx_quality then if track.tx_quality < track.ping_quality then c1 = c1 .. " (" .. type .. "blocked retries)" else c1 = c1 .. " (" .. type .. "blocked latency)" end else c1 = c1 .. " (" .. type .. "blocked latency)" end else if track.tx_quality then c1 = c1 .. " (" .. type .. "blocked retries)" end end else c1 = c1 .. " (" .. type .. "blocked)" end if track.snr then c4b = track.snr if track.rev_snr then c4b = c4b .. "/" .. track.rev_snr end end if track.quality then c4c = track.quality .. "%" end if track.distance then if true then local v = track.distance * 0.000621371 if v > 1 then c5b = math.ceil(v) .. " miles" elseif v > 0.5 then c5b = "1 mile" end else c5b = math.ceil(track.distance * 0.001) .. " km" end end local tbody = "" rows[#rows + 1] = { key = name:lower(), row = tbody } end end local cn = {} if #rows > 0 then table.sort(rows, function(a,b) return a.key < b.key end) for _, row in ipairs(rows) do cn[row.key] = true html.print(row.row) end -- discard rows = nil else html.print("") end --add previous neighbors 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 if not cn[host:lower()] then cn[host:lower()] = true local row = "" 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 end if hosts[ip] and hosts[ip].hosts then for _, v in ipairs(hosts[ip].hosts) do row = row .. "" end end row = row .. "" rows[#rows + 1] = { key = age, row = row } end end end if #rows > 0 then html.print("") 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 end cn = nil -- end current neighbors table html.print("
Current NeighborsLAN HostnameLQNLQSNRQualityTxMbpsDistanceService Name
" .. c1 .. "" .. c3 .. "" .. c4 .. "" .. c4b .. "" .. c4c .. "" .. c5 .. "" .. c5b .. "" .. v .. "
" .. v .. "
" .. c1 .. "" .. c3 .. "" .. c4 .. "" .. c4b .. "" .. c4c .. "" .. c5 .. "" .. c5b .. "
" .. localpart .. "" .. v .. "
" .. v .. "
" .. localpart .. "
" .. c1 .. "" .. c4b .. "" .. c4c .. "" .. c5b .. "
none
" .. host .. "" if age < 3600 then local val = math.floor(age / 60) if val == 1 then row = row .. "1 minute ago
" .. v .. "
Previous Neighbors
") -- show remote node table html.print("") html.print("") html.print("") 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") if localpart then local tactical = "" if host.tactical then tactical = " / " .. host.tactical end local etx = string.format("%.2f", host.etx) local c1 = "" .. localpart .. tactical .. "" local nodeiface local tuniface local waniface local hidden local mycount = 0 if midcount[ip] then mycount = midcount[ip] if xlinkcount[ip] then mycount = mycount - xlinkcount[ip] nodeiface = "xlink*" .. xlinkcount[ip] end end if dtd[ip] then mycount = mycount - 1 end if hosts[ip].tactical then mycount = mycount - 1 end if mycount > 0 then tuniface = "tun*" .. mycount end if wangateway[ip] then waniface = "wan" end local track = trackers[ip] if track and track.hidden then hidden = "hidden" end if nodeiface or waniface then c1 = c1 .. " (" .. ac(ac(ac(nodeiface, tuniface), waniface), hidden) .. ")" end local c3 = string.format("%s", etx) local tbody = "" if services[host.name] then local first = true for _, v in pairs(services[host.name]) do if first then first = false tbody = tbody .. "" else tbody = tbody .. "" end end else tbody = tbody .. "" end for _, dmzhost in ipairs(host.hosts) do local localpart = dmzhost:match("(.*)%.local%.mesh") if localpart then if services[dmzhost] then local first = true for _, v in pairs(services[dmzhost]) do if first then first = false tbody = tbody .. "" else tbody = tbody .. "" end end else tbody = tbody .. "" end end end tbody = tbody .. "" rows[#rows + 1] = { key = string.format("%05d/%s", math.floor(100 * host.etx), host.name:lower()), row = tbody } end 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 else html.print("") end -- discard rows = nil neighbor = nil dtd = nil midcount = nil xlinkcount = nil wangateway = nil services = nil trackers = nil links = nil ipalias = nil hosts = nil history = nil -- end remote nodes table html.print("
Remote NodesLAN HostnameETXService Name
" .. c1 .. "" .. c3 .. "" .. v .. "
" .. v .. "
" .. c1 .. "" .. c3 .. "
" .. localpart .. "" .. v .. "
" .. v .. "
" .. localpart .. "
none
") html.print("") html.print("") html.footer(); html.print("") html.print("")