diff --git a/files/usr/local/bin/mgr/lqm.lua b/files/usr/local/bin/mgr/lqm.lua index 6d378a86..6a4305b6 100755 --- a/files/usr/local/bin/mgr/lqm.lua +++ b/files/usr/local/bin/mgr/lqm.lua @@ -47,6 +47,10 @@ local tx_quality_run_avg = 0.8 -- tx quality running average local ping_timeout = 1.0 -- timeout before ping gives a qualtiy penalty local dtd_distance = 50 -- distance (meters) after which nodes connected with DtD links are considered different sites +local IPTABLES = "/usr/sbin/iptables" +local IW = "/usr/sbin/iw" +local ARPING = "/usr/sbin/arping" + local myhostname = (info.get_nvram("node") or "localnode"):lower() local now = 0 @@ -91,11 +95,11 @@ function should_block(track) end function should_nonpair_block(track) - return track.blocks.dtd or track.blocks.signal or track.blocks.distance or track.blocks.user or track.blocks.quality + return track.blocks.dtd or track.blocks.signal or track.blocks.distance or track.blocks.user or track.blocks.quality or track.type ~= "RF" end -function only_quality_block(track) - return track.blocked and track.blocks.quality and not ( +function inject_quality_traffic(track) + return track.ip and track.type ~= "DtD" and track.blocked and track.blocks.quality and not ( track.blocks.dtd or track.blocks.signal or track.blocks.distance or track.blocks.user or track.blocks.dup ) end @@ -111,15 +115,29 @@ end function update_block(track) if should_block(track) then track.blocked = true - if os.execute("/usr/sbin/iptables -C input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") ~= 0 then - os.execute("/usr/sbin/iptables -I input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") - return "blocked" + if track.type == "Tunnel" then + if os.execute(IPTABLES .. " -C input_lqm -p udp --destination-port 698 --in-interface " .. track.device .. " -j DROP 2> /dev/null") ~= 0 then + os.execute(IPTABLES .. " -I input_lqm -p udp --destination-port 698 --in-interface " .. track.device .. " -j DROP 2> /dev/null") + return "blocked" + end + else + if os.execute(IPTABLES .. " -C input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") ~= 0 then + os.execute(IPTABLES .. " -I input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") + return "blocked" + end end else track.blocked = false - if os.execute("/usr/sbin/iptables -C input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") == 0 then - os.execute("/usr/sbin/iptables -D input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") - return "unblocked" + if track.type == "Tunnel" then + if os.execute(IPTABLES .. " -C input_lqm -p udp --destination-port 698 --in-interface " .. track.device .. " -j DROP 2> /dev/null") == 0 then + os.execute(IPTABLES .. " -D input_lqm -p udp --destination-port 698 --in-interface " .. track.device .. " -j DROP 2> /dev/null") + return "blocked" + end + else + if os.execute(IPTABLES .. " -C input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") == 0 then + os.execute(IPTABLES .. " -D input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") + return "unblocked" + end end end return "unchanged" @@ -127,7 +145,8 @@ end function force_remove_block(track) track.blocked = false - os.execute("/usr/sbin/iptables -D input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") + os.execute(IPTABLES .. " -D input_lqm -p udp --destination-port 698 -m mac --mac-source " .. track.mac .. " -j DROP 2> /dev/null") + os.execute(IPTABLES .. " -D input_lqm -p udp --destination-port 698 --in-interface " .. track.device .. " -j DROP 2> /dev/null") end -- Distance in meters between two points @@ -147,15 +166,17 @@ local cursor = uci.cursor() -- Get radio local radioname = "radio0" +local radiomode = "none" for i = 0,2 do - if cursor:get("wireless","@wifi-iface[" .. i .. "]", "network") == "wifi" then - radioname = cursor:get("wireless","@wifi-iface[" .. i .. "]", "device") + if cursor:get("wireless", "@wifi-iface[" .. i .. "]", "network") == "wifi" then + radioname = cursor:get("wireless", "@wifi-iface[" .. i .. "]", "device") + radiomode = cursor:get("wireless", "@wifi-iface[" .. i .. "]", "mode") break end end local phy = "phy" .. radioname:match("radio(%d+)") -local wlan = get_ifname("wifi") +local wlan = cursor:get("network", "wifi", "ifname") function lqm() @@ -168,14 +189,14 @@ function lqm() wait_for_ticks(math.max(1, 30 - nixio.sysinfo().uptime)) -- Create filters (cannot create during install as they disappear on reboot) - os.execute("/usr/sbin/iptables -F input_lqm 2> /dev/null") - os.execute("/usr/sbin/iptables -X input_lqm 2> /dev/null") - os.execute("/usr/sbin/iptables -N input_lqm 2> /dev/null") - os.execute("/usr/sbin/iptables -D INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null") - os.execute("/usr/sbin/iptables -I INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null") + os.execute(IPTABLES .. " -F input_lqm 2> /dev/null") + os.execute(IPTABLES .. " -X input_lqm 2> /dev/null") + os.execute(IPTABLES .. " -N input_lqm 2> /dev/null") + os.execute(IPTABLES .. " -D INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null") + os.execute(IPTABLES .. " -I INPUT -j input_lqm -m comment --comment 'block low quality links' 2> /dev/null") -- We dont know any distances yet - os.execute("iw " .. phy .. " set distance auto") + os.execute(IW .. " " .. phy .. " set distance auto") -- Setup a first_run timeout if this is our first every run if cursor:get("aredn", "@lqm[0]", "first_run") == "0" then @@ -185,6 +206,7 @@ function lqm() end local tracker = {} + local dtdlinks = {} while true do now = nixio.sysinfo().uptime @@ -201,47 +223,109 @@ function lqm() end ) - local kv = { - ["signal avg:"] = "signal", - ["tx packets:"] = "tx_packets", - ["tx retries:"] = "tx_retries", - ["tx failed:"] = "tx_fail", - ["tx bitrate:"] = "tx_rate" - } local stations = {} - local station = {} - local noise = iwinfo.nl80211.noise(wlan) or -95 - for line in io.popen("iw " .. wlan .. " station dump"):lines() - do - local mac = line:match("^Station ([0-9a-f:]+) ") - if mac then - station = { - signal = 0, - noise = noise, - } - stations[mac:upper()] = station - else - for k, v in pairs(kv) - do - local val = line:match(k .. "%s*([%d%-]+)") - if val then - station[v] = tonumber(val) + + -- RF + if radiomode == "adhoc" then + local kv = { + ["signal avg:"] = "signal", + ["tx packets:"] = "tx_packets", + ["tx retries:"] = "tx_retries", + ["tx failed:"] = "tx_fail" + } + local station = {} + local noise = iwinfo.nl80211.noise(wlan) or -95 + for line in io.popen(IW .. " " .. wlan .. " station dump"):lines() + do + local mac = line:match("^Station ([0-9a-f:]+) ") + if mac then + station = { + type = "RF", + device = wlan, + mac = mac:upper(), + signal = 0, + noise = noise, + ip = nil + } + local entry = arps[station.mac] + if entry then + station.ip = entry["IP address"] + end + stations[#stations + 1] = station + else + for k, v in pairs(kv) + do + local val = line:match(k .. "%s*([%d%-]+)") + if val then + station[v] = tonumber(val) + end end end end end - for mac, station in pairs(stations) + -- Tunnels + local tunnel = {} + for line in io.popen("ifconfig"):lines() + do + local tun = line:match("^(tun%d+)%s") + if tun then + tunnel = { + type = "Tunnel", + device = tun, + signal = nil, + ip = nil, + mac = nil, + tx_packets = 0, + tx_fail = 0, + tx_retries = 0 + } + stations[#stations + 1] = tunnel + else + local ip = line:match("P-t-P:(%d+%.%d+%.%d+%.%d+)") + if ip then + tunnel.ip = ip + -- Fake a mac from the ip + local a, b, c, d = ip:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") + tunnel.mac = string.format("00:00:%02X:%02X:%02X:%02X", a, b, c, d) + end + local txp, txf = line:match("TX packets:(%d+)%s+errors:(%d+)") + if txp and txf then + tunnel.tx_packets = txp + tunnel.tx_fail = txf + end + end + end + + -- DtD + for mac, entry in pairs(arps) + do + if entry.Device:match("%.2$") and entry["Flags"] ~= "0x0" then + stations[#stations + 1] = { + type = "DtD", + device = entry.Device, + signal = nil, + ip = entry["IP address"], + mac = mac:upper(), + tx_packets = 0, + tx_fail = 0, + tx_retries = 0 + } + end + end + + for _, station in ipairs(stations) do if station.signal ~= 0 then - local snr = station.signal - station.noise - if not tracker[mac] then - tracker[mac] = { + if not tracker[station.mac] then + tracker[station.mac] = { + type = station.type, + device = station.device, firstseen = now, lastseen = now, pending = now + pending_timeout, refresh = 0, - mac = mac, + mac = station.mac, station = nil, ip = nil, hostname = nil, @@ -259,8 +343,6 @@ function lqm() snr = 0, rev_snr = nil, avg_snr = 0, - links = {}, - tx_rate = 0, last_tx = nil, last_tx_total = nil, tx_quality = 100, @@ -268,28 +350,28 @@ function lqm() quality = 100 } end - local track = tracker[mac] + local track = tracker[station.mac] - -- If we have a direct dtd connection to this device, make sure we use that - local entry = arps[mac] - if entry then - track.ip = entry["IP address"] - local a, b, c = mac:match("^(..:..:..:)(..)(:..:..)$") - local dtd = arps[string.format("%s%02x%s", a, tonumber(b, 16) + 1, c):upper()] - if dtd and dtd.Device:match("%.2$") then - track.blocks.dtd = true - end + -- IP and Hostname + if station.ip and station.ip ~= track.ip then + track.ip = station.ip + track.hostname = nil + end + if not track.hostname and track.ip then local hostname = nixio.getnameinfo(track.ip) if hostname then - track.hostname = hostname:lower():match("^(.*)%.local%.mesh$") + track.hostname = hostname:lower():gsub("^dtdlink%.",""):gsub("^mid%d+%.",""):gsub("%.local%.mesh$", "") end end -- Running average SNR - if track.snr == 0 then - track.snr = snr - else - track.snr = math.ceil(snr_run_avg * track.snr + (1 - snr_run_avg) * snr) + if station.type == "RF" then + local snr = station.signal - station.noise + if track.snr == 0 then + track.snr = snr + else + track.snr = math.ceil(snr_run_avg * track.snr + (1 - snr_run_avg) * snr) + end end -- Running average estimate of link quality @@ -307,8 +389,6 @@ function lqm() track.tx_quality = math.min(100, math.max(0, math.ceil(tx_quality_run_avg * track.tx_quality + (1 - tx_quality_run_avg) * tx_quality))) end - track.tx_rate = station.tx_rate - track.lastseen = now end end @@ -323,6 +403,11 @@ function lqm() -- Only refresh remote attributes periodically if track.ip and (now > track.refresh or is_pending(track)) then track.refresh = now + refresh_timeout + + local old_rev_snr = track.rev_snr + track.rev_snr = null + dtdlinks[track.mac] = {} + local info = json.parse(luci.sys.httpget("http://" .. track.ip .. ":8080/cgi-bin/sysinfo.json?link_info=1&lqm=1")) if info then if tonumber(info.lat) and tonumber(info.lon) then @@ -332,20 +417,11 @@ function lqm() track.distance = calc_distance(lat, lon, track.lat, track.lon) end end - local old_rev_snr = track.rev_snr - track.links = {} - -- Note: We cannot assume a missing link means no wifi connection - track.rev_snr = null - if info.lqm and info.lqm.enabled then - for _, rtrack in pairs(info.lqm.info.trackers) - do - if rtrack.hostname then - local hostname = rtrack.hostname:lower():gsub("^dtdlink%.",""):gsub("%.local%.mesh$", "") - track.links[hostname] = { - type = "RF", - snr = rtrack.snr - } - if myhostname == hostname then + if track.type == "RF" then + if info.lqm and info.lqm.enabled then + for _, rtrack in pairs(info.lqm.info.trackers) + do + if myhostname == rtrack.hostname and (not rtrack.type or rtrack.type == "RF") then if not old_rev_snr or not rtrack.snr then track.rev_snr = rtrack.snr else @@ -353,30 +429,22 @@ function lqm() end end end - end - for ip, link in pairs(info.link_info) - do - if link.hostname and link.linkType == "DTD" then - track.links[link.hostname:lower()] = { type = "DTD" } + for ip, link in pairs(info.link_info) + do + if link.hostname and link.linkType == "DTD" then + dtdlinks[track.mac][link.hostname:lower():gsub("^dtdlink%.",""):gsub("%.local%.mesh$", "")] = true + end end - end - elseif info.link_info then - -- If there's no LQM information we fallback on using link information. - for ip, link in pairs(info.link_info) - do - if link.hostname then - local hostname = link.hostname:lower():gsub("^dtdlink%.",""):gsub("%.local%.mesh$", "") - if link.linkType == "DTD" then - track.links[hostname] = { type = link.linkType } - elseif link.linkType == "RF" and link.signal and link.noise then - local snr = link.signal - link.noise - if not track.links[hostname] then - track.links[hostname] = { - type = link.linkType, - snr = snr - } - end - if myhostname == hostname then + elseif info.link_info then + -- If there's no LQM information we fallback on using link information. + for ip, link in pairs(info.link_info) + do + if link.hostname then + local hostname = link.hostname:lower():gsub("^dtdlink%.",""):gsub("%.local%.mesh$", "") + if link.linkType == "DTD" then + dtdlinks[track.mac][hostname] = true + elseif link.linkType == "RF" and link.signal and link.noise and myhostname == hostname then + local snr = link.signal - link.noise if not old_rev_snr then track.rev_snr = snr else @@ -387,10 +455,6 @@ function lqm() end end end - else - -- Clear these if we cannot talk to the other end, so we dont use stale values - track.links = {} - track.rev_snr = nil end end @@ -415,13 +479,30 @@ function lqm() if config.ping_penalty <= 0 then track.ping_quality = 100 elseif should_ping(track) then - -- Make an arp request to the target ip to see if we get a timely reply. By using ARP we avoid any - -- potential routing issues and avoid any firewall blocks on the other end. - -- As the request is broadcast, we avoid any potential distance/scope timing issues as we dont wait for the - -- packet to be acked. The reply will be unicast to us, and our ack to that is unimportant to the latency test. local success = 100 - if os.execute("/usr/sbin/arping -f -w " .. ping_timeout .. " -I " .. wlan .. " " .. track.ip .. " >/dev/null") ~= 0 then - success = 0 + if track.type == "Tunnel" then + -- Tunnels have no MAC, so we can only use IP level pings. + local sigsock = nixio.socket("inet", "dgram") + sigsock:setopt("socket", "bindtodevice", track.device) + sigsock:setopt("socket", "dontroute", 1) + sigsock:setopt("socket", "rcvtimeo", ping_timeout) + -- Must connect or we wont see the error + sigsock:connect(track.ip, 8080) + sigsock:send("") + -- There's no actual UDP server at the other end so recv will either timeout and return 'false' if the link is slow, + -- or will error and return 'nil' if there is a node and it send back an ICMP error quickly (which for our purposes is a positive) + if sigsock:recv(0) == false then + success = 0 + end + sigsock:close() + else + -- Make an arp request to the target ip to see if we get a timely reply. By using ARP we avoid any + -- potential routing issues and avoid any firewall blocks on the other end. + -- As the request is broadcast, we avoid any potential distance/scope timing issues as we dont wait for the + -- packet to be acked. The reply will be unicast to us, and our ack to that is unimportant to the latency test. + if os.execute(ARPING .. " -f -w " .. ping_timeout .. " -I " .. track.device .. " " .. track.ip .. " >/dev/null") ~= 0 then + success = 0 + end end local ping_loss_run_avg = 1 - config.ping_penalty / 100 track.ping_quality = math.ceil(ping_loss_run_avg * track.ping_quality + (1 - ping_loss_run_avg) * success) @@ -435,11 +516,11 @@ function lqm() -- bad, it wont be used and we can never tell if it becomes good again. Beware injecting too -- much traffic because, on very poor links, this can generate multiple retries per packet, flooding -- the wifi channel - if track.ip and only_quality_block(track) then + if inject_quality_traffic(track) then -- Create socket we use to inject traffic into degraded links -- This is setup so it ignores routing and will always send to the correct wifi station local sigsock = nixio.socket("inet", "dgram") - sigsock:setopt("socket", "bindtodevice", wlan) + sigsock:setopt("socket", "bindtodevice", track.device) sigsock:setopt("socket", "dontroute", 1) for _ = 1,quality_injection_max do @@ -498,28 +579,42 @@ function lqm() -- Work out what to block, unblock and limit for _, track in pairs(tracker) do - -- When unblocked link signal becomes too low, block - if not track.blocks.signal then - if track.snr < config.low or (track.rev_snr and track.rev_snr < config.low) then - track.blocks.signal = true - end - -- when blocked link becomes (low+margin) again, unblock - else - if track.snr >= config.low + config.margin and (not track.rev_snr or track.rev_snr >= config.low + config.margin) then - track.blocks.signal = false - -- When signal is good enough to unblock a link but the quality is low, artificially bump - -- it up to give the link chance to recover - if track.blocks.quality then - track.quality = config.min_quality + config.margin_quality - end - end - end + -- SNR and distance blocks only related to RF links + if track.type == "RF" then + + -- If we have a direct dtd connection to this device, make sure we use that + local a, b, c = track.mac:match("^(..:..:..:)(..)(:..:..)$") + local dtd = tracker[string.format("%s%02X%s", a, tonumber(b, 16) + 1, c)] + if dtd and dtd.type == "DtD" then + track.blocks.dtd = true + else + track.blocks.dtd = false + end + + -- When unblocked link signal becomes too low, block + if not track.blocks.signal then + if track.snr < config.low or (track.rev_snr and track.rev_snr < config.low) then + track.blocks.signal = true + end + -- when blocked link becomes (low+margin) again, unblock + else + if track.snr >= config.low + config.margin and (not track.rev_snr or track.rev_snr >= config.low + config.margin) then + track.blocks.signal = false + -- When signal is good enough to unblock a link but the quality is low, artificially bump + -- it up to give the link chance to recover + if track.blocks.quality then + track.quality = config.min_quality + config.margin_quality + end + end + end + + -- Block any nodes which are too distant + if not track.distance or (track.distance >= config.min_distance and track.distance <= config.max_distance) then + track.blocks.distance = false + else + track.blocks.distance = true + end - -- Block any nodes which are too distant - if not track.distance or (track.distance >= config.min_distance and track.distance <= config.max_distance) then - track.blocks.distance = false - else - track.blocks.distance = true end -- Block if user requested it @@ -552,8 +647,7 @@ function lqm() for _, track2 in pairs(tracker) do if track ~= track2 and track2.hostname and not should_nonpair_block(track2) then - local connection = track.links[track2.hostname] - if connection and connection.type == "DTD" then + if dtdlinks[track.mac][track2.hostname] then if not (track.lat and track.lon and track2.lat and track2.lon) or calc_distance(track.lat, track.lon, track2.lat, track2.lon) < dtd_distance then tracklist[#tracklist + 1] = track2 end @@ -600,8 +694,8 @@ function lqm() track.pending = now + pending_timeout end - -- Find the most distant, unblocked, routable, node - if not track.blocked and track.distance then + -- Find the most distant, unblocked, routable, RF node + if track.type == "RF" and not track.blocked and track.distance then if not is_pending(track) and track.routable then if track.distance > distance then distance = track.distance @@ -626,13 +720,13 @@ function lqm() -- Update the wifi distance if distance > 0 then - coverage = math.floor((distance * 2 * 0.0033) / 3) -- airtime - os.execute("iw " .. phy .. " set coverage " .. coverage) + coverage = math.min(255, math.floor((distance * 2 * 0.0033) / 3)) -- airtime + os.execute(IW .. " " .. phy .. " set coverage " .. coverage) elseif alt_distance > 1 then - coverage = math.floor((alt_distance * 2 * 0.0033) / 3) - os.execute("iw " .. phy .. " set coverage " .. coverage) + coverage = math.min(255, math.floor((alt_distance * 2 * 0.0033) / 3)) + os.execute(IW .. " " .. phy .. " set coverage " .. coverage) else - os.execute("iw " .. phy .. " set distance auto") + os.execute(IW .. " " .. phy .. " set distance auto") end -- Save this for the UI diff --git a/files/www/cgi-bin/lqm b/files/www/cgi-bin/lqm index 859e5d03..662844fb 100755 --- a/files/www/cgi-bin/lqm +++ b/files/www/cgi-bin/lqm @@ -76,16 +76,16 @@ html.print([[ #links > div { padding: 2px 0; } - .m { + span { display: inline-block; + } + .m { width: 220px; } .s { - display: inline-block; width: 90px; } .p { - display: inline-block; width: 110px; } @@ -105,7 +105,7 @@ html.print([[
- RF NeighborSNRDistanceQualityStatus   + NeighborLinkSNRDistanceQualityStatus  
@@ -154,6 +154,9 @@ html.print([[
} return "idle"; } + const get_snr = track => { + return track.snr <= 0 ? "-" : track.snr + ("rev_snr" in track ? "/" + track.rev_snr : ""); + } const name = track => { if (track.hostname) { return `${track.hostname}`; @@ -198,13 +201,13 @@ html.print([[
} break; } - links += `
${name(track)}${track.snr}${"rev_snr" in track ? "/" + track.rev_snr : ""}${distance}${quality}${status}
`; + links += `
${name(track)}${track.type}${get_snr(track)}${distance}${quality}${status}
`; }); if (links.length) { document.getElementById("links").innerHTML = links; } else { - document.getElementById("links").innerHTML = `
Currently no RF neighbor data available
` + document.getElementById("links").innerHTML = `
Currently no neighbor data available
` } } const fetchAndUpdate = () => {