mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 08:23:56 +08:00
248 lines
8.8 KiB
Bash
Executable File
248 lines
8.8 KiB
Bash
Executable File
#!/bin/sh
|
|
# shellcheck shell=dash
|
|
# Optional Netify Agent (netifyd) integration — DPI application labels per device.
|
|
#
|
|
# Entirely optional: every entry point degrades to an empty result when netifyd
|
|
# is not installed, not running, or no socket reader is available, so the rest
|
|
# of trafficctl behaves exactly as before.
|
|
#
|
|
# Usage:
|
|
# trafficctl-netify.sh status — availability probe (JSON object)
|
|
# trafficctl-netify.sh collect [secs] — sample the socket, refresh the cache
|
|
# trafficctl-netify.sh list — cached per-device app breakdown
|
|
# trafficctl-netify.sh raw [secs] — raw socket lines (debugging)
|
|
#
|
|
# Telemetry comes from the agent's socket SINK, not from netifyd.sock: in
|
|
# Agent v5 that unix socket is a request/response API which never pushes flow
|
|
# data, so connecting to it returns nothing at all. The sink's bind address is
|
|
# read from netify-sink-socket.json (typically tcp://127.0.0.1:1780).
|
|
#
|
|
# Two payload shapes are handled: the aggregator's {"stats":[...]} records
|
|
# (application_id/local_ip/download/upload) and raw per-flow records
|
|
# (detected_application_name/local_ip/total_bytes). Records are parsed by field
|
|
# extraction rather than strict JSON, which tolerates either framing.
|
|
|
|
. /usr/local/bin/trafficctl-fw.sh
|
|
|
|
CACHE="/tmp/trafficctl_netify.json"
|
|
LOCK="/tmp/trafficctl_netify.lock"
|
|
DEFAULT_SOCKET="/var/run/netifyd/netifyd.sock"
|
|
|
|
netify_socket() {
|
|
local s
|
|
s=$(uci -q get trafficctl.main.netify_socket 2>/dev/null)
|
|
[ -n "$s" ] && { echo "$s"; return; }
|
|
echo "$DEFAULT_SOCKET"
|
|
}
|
|
|
|
# Where flow telemetry is actually published: an explicit override, else the
|
|
# first enabled socket-sink channel, else the legacy unix socket (Agent v4).
|
|
netify_endpoint() {
|
|
local ep
|
|
ep=$(uci -q get trafficctl.main.netify_endpoint 2>/dev/null)
|
|
[ -n "$ep" ] && { echo "$ep"; return; }
|
|
ep=$(sed -n 's/.*"bind_address\"[ \t]*:[ \t]*"\(tcp:\/\/[^"]*\)".*/\1/p' \
|
|
/etc/netifyd/netify-sink-socket.json 2>/dev/null | head -1)
|
|
[ -n "$ep" ] && { echo "$ep"; return; }
|
|
echo "unix://$(netify_socket)"
|
|
}
|
|
|
|
netify_enabled() {
|
|
[ "$(uci -q get trafficctl.main.netify_enabled 2>/dev/null)" != "0" ]
|
|
}
|
|
|
|
# Echo the command able to stream the socket, or nothing.
|
|
socket_reader() {
|
|
if command -v socat >/dev/null 2>&1; then
|
|
echo "socat"
|
|
elif command -v nc >/dev/null 2>&1; then
|
|
echo "nc"
|
|
fi
|
|
}
|
|
|
|
read_socket() {
|
|
local secs="$1" ep reader hostport path
|
|
ep=$(netify_endpoint)
|
|
reader=$(socket_reader)
|
|
[ -n "$reader" ] || return 1
|
|
|
|
case "$ep" in
|
|
tcp://*)
|
|
hostport=${ep#tcp://}
|
|
case "$reader" in
|
|
socat) timeout "$secs" socat -u "TCP-CONNECT:$hostport" - 2>/dev/null ;;
|
|
nc) timeout "$secs" nc "${hostport%%:*}" "${hostport##*:}" 2>/dev/null ;;
|
|
esac
|
|
;;
|
|
unix://*|/*)
|
|
path=${ep#unix://}
|
|
[ -S "$path" ] || return 1
|
|
case "$reader" in
|
|
socat) timeout "$secs" socat -u "UNIX-CONNECT:$path" - 2>/dev/null ;;
|
|
nc) timeout "$secs" nc -U "$path" 2>/dev/null ;;
|
|
esac
|
|
;;
|
|
*) return 1 ;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# Tests (and offline debugging) feed recorded agent output instead of a socket.
|
|
feed_lines() {
|
|
if [ -n "$TCTL_NETIFY_FEED" ] && [ -f "$TCTL_NETIFY_FEED" ]; then
|
|
cat "$TCTL_NETIFY_FEED"
|
|
else
|
|
read_socket "$1"
|
|
fi
|
|
}
|
|
|
|
do_status() {
|
|
local sock reader installed=false running=false readable=false age=-1 now mtime
|
|
sock=$(netify_socket)
|
|
reader=$(socket_reader)
|
|
command -v netifyd >/dev/null 2>&1 && installed=true
|
|
[ -S "$sock" ] && running=true
|
|
[ -n "$reader" ] && readable=true
|
|
if [ -f "$CACHE" ]; then
|
|
now=$(date +%s)
|
|
mtime=$(date -r "$CACHE" +%s 2>/dev/null || echo 0)
|
|
age=$(( now - mtime ))
|
|
fi
|
|
printf '{"enabled":%s,"installed":%s,"running":%s,"socket":"%s","endpoint":"%s","reader":"%s","readable":%s,"cache_age":%d}\n' \
|
|
"$(netify_enabled && echo true || echo false)" \
|
|
"$installed" "$running" "$sock" "$(netify_endpoint)" "${reader:-none}" "$readable" "$age"
|
|
}
|
|
|
|
# Aggregate flow records into "per local IP → per application" totals.
|
|
#
|
|
# The agent re-emits a flow as its byte counters grow (flow, flow_dpi_update,
|
|
# flow_dpi_complete), so totals are tracked per flow digest and only summed at
|
|
# the end — adding every sighting would multiply-count the same traffic.
|
|
parse_flows() {
|
|
# One JSON object per line so the field extractor sees one record at a
|
|
# time — the aggregator packs every device into a single "stats" array.
|
|
sed 's/},[ ]*{/}\n{/g' | awk '
|
|
function field(line, key, re, seg, v) {
|
|
re = "\"" key "\"[ \t]*:[ \t]*"
|
|
if (!match(line, re)) return ""
|
|
seg = substr(line, RSTART + RLENGTH)
|
|
if (substr(seg, 1, 1) == "\"") {
|
|
seg = substr(seg, 2)
|
|
if (!match(seg, /"/)) return ""
|
|
return substr(seg, 1, RSTART - 1)
|
|
}
|
|
if (!match(seg, /^-?[0-9]+/)) return ""
|
|
return substr(seg, RSTART, RLENGTH) + 0
|
|
}
|
|
{
|
|
# Only flow-bearing records carry local_ip; skip status/hello lines.
|
|
ip = field($0, "local_ip")
|
|
if (ip !~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) next
|
|
|
|
# Aggregator records name the app as "<id>.netify.<name>".
|
|
app = field($0, "application_id")
|
|
sub(/^[0-9]+\./, "", app)
|
|
if (app == "") app = field($0, "detected_application_name")
|
|
# Agent prefixes catalogued apps with "netify."; the bare name reads
|
|
# better in a table.
|
|
sub(/^netify\./, "", app)
|
|
if (app == "" || app == "unknown") app = field($0, "detected_protocol_name")
|
|
if (app == "" || app == "Unknown") {
|
|
host = field($0, "host_server_name")
|
|
if (host != "") app = host; else app = "unclassified"
|
|
}
|
|
gsub(/[^a-zA-Z0-9._ -]/, "", app)
|
|
if (app == "") app = "unclassified"
|
|
|
|
digest = field($0, "digest")
|
|
# Aggregator: per-interval byte deltas, already summed per device+app.
|
|
bytes = field($0, "download") + field($0, "upload")
|
|
if (bytes == 0) bytes = field($0, "total_bytes")
|
|
if (bytes == "" || bytes == 0) bytes = field($0, "local_bytes") + field($0, "other_bytes")
|
|
if (digest == "") digest = ip "|" app "|" NR
|
|
|
|
key = ip SUBSEP app
|
|
# Last sighting of a digest carries its cumulative total.
|
|
if (!(digest in seen)) { seen[digest] = key; flows[key]++ }
|
|
dbytes[digest] = bytes + 0
|
|
}
|
|
END {
|
|
for (d in seen) total[seen[d]] += dbytes[d]
|
|
for (k in total) {
|
|
split(k, p, SUBSEP)
|
|
printf "%s %s %.0f %d\n", p[1], p[2], total[k], flows[k]
|
|
}
|
|
}'
|
|
}
|
|
|
|
do_collect() {
|
|
# The aggregator publishes on an interval (~15s), so a short sample window
|
|
# usually catches nothing at all — wait long enough to span one report.
|
|
local secs="${1:-18}" raw parsed tmp
|
|
case "$secs" in ''|*[!0-9]*) secs=18 ;; esac
|
|
[ "$secs" -lt 1 ] && secs=1
|
|
[ "$secs" -gt 60 ] && secs=60
|
|
|
|
netify_enabled || { echo '{"ok":false,"msg":"netify integration disabled"}'; return 0; }
|
|
|
|
# One collector at a time — overlapping samples would double the socket
|
|
# load and race on the cache file.
|
|
mkdir "$LOCK" 2>/dev/null || { echo '{"ok":false,"msg":"collection already running"}'; return 0; }
|
|
# shellcheck disable=SC2064
|
|
trap "rmdir '$LOCK' 2>/dev/null" EXIT INT TERM
|
|
|
|
raw=$(feed_lines "$secs")
|
|
if [ -z "$raw" ]; then
|
|
echo '{"ok":false,"msg":"no data from netifyd socket"}'
|
|
return 0
|
|
fi
|
|
|
|
parsed=$(printf '%s\n' "$raw" | parse_flows)
|
|
tmp="${CACHE}.tmp"
|
|
# Group by IP, apps sorted by bytes descending.
|
|
printf '%s\n' "$parsed" | sort -k1,1 -k3,3nr | awk '
|
|
BEGIN { printf "[" }
|
|
NF >= 4 {
|
|
if ($1 != cur) {
|
|
if (cur != "") printf "]},"
|
|
printf "{\"ip\":\"%s\",\"top\":\"%s\",\"apps\":[", $1, $2
|
|
cur = $1
|
|
first = 1
|
|
}
|
|
if (!first) printf ","
|
|
printf "{\"name\":\"%s\",\"bytes\":%.0f,\"flows\":%d}", $2, $3, $4
|
|
first = 0
|
|
}
|
|
END {
|
|
if (cur != "") printf "]}"
|
|
printf "]\n"
|
|
}' > "$tmp"
|
|
mv "$tmp" "$CACHE"
|
|
|
|
printf '{"ok":true,"msg":"collected %s","devices":%d}\n' \
|
|
"${secs}s" "$(printf '%s\n' "$parsed" | awk 'NF{print $1}' | sort -u | wc -l)"
|
|
}
|
|
|
|
do_list() {
|
|
if [ -f "$CACHE" ]; then
|
|
cat "$CACHE"
|
|
else
|
|
echo '[]'
|
|
fi
|
|
}
|
|
|
|
do_raw() {
|
|
local secs="${1:-18}"
|
|
case "$secs" in ''|*[!0-9]*) secs=18 ;; esac
|
|
[ "$secs" -gt 60 ] && secs=60
|
|
feed_lines "$secs"
|
|
}
|
|
|
|
case "$1" in
|
|
status) do_status ;;
|
|
collect) do_collect "$2" ;;
|
|
list) do_list ;;
|
|
raw) do_raw "$2" ;;
|
|
*) echo '{"ok":false,"msg":"usage: trafficctl-netify.sh status|collect [secs]|list|raw [secs]"}'; exit 1 ;;
|
|
esac
|