mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 11:53:56 +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_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_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