mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +08:00
🔥 Sync 2026-10-30 21:24:21
This commit is contained in:
1 parent
c0bd3e8501
commit
b7ebf9532e
2 files changed
+240
-1
No files matched your search
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
LUCI_TITLE:=LuCI Status Pages
|
LUCI_TITLE:=LuCI Status Pages
|
||||||
LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
|
LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
|
||||||
|
|
||||||
PKG_RELEASE:=38
|
PKG_RELEASE:=39
|
||||||
PKG_BUILD_DEPENDS:=iwinfo
|
PKG_BUILD_DEPENDS:=iwinfo
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
-- Shared helpers for luci-app-netstat.
|
||||||
|
--
|
||||||
|
-- Everything here relies only on what every OpenWrt router has: the vnStat 2
|
||||||
|
-- CLI, procfs/sysfs and netifd over ubus. There are no board-specific
|
||||||
|
-- interface names or device paths; the uplink is found from the routing
|
||||||
|
-- table and history comes from whatever vnStat has in its database.
|
||||||
|
|
||||||
|
local nixio = require "nixio"
|
||||||
|
local fs = require "nixio.fs"
|
||||||
|
local util = require "luci.util"
|
||||||
|
local jsonc = require "luci.jsonc"
|
||||||
|
local uci = require "luci.model.uci"
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
M.VNSTAT = "/usr/bin/vnstat"
|
||||||
|
|
||||||
|
-- Netdev and logical interface names are used in shell commands, so only
|
||||||
|
-- allow the characters Linux and netifd actually use.
|
||||||
|
function M.safe_name(name)
|
||||||
|
return type(name) == "string" and name ~= "" and name:match("^[%w%-%._]+$") ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.netdev_exists(name)
|
||||||
|
return M.safe_name(name) and fs.access("/sys/class/net/" .. name) and true or false
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.vnstat_installed()
|
||||||
|
return fs.access(M.VNSTAT, "x") and true or false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Interfaces present in the vnStat database, one per line from
|
||||||
|
-- "vnstat --dbiflist 1". Returns a sorted list and a lookup set.
|
||||||
|
function M.db_interfaces()
|
||||||
|
local list, set = {}, {}
|
||||||
|
if not M.vnstat_installed() then return list, set end
|
||||||
|
local out = util.exec(M.VNSTAT .. " --dbiflist 1 2>/dev/null") or ""
|
||||||
|
for name in out:gmatch("[^\n]+") do
|
||||||
|
name = name:match("^%s*(.-)%s*$")
|
||||||
|
if M.safe_name(name) and not set[name] then
|
||||||
|
set[name] = true
|
||||||
|
list[#list + 1] = name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(list)
|
||||||
|
return list, set
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Run "vnstat --json <mode> <limit> [-i iface]" and return the raw text if it
|
||||||
|
-- looks like JSON, or nil (vnStat missing, interface not in the database, ...).
|
||||||
|
function M.vnstat_json_raw(mode, limit, iface)
|
||||||
|
if not M.vnstat_installed() then return nil end
|
||||||
|
local cmd = M.VNSTAT .. " --json " .. mode .. " " .. tostring(tonumber(limit) or 0)
|
||||||
|
if iface then
|
||||||
|
if not M.safe_name(iface) then return nil end
|
||||||
|
cmd = cmd .. " -i " .. iface
|
||||||
|
end
|
||||||
|
local out = util.exec(cmd .. " 2>/dev/null") or ""
|
||||||
|
if not out:match("^%s*{") then return nil end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Devices that carry a default route, lowest metric first (IPv4, then IPv6).
|
||||||
|
-- Read straight from the kernel so it works whatever the uplink is called
|
||||||
|
-- (pppoe-wan, wwan0, eth1, wg0, ...).
|
||||||
|
function M.default_route_devices()
|
||||||
|
local found, seen = {}, {}
|
||||||
|
local function add(dev, metric)
|
||||||
|
if dev and dev ~= "lo" and not seen[dev] then
|
||||||
|
seen[dev] = true
|
||||||
|
found[#found + 1] = { dev = dev, metric = metric or 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local f = io.open("/proc/net/route", "r")
|
||||||
|
if f then
|
||||||
|
local v4 = {}
|
||||||
|
for line in f:lines() do
|
||||||
|
local fld = {}
|
||||||
|
for w in line:gmatch("%S+") do fld[#fld + 1] = w end
|
||||||
|
-- Iface Destination Gateway Flags RefCnt Use Metric Mask ...
|
||||||
|
if fld[2] == "00000000" and fld[8] == "00000000" then
|
||||||
|
v4[#v4 + 1] = { dev = fld[1], metric = tonumber(fld[7]) or 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
f:close()
|
||||||
|
table.sort(v4, function(a, b) return a.metric < b.metric end)
|
||||||
|
for _, r in ipairs(v4) do add(r.dev, r.metric) end
|
||||||
|
end
|
||||||
|
|
||||||
|
f = io.open("/proc/net/ipv6_route", "r")
|
||||||
|
if f then
|
||||||
|
local v6 = {}
|
||||||
|
for line in f:lines() do
|
||||||
|
local fld = {}
|
||||||
|
for w in line:gmatch("%S+") do fld[#fld + 1] = w end
|
||||||
|
-- dest prefixlen src srcprefixlen nexthop metric refcnt use flags dev
|
||||||
|
if fld[1] == "00000000000000000000000000000000" and fld[2] == "00" and fld[10] then
|
||||||
|
v6[#v6 + 1] = { dev = fld[10], metric = tonumber(fld[6], 16) or 0 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
f:close()
|
||||||
|
table.sort(v6, function(a, b) return a.metric < b.metric end)
|
||||||
|
for _, r in ipairs(v6) do add(r.dev, r.metric) end
|
||||||
|
end
|
||||||
|
|
||||||
|
local devs = {}
|
||||||
|
for _, r in ipairs(found) do devs[#devs + 1] = r.dev end
|
||||||
|
return devs
|
||||||
|
end
|
||||||
|
|
||||||
|
-- With a "known" set, a device is only accepted if it is in that set (e.g.
|
||||||
|
-- the vnStat database); otherwise it has to exist in /sys/class/net.
|
||||||
|
local function accept(dev, known)
|
||||||
|
if not M.safe_name(dev) then return nil end
|
||||||
|
if known then return known[dev] and dev or nil end
|
||||||
|
return M.netdev_exists(dev) and dev or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Accepts a logical netifd interface name ("wan", "modem") or a netdev name
|
||||||
|
-- ("eth1", "wwan0") and returns the matching netdev, or nil. Logical names
|
||||||
|
-- win: on DSA routers the WAN port netdev is often also called "wan", but
|
||||||
|
-- the logical interface's L3 device (e.g. pppoe-wan) is the real uplink.
|
||||||
|
function M.resolve_device(name, known)
|
||||||
|
if not M.safe_name(name) then return nil end
|
||||||
|
local st = util.ubus("network.interface." .. name, "status", {})
|
||||||
|
if type(st) == "table" then
|
||||||
|
local dev = accept(st.l3_device, known) or accept(st.device, known)
|
||||||
|
if dev then return dev end
|
||||||
|
end
|
||||||
|
return accept(name, known)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.preferred_name()
|
||||||
|
local c = uci.cursor()
|
||||||
|
return c:get_first("netstats", "config", "prefer") or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The device to show: the "Preferred Interface" setting if it resolves,
|
||||||
|
-- otherwise the default-route device.
|
||||||
|
function M.wan_device(known)
|
||||||
|
local dev = M.resolve_device(M.preferred_name(), known)
|
||||||
|
if dev then return dev end
|
||||||
|
for _, d in ipairs(M.default_route_devices()) do
|
||||||
|
dev = accept(d, known)
|
||||||
|
if dev then return dev end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function first_addr(list)
|
||||||
|
if type(list) == "table" and type(list[1]) == "table" then
|
||||||
|
return list[1].address
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Local address of a device: netifd's view first, then the kernel's.
|
||||||
|
function M.device_ipaddr(dev)
|
||||||
|
if not M.safe_name(dev) then return nil end
|
||||||
|
|
||||||
|
local dump = util.ubus("network.interface", "dump", {})
|
||||||
|
if type(dump) == "table" and type(dump.interface) == "table" then
|
||||||
|
for _, key in ipairs({ "ipv4-address", "ipv6-address" }) do
|
||||||
|
for _, ifc in ipairs(dump.interface) do
|
||||||
|
if ifc.up and ifc.l3_device == dev then
|
||||||
|
local a = first_addr(ifc[key])
|
||||||
|
if a then return a end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local addrs = nixio.getifaddrs() or {}
|
||||||
|
for _, fam in ipairs({ "inet", "inet6" }) do
|
||||||
|
for _, a in ipairs(addrs) do
|
||||||
|
if a.name == dev and a.family == fam and a.addr
|
||||||
|
and not a.addr:match("^[Ff][Ee]80:") then
|
||||||
|
return a.addr
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This month's rx/tx bytes for a device from the vnStat database, cached in
|
||||||
|
-- /tmp for a minute because the status widget polls every two seconds and
|
||||||
|
-- vnStat only writes to its database every few minutes anyway.
|
||||||
|
local CACHE = "/tmp/luci-netstat-vnstat-month"
|
||||||
|
|
||||||
|
function M.vnstat_month_total(dev)
|
||||||
|
if not M.safe_name(dev) then return nil end
|
||||||
|
|
||||||
|
local now = os.time()
|
||||||
|
local mtime = fs.stat(CACHE, "mtime")
|
||||||
|
if mtime and now - mtime < 60 and now >= mtime then
|
||||||
|
local c = fs.readfile(CACHE) or ""
|
||||||
|
local cdev, rx, tx = c:match("^(%S+) (%d+) (%d+)")
|
||||||
|
if cdev == dev then
|
||||||
|
return { rx = tonumber(rx), tx = tonumber(tx) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local raw = M.vnstat_json_raw("m", 1, dev)
|
||||||
|
local data = raw and jsonc.parse(raw)
|
||||||
|
if type(data) ~= "table" or type(data.interfaces) ~= "table" then return nil end
|
||||||
|
local ifc = data.interfaces[1]
|
||||||
|
if type(ifc) ~= "table" or type(ifc.traffic) ~= "table" then return nil end
|
||||||
|
|
||||||
|
-- jsonversion 1 (vnStat 1.x) used plural keys and KiB units.
|
||||||
|
local legacy = tostring(data.jsonversion) == "1"
|
||||||
|
local months = ifc.traffic[legacy and "months" or "month"]
|
||||||
|
local scale = legacy and 1024 or 1
|
||||||
|
|
||||||
|
local rx, tx = 0, 0
|
||||||
|
local last = type(months) == "table" and months[#months]
|
||||||
|
if type(last) == "table" and type(last.date) == "table" then
|
||||||
|
local t = os.date("*t", now)
|
||||||
|
if tonumber(last.date.year) == t.year and tonumber(last.date.month) == t.month then
|
||||||
|
rx = (tonumber(last.rx) or 0) * scale
|
||||||
|
tx = (tonumber(last.tx) or 0) * scale
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
fs.writefile(CACHE, string.format("%s %.0f %.0f\n", dev, rx, tx))
|
||||||
|
return { rx = rx, tx = tx }
|
||||||
|
end
|
||||||
|
|
||||||
|
-- DatabaseDir from /etc/vnstat.conf, or vnStat's built-in default.
|
||||||
|
function M.vnstat_dbdir()
|
||||||
|
local conf = fs.readfile("/etc/vnstat.conf") or ""
|
||||||
|
for line in conf:gmatch("[^\n]+") do
|
||||||
|
local dir = line:match("^%s*DatabaseDir%s+\"([^\"]+)\"")
|
||||||
|
if dir then return dir end
|
||||||
|
end
|
||||||
|
return "/var/lib/vnstat"
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
Reference in new issue
Block a user