mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 00:35:13 +08:00
🔥 Sync 2026-08-25 02:47:49
This commit is contained in:
1 parent
354510445a
commit
d6443a937f
13 files changed
+1032
-672
No files matched your search
@@ -206,6 +206,89 @@ function parse_limits(raw)
|
||||
return limits
|
||||
end
|
||||
|
||||
-- Live per-subflow TCP metrics via "ss -tin" (no per-WAN "src <ip>" filter --
|
||||
-- one call for every established connection on the box, then each
|
||||
-- connection's local address is looked up against the endpoints list to
|
||||
-- attribute it to a WAN and pick up its id/backup/fullmesh flags). This
|
||||
-- mirrors the shell/awk field parsing in omr-metrics' 040-metrics
|
||||
-- post-tracking hook, reimplemented natively in Lua here so the Diagnostics
|
||||
-- page stays self-contained: no dependency on omr-metrics being installed,
|
||||
-- and always a live read rather than whatever omr-tracker last cached for a
|
||||
-- WAN that may not have cycled recently.
|
||||
--
|
||||
-- Known limitation: a connection is only attributed to a WAN if its local
|
||||
-- address matches a registered MPTCP endpoint. In practice every
|
||||
-- multipath-enabled interface gets one (multipath's "on"/"backup" hotplug
|
||||
-- path always registers an endpoint for it), so this only misses a WAN that
|
||||
-- somehow never got one -- which "wan_endpoint_status" below already flags
|
||||
-- as its own issue.
|
||||
local function tobps(s)
|
||||
local v = tonumber((s or ""):match("^[%d%.]+")) or 0
|
||||
if s:find("Gbps") then return math.floor(v * 1000000000)
|
||||
elseif s:find("Mbps") then return math.floor(v * 1000000)
|
||||
elseif s:find("[Kk]bps") then return math.floor(v * 1000)
|
||||
end
|
||||
return math.floor(v)
|
||||
end
|
||||
|
||||
-- ss prints "ip:port", or "[v6addr]:port" for IPv6.
|
||||
local function split_hostport(s)
|
||||
local ip, port = (s or ""):match("^(.-):(%d+)$")
|
||||
if not ip then return s, nil end
|
||||
return (ip:gsub("^%[", ""):gsub("%]$", "")), tonumber(port)
|
||||
end
|
||||
|
||||
function parse_ss_subflows(raw, endpoints)
|
||||
local by_addr = {}
|
||||
for _, ep in ipairs(endpoints) do by_addr[ep.address] = ep end
|
||||
|
||||
local subflows = {}
|
||||
local cur
|
||||
for line in (raw or ""):gmatch("[^\n]+") do
|
||||
local state, lhost, rhost = line:match("^(%S+)%s+%d+%s+%d+%s+(%S+)%s+(%S+)")
|
||||
if state == "ESTAB" then
|
||||
if cur then subflows[#subflows + 1] = cur end
|
||||
cur = nil
|
||||
local lip, lport = split_hostport(lhost)
|
||||
local rip, rport = split_hostport(rhost)
|
||||
local ep = by_addr[lip]
|
||||
if ep then
|
||||
cur = {
|
||||
dev = ep.dev, endpoint_id = ep.id, backup = ep.backup,
|
||||
local_ip = lip, local_port = lport,
|
||||
remote_ip = rip, remote_port = rport,
|
||||
}
|
||||
end
|
||||
elseif cur then
|
||||
for token in line:gmatch("%S+") do
|
||||
if token:match("^cwnd:") then cur.cwnd = tonumber(token:match(":(%d+)"))
|
||||
elseif token:match("^ssthresh:") then cur.ssthresh = tonumber(token:match(":(%d+)"))
|
||||
elseif token:match("^rtt:") then
|
||||
local r, rv = token:match(":([%d%.]+)/([%d%.]+)")
|
||||
cur.rtt = tonumber(r); cur.rttvar = tonumber(rv)
|
||||
elseif token:match("^retrans:") then
|
||||
local c, t = token:match(":(%d+)/(%d+)")
|
||||
cur.retrans = tonumber(c); cur.retrans_total = tonumber(t)
|
||||
elseif token:match("^bytes_sent:") then cur.bytes_sent = tonumber(token:match(":(%d+)"))
|
||||
elseif token:match("^bytes_acked:") then cur.bytes_acked = tonumber(token:match(":(%d+)"))
|
||||
elseif token:match("^bytes_retrans:") then cur.bytes_retrans = tonumber(token:match(":(%d+)"))
|
||||
elseif token:match("^bytes_received:") then cur.bytes_received = tonumber(token:match(":(%d+)"))
|
||||
elseif token:match("^minrtt:") then cur.min_rtt = tonumber(token:match(":([%d%.]+)"))
|
||||
end
|
||||
end
|
||||
-- pacing_rate/delivery_rate are separate space-joined tokens
|
||||
-- ("pacing_rate 6.4Mbps"), not "key:value" -- match on the whole
|
||||
-- line instead of a single token.
|
||||
local pr = line:match("pacing_rate%s+(%S+)")
|
||||
if pr then cur.pacing_rate = tobps(pr) end
|
||||
local dr = line:match("delivery_rate%s+(%S+)")
|
||||
if dr then cur.delivery_rate = tobps(dr) end
|
||||
end
|
||||
end
|
||||
if cur then subflows[#subflows + 1] = cur end
|
||||
return subflows
|
||||
end
|
||||
|
||||
-- Per-WAN endpoint cross-check: for every interface the router considers
|
||||
-- multipath-enabled, is its device actually registered as an MPTCP
|
||||
-- endpoint? If not, that WAN can never carry an MPTCP subflow no matter
|
||||
@@ -331,6 +414,20 @@ function build_issues(settings, counters, wans)
|
||||
"that corrupts data) triggers this.")
|
||||
end
|
||||
|
||||
-- RcvPruned counts data the kernel already received on an MPTCP subflow
|
||||
-- but then had to drop from the receive queue under socket memory
|
||||
-- pressure -- unlike the other counters here, this points at a local
|
||||
-- resource problem on this router (or the VPS, if it's the one pruning),
|
||||
-- not the network path: the data arrived fine and was thrown away anyway.
|
||||
if (counters.RcvPruned or 0) > 0 then
|
||||
add_issue(issues, "warning", "mptcp_rcv_pruned",
|
||||
string.format("%d segment(s) pruned from an MPTCP receive queue (memory pressure).", counters.RcvPruned),
|
||||
"Data that was already received had to be dropped because the socket ran out of receive buffer " ..
|
||||
"space -- a local resource problem, not a network issue. Usually means something is reading from " ..
|
||||
"the proxied connection too slowly (a slow/blocked application) or overall memory is tight. If " ..
|
||||
"this keeps climbing, check free memory and whether any proxy process is stalled.")
|
||||
end
|
||||
|
||||
-- Split by direction, not just summed: which side is doing the closing
|
||||
-- matters. Confirmed live on the bench (2026-08-16) -- a steady climb
|
||||
-- here traced back to OMR's own per-WAN shadowsocks health-check loop
|
||||
@@ -736,6 +833,17 @@ local methods = {
|
||||
established_count = established_count + 1
|
||||
end
|
||||
|
||||
-- Attribute each subflow's device to its UCI interface name
|
||||
-- (endpoints only carry the device, e.g. "eth1", not "wan1").
|
||||
local dev_to_name = {}
|
||||
for _, w in ipairs(wans) do
|
||||
if w.device ~= "" then dev_to_name[w.device] = w.name end
|
||||
end
|
||||
local subflows = parse_ss_subflows(exec_output("ss -tin 2>/dev/null"), endpoints)
|
||||
for _, sf in ipairs(subflows) do
|
||||
sf.wan = dev_to_name[sf.dev] or sf.dev
|
||||
end
|
||||
|
||||
return {
|
||||
settings = settings,
|
||||
counters = counters,
|
||||
@@ -744,6 +852,7 @@ local methods = {
|
||||
wans = wans,
|
||||
issues = issues,
|
||||
established_count = established_count,
|
||||
subflows = subflows,
|
||||
}
|
||||
end
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user