mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 16:44:23 +08:00
391 lines
15 KiB
Bash
Executable File
391 lines
15 KiB
Bash
Executable File
#!/bin/sh
|
|
# shellcheck shell=dash
|
|
# Detailed device info with connections.
|
|
# Usage: trafficctl-device.sh <ip> [--rdns] [--proto tcp|udp|all]
|
|
|
|
. /usr/local/bin/trafficctl-fw.sh
|
|
|
|
LAN_DEV=$(tctl_get_lan_device)
|
|
|
|
# Parse arguments
|
|
IP=""
|
|
DO_RDNS=0
|
|
PROTO_FILTER="all"
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--rdns) DO_RDNS=1 ;;
|
|
--proto) shift; PROTO_FILTER="$1" ;;
|
|
*) [ -z "$IP" ] && IP="$1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [ -z "$IP" ]; then
|
|
echo '{"ok":false,"msg":"usage: trafficctl-device.sh <ip> [--rdns] [--proto tcp|udp|all]"}'
|
|
exit 1
|
|
fi
|
|
|
|
if ! tctl_validate_ip "$IP"; then
|
|
echo '{"ok":false,"msg":"invalid IP address"}'
|
|
exit 1
|
|
fi
|
|
|
|
# Get device name from DHCP leases
|
|
NAME=""
|
|
MAC=""
|
|
if [ -f /tmp/dhcp.leases ]; then
|
|
NAME=$(awk -v ip="$IP" '$3 == ip {print $4}' /tmp/dhcp.leases | head -1)
|
|
MAC=$(awk -v ip="$IP" '$3 == ip {print $2}' /tmp/dhcp.leases | head -1)
|
|
fi
|
|
if [ -z "$MAC" ]; then
|
|
MAC=$(ip neigh show "$IP" 2>/dev/null | grep -oE '[0-9a-fA-F:]{17}' | head -1)
|
|
fi
|
|
MAC=$(echo "$MAC" | tr 'A-F' 'a-f')
|
|
[ "$NAME" = "*" ] && NAME=""
|
|
|
|
# Same precedence as the summary: manual alias wins, then the lease name above,
|
|
# then cached reverse DNS (the only automatic source for routed devices).
|
|
ALIAS=$(awk -v ip="$IP" '$1 == ip {sub(/^[^ ]+ +/, ""); print; exit}' /etc/trafficctl/names 2>/dev/null)
|
|
[ -n "$ALIAS" ] && NAME="$ALIAS"
|
|
if [ -z "$NAME" ] && [ "$(uci -q get trafficctl.main.resolve_names 2>/dev/null)" != "0" ]; then
|
|
NAME=$(awk -v ip="$IP" '$1 == ip && $2 != "-" {print $2; exit}' /tmp/trafficctl_rdns_cache 2>/dev/null)
|
|
fi
|
|
[ -z "$NAME" ] && NAME="*"
|
|
|
|
# Detect connection type — specific interface or band
|
|
CONN_TYPE=""
|
|
CONN_LAST=""
|
|
CONN_CACHE="/tmp/trafficctl_conn_cache"
|
|
[ -f "$CONN_CACHE" ] || : > "$CONN_CACHE"
|
|
|
|
if ! tctl_ip_in_lan "$IP"; then
|
|
# Not on any connected LAN subnet — reached via a downstream router.
|
|
CONN_TYPE="routed"
|
|
elif [ -n "$MAC" ]; then
|
|
_wifi_stations=$(
|
|
for _wi in $(iw dev 2>/dev/null | awk '/Interface/{print $2}'); do
|
|
_ch=$(iw dev "$_wi" info 2>/dev/null | awk '/channel/{print $2}')
|
|
_band=""
|
|
if [ -n "$_ch" ]; then
|
|
if [ "$_ch" -le 14 ] 2>/dev/null; then _band="2.4G"
|
|
elif [ "$_ch" -le 177 ] 2>/dev/null; then _band="5G"
|
|
else _band="6G"; fi
|
|
fi
|
|
iw dev "$_wi" station dump 2>/dev/null | awk -v iface="$_wi" -v band="$_band" \
|
|
'/Station/{print tolower($2), iface, band}'
|
|
done
|
|
)
|
|
WIFI_LINE=$(echo "$_wifi_stations" | grep -i "$MAC")
|
|
if [ -n "$WIFI_LINE" ]; then
|
|
CONN_TYPE=$(echo "$WIFI_LINE" | awk '{print $3}')
|
|
[ -z "$CONN_TYPE" ] && CONN_TYPE="wifi"
|
|
else
|
|
BR_PORT=$(brctl showmacs "$LAN_DEV" 2>/dev/null | awk -v mac="$MAC" '
|
|
NR>1 && $3=="no" && tolower($2)==mac {print $1; exit}')
|
|
if [ -n "$BR_PORT" ]; then
|
|
PORT_IFACE=""
|
|
for pdir in /sys/class/net/"$LAN_DEV"/brif/*/; do
|
|
[ -d "$pdir" ] || continue
|
|
pno=$(cat "${pdir}port_no" 2>/dev/null)
|
|
[ -z "$pno" ] && continue
|
|
if [ $(( pno )) -eq "$BR_PORT" ] 2>/dev/null; then
|
|
PORT_IFACE=$(basename "$pdir")
|
|
break
|
|
fi
|
|
done
|
|
case "$PORT_IFACE" in
|
|
phy*|wlan*) CONN_TYPE="wifi" ;;
|
|
"") ;;
|
|
*) CONN_TYPE="$PORT_IFACE" ;;
|
|
esac
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [ -n "$CONN_TYPE" ]; then
|
|
sed -i "/^$IP /d" "$CONN_CACHE" 2>/dev/null
|
|
echo "$IP $CONN_TYPE $(date +%s)" >> "$CONN_CACHE"
|
|
else
|
|
_arp_state=$(ip neigh show "$IP" 2>/dev/null | awk '{print $NF}')
|
|
case "$_arp_state" in
|
|
REACHABLE|STALE|DELAY|PROBE) CONN_TYPE="ethernet" ;;
|
|
*)
|
|
CONN_TYPE="?"
|
|
_cached=$(grep "^$IP " "$CONN_CACHE" 2>/dev/null | tail -1)
|
|
if [ -n "$_cached" ]; then
|
|
CONN_LAST=$(echo "$_cached" | awk '{print $2 "@" $3}')
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Check blocked status
|
|
BLOCKED=false
|
|
BLOCK_PACKETS=0
|
|
BLOCK_BYTES=0
|
|
if tctl_is_blocked "$IP"; then
|
|
BLOCKED=true
|
|
if [ "$TCTL_FW" = "nft" ]; then
|
|
# A block is two rules — the v4 one keyed on the address and the v6 one
|
|
# keyed on the MAC (see tctl_block_add) — so the counters are summed
|
|
# across both. The v6 rule carries no address, only its "_mac" comment.
|
|
block_line=$(nft list chain inet fw4 forward 2>/dev/null \
|
|
| grep -e "ip saddr $IP.*drop" -e "comment \"$(tctl_block_comment "$IP")_mac\"")
|
|
BLOCK_PACKETS=$(echo "$block_line" | grep -oE 'packets [0-9]+' | awk '{ t += $2 } END { printf "%.0f", t }')
|
|
BLOCK_BYTES=$(echo "$block_line" | grep -oE 'bytes [0-9]+' | awk '{ t += $2 } END { printf "%.0f", t }')
|
|
else
|
|
block_line=$(iptables -L FORWARD -nvx 2>/dev/null | grep "DROP" | grep "$IP")
|
|
BLOCK_PACKETS=$(echo "$block_line" | awk '{print $1}')
|
|
BLOCK_BYTES=$(echo "$block_line" | awk '{print $2}')
|
|
fi
|
|
fi
|
|
[ -z "$BLOCK_PACKETS" ] && BLOCK_PACKETS=0
|
|
[ -z "$BLOCK_BYTES" ] && BLOCK_BYTES=0
|
|
|
|
# Check wifi blocked
|
|
WIFI_BLOCKED=false
|
|
if [ -n "$MAC" ]; then
|
|
IFACES=$(tctl_get_wifi_interfaces)
|
|
for iface in $IFACES; do
|
|
maclist=$(uci -q get "wireless.${iface}.maclist")
|
|
listed=0
|
|
echo "$maclist" | grep -qi "$MAC" && listed=1
|
|
# Being listed means the opposite thing per policy: on a deny radio a
|
|
# listed MAC is blocked, on an allow (whitelist) radio an UNlisted one is.
|
|
if [ "$(tctl_get_wifi_filter_mode "$iface")" = "allow" ]; then
|
|
[ "$listed" = "0" ] && { WIFI_BLOCKED=true; break; }
|
|
else
|
|
[ "$listed" = "1" ] && { WIFI_BLOCKED=true; break; }
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Get rate limit
|
|
RATE_LIM=0
|
|
if [ "$TCTL_FW" = "nft" ]; then
|
|
RATE_LIM=$(nft list table netdev tm_ratelimit 2>/dev/null | grep "daddr $IP" | \
|
|
grep -oE '[0-9]+ kbytes' | awk '{print $1 * 8}')
|
|
else
|
|
RATE_LIM=$(iptables -t mangle -L FORWARD -nv 2>/dev/null | grep "rl_ratelimit" | grep "$IP" | \
|
|
grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//')
|
|
fi
|
|
[ -z "$RATE_LIM" ] && RATE_LIM=0
|
|
|
|
# Get shape rate
|
|
SHAPE_KBIT=0
|
|
o3=$(echo "$IP" | cut -d. -f3)
|
|
o4=$(echo "$IP" | cut -d. -f4)
|
|
dec=$((o3 * 256 + o4))
|
|
hex=$(printf "%x" "$dec")
|
|
classid="1:$hex"
|
|
# Skip reserved HTB classes (root 1:1 and default 1:fffe)
|
|
shape_info=""
|
|
case "$hex" in 1|fffe) ;; *) shape_info=$(tc class show dev "$LAN_DEV" classid "$classid" 2>/dev/null) ;; esac
|
|
if [ -n "$shape_info" ]; then
|
|
SHAPE_KBIT=$(echo "$shape_info" | grep -oE 'rate [0-9]+[A-Za-z]+' | head -1 | awk '{
|
|
rate=$2; num=rate+0
|
|
if (rate ~ /Gbit/) print num*1000000
|
|
else if (rate ~ /Mbit/) print num*1000
|
|
else if (rate ~ /[Kk]bit/) print num
|
|
else print num
|
|
}')
|
|
fi
|
|
[ -z "$SHAPE_KBIT" ] && SHAPE_KBIT=0
|
|
|
|
# Parse conntrack for connections and totals
|
|
TIMESTAMP=$(date +%s)
|
|
CONNTRACK_DATA=$(cat /proc/net/nf_conntrack 2>/dev/null | grep "src=$IP ")
|
|
|
|
# Compute metadata (totals + states)
|
|
META_LINE=$(echo "$CONNTRACK_DATA" | awk -v ip="$IP" -v pf="$PROTO_FILTER" '
|
|
BEGIN { total=0; n_tcp=0; n_udp=0; n_other=0; est=0; tw=0; ss=0; cw=0 }
|
|
{
|
|
proto=""
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i == "tcp") proto="tcp"
|
|
else if ($i == "udp") proto="udp"
|
|
else if ($i == "icmp") proto="icmp"
|
|
}
|
|
if (pf != "all" && proto != pf) next
|
|
|
|
dst=""; bytes=0; state=""
|
|
src_key = "src=" ip
|
|
seen_src=0; got_dst=0
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i == src_key && !seen_src) { seen_src=1; continue }
|
|
if (seen_src && !got_dst && index($i, "dst=") == 1) { dst=substr($i, 5); got_dst=1 }
|
|
if (seen_src && !got_dst) continue
|
|
if (seen_src && index($i, "bytes=") == 1 && bytes == 0) { bytes=substr($i, 7)+0 }
|
|
if ($i == "ESTABLISHED") state="ESTABLISHED"
|
|
else if ($i == "TIME_WAIT") state="TIME_WAIT"
|
|
else if ($i == "SYN_SENT") state="SYN_SENT"
|
|
else if ($i == "CLOSE_WAIT") state="CLOSE_WAIT"
|
|
}
|
|
if (dst == "" || dst == ip) next
|
|
total += bytes
|
|
if (proto == "tcp") n_tcp++
|
|
else if (proto == "udp") n_udp++
|
|
else n_other++
|
|
if (state == "ESTABLISHED") est++
|
|
else if (state == "TIME_WAIT") tw++
|
|
else if (state == "SYN_SENT") ss++
|
|
else if (state == "CLOSE_WAIT") cw++
|
|
}
|
|
END { printf "%.0f %d %d %d %d %d %d %d", total, n_tcp, n_udp, n_other, est, tw, ss, cw }
|
|
')
|
|
|
|
TOTAL=$(echo "$META_LINE" | awk '{print $1}')
|
|
N_TCP=$(echo "$META_LINE" | awk '{print $2}')
|
|
N_UDP=$(echo "$META_LINE" | awk '{print $3}')
|
|
N_OTHER=$(echo "$META_LINE" | awk '{print $4}')
|
|
EST=$(echo "$META_LINE" | awk '{print $5}')
|
|
TW=$(echo "$META_LINE" | awk '{print $6}')
|
|
SS=$(echo "$META_LINE" | awk '{print $7}')
|
|
CW=$(echo "$META_LINE" | awk '{print $8}')
|
|
|
|
[ -z "$TOTAL" ] && TOTAL=0
|
|
[ -z "$N_TCP" ] && N_TCP=0
|
|
[ -z "$N_UDP" ] && N_UDP=0
|
|
[ -z "$N_OTHER" ] && N_OTHER=0
|
|
[ -z "$EST" ] && EST=0
|
|
[ -z "$TW" ] && TW=0
|
|
[ -z "$SS" ] && SS=0
|
|
[ -z "$CW" ] && CW=0
|
|
|
|
# Build connections JSON array
|
|
# Resolve egress interface per destination for policy-routed connections
|
|
# (mwan3 etc.). Only connections carrying a NON-zero conntrack mark are
|
|
# resolved: `ip route get <dst> mark <mark>` honours the fwmark ip-rules, so it
|
|
# reports the WAN that mwan3 actually selected. mark=0 connections (no policy
|
|
# routing, e.g. plain single-WAN or podkop) are left blank instead of showing a
|
|
# misleading main-table egress.
|
|
OIF_MAP="/tmp/trafficctl_oif_$$"
|
|
: > "$OIF_MAP"
|
|
if command -v ip >/dev/null 2>&1; then
|
|
echo "$CONNTRACK_DATA" | awk -v ip="$IP" '
|
|
{ dst=""; mark="0"; sk="src=" ip; seen=0; got=0
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i==sk && !seen) { seen=1; continue }
|
|
if (seen && !got && index($i,"dst=")==1) { dst=substr($i,5); got=1 }
|
|
if (index($i,"mark=")==1) mark=substr($i,6)
|
|
}
|
|
if (dst!="" && dst!=ip && mark!="0" && mark!="") print dst, mark
|
|
}' | sort -u | while read -r dip dmark; do
|
|
[ -n "$dip" ] || continue
|
|
dev=$(ip route get "$dip" mark "$dmark" 2>/dev/null | grep -oE 'dev [^ ]+' | head -1 | cut -d' ' -f2)
|
|
case "$dev" in lo|"") continue ;; esac
|
|
echo "$dip $dev" >> "$OIF_MAP"
|
|
done
|
|
fi
|
|
|
|
CONNS_OUT=$(echo "$CONNTRACK_DATA" | awk -v ip="$IP" -v pf="$PROTO_FILTER" -v oifmap="$OIF_MAP" '
|
|
BEGIN {
|
|
n=0
|
|
while ((getline line < oifmap) > 0) { split(line, p, " "); oifdev[p[1]] = p[2] }
|
|
close(oifmap)
|
|
}
|
|
{
|
|
proto=""
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i == "tcp") proto="tcp"
|
|
else if ($i == "udp") proto="udp"
|
|
else if ($i == "icmp") proto="icmp"
|
|
}
|
|
if (pf != "all" && proto != pf) next
|
|
|
|
dst=""; dport=""; bytes=0; state=""
|
|
src_key = "src=" ip
|
|
seen_src=0; got_dst=0
|
|
for (i=1; i<=NF; i++) {
|
|
if ($i == src_key && !seen_src) { seen_src=1; continue }
|
|
if (seen_src && !got_dst && index($i, "dst=") == 1) { dst=substr($i, 5); got_dst=1 }
|
|
if (seen_src && !got_dst) continue
|
|
if (seen_src && index($i, "dport=") == 1 && dport == "") dport=substr($i, 7)
|
|
if (seen_src && index($i, "bytes=") == 1 && bytes == 0) bytes=substr($i, 7)+0
|
|
if ($i == "ESTABLISHED") state="ESTABLISHED"
|
|
else if ($i == "TIME_WAIT") state="TIME_WAIT"
|
|
else if ($i == "SYN_SENT") state="SYN_SENT"
|
|
else if ($i == "CLOSE_WAIT") state="CLOSE_WAIT"
|
|
else if ($i == "FIN_WAIT") state="FIN_WAIT"
|
|
}
|
|
if (dst == "" || dst == ip) next
|
|
if (dport == "") dport = "0"
|
|
|
|
# Service name lookup
|
|
svc=""
|
|
if (dport == "80") svc="http"
|
|
else if (dport == "443") svc="https"
|
|
else if (dport == "53") svc="dns"
|
|
else if (dport == "22") svc="ssh"
|
|
else if (dport == "123") svc="ntp"
|
|
else if (dport == "5353") svc="mdns"
|
|
else if (dport == "21") svc="ftp"
|
|
else if (dport == "25") svc="smtp"
|
|
else if (dport == "993") svc="imaps"
|
|
else if (dport == "8080") svc="http-alt"
|
|
|
|
oif = ""
|
|
if (dst in oifdev) oif = oifdev[dst]
|
|
if (n > 0) printf ","
|
|
printf "{\"proto\":\"%s\",\"dst\":\"%s\",\"host\":\"\",\"port\":%s,\"service\":\"%s\",\"bytes\":%.0f,\"state\":\"%s\",\"oif\":\"%s\"}", proto, dst, dport, svc, bytes, state, oif
|
|
n++
|
|
}
|
|
')
|
|
rm -f "$OIF_MAP"
|
|
|
|
# Optionally resolve DNS for connection destinations
|
|
if [ "$DO_RDNS" = "1" ]; then
|
|
DST_IPS=$(echo "$CONNTRACK_DATA" | grep -oE 'dst=[0-9.]+' | sed 's/dst=//' | sort -u | grep -v "^$IP$")
|
|
if [ -n "$DST_IPS" ]; then
|
|
RDNS_MAP="/tmp/trafficctl_rdns_$$"
|
|
# shellcheck disable=SC2064
|
|
trap "rm -f '$RDNS_MAP'" EXIT INT TERM
|
|
: > "$RDNS_MAP"
|
|
RDNS_DONE=0
|
|
# Batch resolve via ubus network.rrdns (same as LuCI frontend; no extra packages needed)
|
|
if command -v ubus >/dev/null 2>&1; then
|
|
ADDRS_JSON=$(echo "$DST_IPS" | awk '{printf "%s\"%s\"", (NR>1?",":""), $1}')
|
|
RDNS_RESULT=$(ubus call network.rrdns lookup \
|
|
"{\"addrs\":[$ADDRS_JSON],\"timeout\":3000,\"limit\":64}" 2>/dev/null)
|
|
if [ -n "$RDNS_RESULT" ]; then
|
|
RDNS_DONE=1
|
|
for dip in $DST_IPS; do
|
|
host=$(printf '%s' "$RDNS_RESULT" | jsonfilter -e "@[\"$dip\"]" 2>/dev/null)
|
|
case "$host" in
|
|
*[!a-zA-Z0-9._-]*|"") continue ;;
|
|
esac
|
|
echo "$dip $host" >> "$RDNS_MAP"
|
|
done
|
|
fi
|
|
fi
|
|
# Fallback: per-IP nslookup (BusyBox, always available on OpenWrt)
|
|
if [ "$RDNS_DONE" = "0" ] && command -v nslookup >/dev/null 2>&1; then
|
|
for dip in $DST_IPS; do
|
|
host=$(nslookup "$dip" 2>/dev/null | sed -n 's/.*name = \(.*\)\.$/\1/p' | head -1)
|
|
case "$host" in
|
|
*[!a-zA-Z0-9._-]*|"") continue ;;
|
|
esac
|
|
echo "$dip $host" >> "$RDNS_MAP"
|
|
done
|
|
fi
|
|
if [ -s "$RDNS_MAP" ]; then
|
|
# Build sed expression from map
|
|
SED_EXPR=$(awk '{printf "s|\"dst\":\"%s\",\"host\":\"\"|\"dst\":\"%s\",\"host\":\"%s\"|g\n", $1, $1, $2}' "$RDNS_MAP")
|
|
CONNS_OUT=$(printf "%s" "$CONNS_OUT" | sed "$SED_EXPR")
|
|
fi
|
|
rm -f "$RDNS_MAP"
|
|
fi
|
|
fi
|
|
|
|
# Listed as blocked yet associated right now = the radio never got the block.
|
|
WIFI_PENDING=false
|
|
tctl_wifi_block_pending "$([ "$WIFI_BLOCKED" = "true" ] && echo 1 || echo 0)" "$CONN_TYPE" \
|
|
&& WIFI_PENDING=true
|
|
|
|
# Output final JSON
|
|
printf '{"ip":"%s","name":"%s","mac":"%s","conn_type":"%s","conn_last":"%s","timestamp":%d,"blocked":%s,"block_packets":%d,"block_bytes":%.0f,"wifi_blocked":%s,"wifi_block_pending":%s,"total":%.0f,"protocols":{"tcp":%d,"udp":%d,"other":%d},"tcp_states":{"established":%d,"time_wait":%d,"syn_sent":%d,"close_wait":%d},"connections":[%s],"rate_limit_kbit":%d,"shape_kbit":%d}\n' \
|
|
"$IP" "$NAME" "$MAC" "$CONN_TYPE" "$CONN_LAST" "$TIMESTAMP" "$BLOCKED" "$BLOCK_PACKETS" "$BLOCK_BYTES" \
|
|
"$WIFI_BLOCKED" "$WIFI_PENDING" "$TOTAL" "$N_TCP" "$N_UDP" "$N_OTHER" \
|
|
"$EST" "$TW" "$SS" "$CW" "$CONNS_OUT" "$RATE_LIM" "$SHAPE_KBIT"
|