Files

102 lines
4.4 KiB
Bash
Executable File

#!/bin/sh
# shellcheck shell=dash
# Per-device byte counters using nftables maps.
# Works with software flow offload (hook priority -200, before flowtable at -150).
# Output: JSON array
# [{"ip":"…","bytes_in":N,"bytes_out":N,"bytes_tcp":-1,"bytes_udp":-1,
# "src":"nft","degraded":false}]
#
# "degraded" is always false here, and that is the entire point of this script:
# these counters sit ahead of the flowtable, so they keep moving under offload
# where conntrack freezes. If this path is unavailable, trafficctl-bytes.sh
# reports degraded:true instead of quietly handing back frozen numbers.
#
# The counters here are cumulative since the table was created, unlike the
# conntrack sums trafficctl-bytes.sh returns, which cover live flows only.
# "src" lets trafficctl-totals.sh notice the swap and rebaseline instead of
# accumulating the whole map as a single enormous delta.
#
# The protocol split is -1, i.e. "unknown", not 0: a `type ipv4_addr : counter`
# map is keyed by address alone, so there is nothing here to split TCP from UDP
# with. Reporting 0 would render as a device that sent no TCP at all.
. /usr/local/bin/trafficctl-fw.sh
command -v nft >/dev/null 2>&1 || { echo '[]'; exit 0; }
# Create table/maps/chain/rules if not already present (idempotent).
#
# The gate matches the CURRENT rule orientation, not merely "some rule exists".
# Until 1.13.x the maps were keyed the other way round (bytes_in by saddr), so
# a router upgrading from that version already has the chain: a looser check
# would leave the old, inverted rules in place forever and the fix would only
# ever reach fresh installs.
#
# Direction convention, matching trafficctl-bytes.sh and docs/API.md:
# bytes_in = traffic toward the device (download) -> keyed by ip daddr
# bytes_out = traffic from the device (upload) -> keyed by ip saddr
if ! nft list chain inet trafficctl_mon mon_forward 2>/dev/null | grep -q "bytes_in.*daddr"; then
# Drop the old table wholesale rather than appending to it — leaving the
# previous pair of rules in place would double-count every packet. This
# resets the counters once, on upgrade; they are only used for speed
# deltas, so a single discarded sample is the whole cost.
nft delete table inet trafficctl_mon 2>/dev/null
nft add table inet trafficctl_mon 2>/dev/null
nft add map inet trafficctl_mon bytes_in \
'{ type ipv4_addr : counter; flags dynamic; }' 2>/dev/null
nft add map inet trafficctl_mon bytes_out \
'{ type ipv4_addr : counter; flags dynamic; }' 2>/dev/null
nft add chain inet trafficctl_mon mon_forward \
'{ type filter hook forward priority -200; policy accept; }' 2>/dev/null
nft add rule inet trafficctl_mon mon_forward \
'update @bytes_in { ip daddr counter }' 2>/dev/null
nft add rule inet trafficctl_mon mon_forward \
'update @bytes_out { ip saddr counter }' 2>/dev/null
fi
# Dynamic counter maps are unsupported on some kernels ("Not supported").
# Without this check the maps silently never exist, every rule fails, and the
# script returns [] forever — per-device speed just stops working.
if ! nft list map inet trafficctl_mon bytes_in >/dev/null 2>&1; then
TCTL_FORCE_CONNTRACK=1 exec /usr/local/bin/trafficctl-bytes.sh
fi
IN=$(nft list map inet trafficctl_mon bytes_in 2>/dev/null)
OUT=$(nft list map inet trafficctl_mon bytes_out 2>/dev/null)
# Parse: lines look like "192.168.0.100 : counter packets 584 bytes 892341[,]"
printf '%s\n__SEP__\n%s\n' "$IN" "$OUT" | awk '
/^__SEP__$/ { phase = 1; next }
/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+ : counter/ {
ip = ""; val = 0
for (i = 1; i <= NF; i++) {
if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) ip = $i
if ($i == "bytes") val = $(i+1) + 0
}
if (ip != "") {
if (phase == 0) in_b[ip] += val
else out_b[ip] += val
}
}
END {
printf "["
n = 0
for (ip in in_b) {
if (n > 0) printf ","
printf "{\"ip\":\"%s\",\"bytes_in\":%.0f,\"bytes_out\":%.0f,\"bytes_tcp\":-1,\"bytes_udp\":-1,\"src\":\"nft\",\"degraded\":false}", \
ip, in_b[ip], out_b[ip]+0
n++
}
for (ip in out_b) {
if (!(ip in in_b)) {
if (n > 0) printf ","
printf "{\"ip\":\"%s\",\"bytes_in\":0,\"bytes_out\":%.0f,\"bytes_tcp\":-1,\"bytes_udp\":-1,\"src\":\"nft\",\"degraded\":false}", \
ip, out_b[ip]
n++
}
}
printf "]\n"
}
'