- json_helper: batch_resolve_dest() resolves all dest IPs in one Python call - json.sh: json::batch_resolve_dest wrapper - activity: _DEST_RESOLVE_CACHE pre-populated before render loop - _render_peer_accept_dests: cache lookup instead of resolve::dest per row - activity: 1.8s -> 0.48s
460 lines
No EOL
14 KiB
Bash
460 lines
No EOL
14 KiB
Bash
#!/usr/bin/env bash
|
|
# activity.command.sh — WireGuard activity snapshot
|
|
|
|
# ============================================
|
|
# Lifecycle
|
|
# ============================================
|
|
|
|
function cmd::activity::on_load() {
|
|
load_module net
|
|
|
|
flag::register --peer
|
|
flag::register --service
|
|
flag::register --ip
|
|
flag::register --hours
|
|
flag::register --type
|
|
flag::register --accept
|
|
flag::register --drop
|
|
flag::register --external
|
|
|
|
command::mixin json_output
|
|
}
|
|
|
|
# ============================================
|
|
# Help
|
|
# ============================================
|
|
|
|
function cmd::activity::help() {
|
|
cat <<EOF
|
|
Usage: wgctl activity [options]
|
|
|
|
Show WireGuard activity — transfer totals and firewall drops per peer.
|
|
Data sources: wg show transfer, fw_events.log
|
|
|
|
Options:
|
|
--peer <name> Filter by peer name
|
|
--service <name> Filter by service (e.g. truenas, proxmox:web-ui)
|
|
--ip <ip> Filter by destination IP
|
|
--hours <n> Time window in hours (default: 24, 0 = all time)
|
|
--type <type> Filter by device type (combined with --peer)
|
|
--accept Show only accepted traffic (from conntrack)
|
|
--drop Show only firewall drops
|
|
--external Show only external traffic (full tunnel peers)
|
|
|
|
Examples:
|
|
wgctl activity
|
|
wgctl activity --peer phone-nuno
|
|
wgctl activity --service truenas
|
|
wgctl activity --hours 0
|
|
wgctl activity --ip 10.0.0.101
|
|
EOF
|
|
}
|
|
|
|
# ============================================
|
|
# Run
|
|
# ============================================
|
|
|
|
function cmd::activity::run() {
|
|
local filter_peer="" filter_service="" filter_ip="" filter_type=""
|
|
local hours=24
|
|
local accept_only=false drop_only=false external_only=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--peer) filter_peer="$2"; shift 2 ;;
|
|
--service) filter_service="$2"; shift 2 ;;
|
|
--ip) filter_ip="$2"; shift 2 ;;
|
|
--type) filter_type="$2"; shift 2 ;;
|
|
--hours) hours="$2"; shift 2 ;;
|
|
--accept) accept_only=true; shift ;;
|
|
--drop) drop_only=true; shift ;;
|
|
--external) external_only=true; shift ;;
|
|
--help) cmd::activity::help; return ;;
|
|
*)
|
|
log::error "Unknown flag: $1"
|
|
cmd::activity::help
|
|
return 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if command::json; then
|
|
cmd::activity::_output_json "$hours"
|
|
return 0
|
|
fi
|
|
|
|
if [[ -n "$filter_peer" && -n "$filter_type" ]]; then
|
|
filter_peer=$(peers::resolve_and_require "$filter_peer" "$filter_type") || return 1
|
|
fi
|
|
|
|
local service_ip=""
|
|
if [[ -n "$filter_service" ]]; then
|
|
service_ip=$(net::resolve "$filter_service" 2>/dev/null | head -1 | cut -d: -f1) || true
|
|
[[ -z "$service_ip" ]] && log::error "Service not found: ${filter_service}" && return 1
|
|
fi
|
|
[[ -n "$filter_ip" ]] && service_ip="$filter_ip"
|
|
|
|
# ── Fetch data ──
|
|
local data=""
|
|
if ! $accept_only; then
|
|
data=$(json::activity_aggregate \
|
|
"$(ctx::fw_events_log)" "$(ctx::events_log)" \
|
|
"$(config::interface)" "$(ctx::net)" \
|
|
"$(ctx::clients)" "$(ctx::meta)" \
|
|
"$hours" "$filter_peer" "$service_ip" 2>/dev/null)
|
|
fi
|
|
|
|
local accept_data=""
|
|
if ! $drop_only; then
|
|
local since_arg="" ext_flag="0"
|
|
[[ "$hours" -gt 0 ]] && since_arg="${hours}h"
|
|
$external_only && ext_flag="1"
|
|
[[ -f "$(ctx::accept_events_log)" ]] && \
|
|
accept_data=$(json::accept_aggregate \
|
|
"$(ctx::accept_events_log)" "$(ctx::net)" "$(ctx::clients)" \
|
|
"$since_arg" "$filter_peer" "$ext_flag" 2>/dev/null)
|
|
fi
|
|
|
|
[[ -z "$data" && -z "$accept_data" ]] && \
|
|
log::wg_warning "No activity data found" && return 0
|
|
|
|
# ── Build accept maps ──
|
|
declare -gA _ACCEPT_PEER=()
|
|
declare -gA _ACCEPT_DEST_KEYS=()
|
|
declare -gA _ACCEPT_DEST=()
|
|
|
|
while IFS='|' read -r type rest; do
|
|
[[ -z "$type" ]] && continue
|
|
case "$type" in
|
|
peer)
|
|
local a_name a_bi a_bo a_pi a_po a_conns
|
|
IFS='|' read -r a_name a_bi a_bo a_pi a_po a_conns <<< "$rest"
|
|
_ACCEPT_PEER["$a_name"]="${a_bi}|${a_bo}|${a_pi}|${a_po}|${a_conns}"
|
|
;;
|
|
dest)
|
|
local d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count
|
|
IFS='|' read -r d_peer d_ip d_port d_proto d_bytes_orig d_bytes_reply d_count <<< "$rest"
|
|
local d_key="${d_peer}:${d_ip}:${d_port}:${d_proto}"
|
|
_ACCEPT_DEST["$d_key"]="${d_bytes_orig}|${d_bytes_reply}|${d_count}"
|
|
_ACCEPT_DEST_KEYS["$d_peer"]+="${d_key} "
|
|
;;
|
|
esac
|
|
done <<< "$accept_data"
|
|
|
|
# ── Measure column widths ──
|
|
local w_peer=16 w_count=1
|
|
|
|
while IFS='|' read -r type rest; do
|
|
case "$type" in
|
|
peer)
|
|
local name drops
|
|
name=$(echo "$rest" | cut -d'|' -f1)
|
|
drops=$(echo "$rest" | cut -d'|' -f4)
|
|
(( ${#name} > w_peer )) && w_peer=${#name}
|
|
(( ${#drops} > w_count )) && w_count=${#drops}
|
|
;;
|
|
service)
|
|
local svc_count
|
|
svc_count=$(echo "$rest" | cut -d'|' -f3)
|
|
(( ${#svc_count} > w_count )) && w_count=${#svc_count}
|
|
;;
|
|
esac
|
|
done <<< "$data"
|
|
|
|
for a_name in "${!_ACCEPT_PEER[@]}"; do
|
|
(( ${#a_name} > w_peer )) && w_peer=${#a_name}
|
|
local a_conns_val="${_ACCEPT_PEER[$a_name]##*|}"
|
|
(( ${#a_conns_val} > w_count )) && w_count=${#a_conns_val}
|
|
done
|
|
for key in "${!_ACCEPT_DEST[@]}"; do
|
|
local d_val="${_ACCEPT_DEST[$key]}"
|
|
local d_count_val="${d_val##*|}"
|
|
(( ${#d_count_val} > w_count )) && w_count=${#d_count_val}
|
|
done
|
|
|
|
(( w_peer += 2 ))
|
|
local drops_col=$(( w_peer + 30 ))
|
|
|
|
local hours_display="${hours}h"
|
|
[[ "$hours" == "0" ]] && hours_display="all time"
|
|
|
|
log::section "Activity Monitor (last ${hours_display})"
|
|
echo ""
|
|
|
|
if display::is_table "activity"; then
|
|
cmd::activity::_render_table "$data"
|
|
return 0
|
|
fi
|
|
|
|
# ── Accept dest inline renderer ──
|
|
_render_peer_accept_dests() {
|
|
local peer_name="$1"
|
|
local keys="${_ACCEPT_DEST_KEYS[$peer_name]:-}"
|
|
[[ -z "$keys" ]] && return 0
|
|
for d_key in $keys; do
|
|
local dest_stats="${_ACCEPT_DEST[$d_key]:-}"
|
|
[[ -z "$dest_stats" ]] && continue
|
|
local d_bytes_orig d_bytes_reply d_count
|
|
IFS='|' read -r d_bytes_orig d_bytes_reply d_count <<< "$dest_stats"
|
|
local rest_key="${d_key#${peer_name}:}"
|
|
local d_ip="${rest_key%%:*}"
|
|
local pp="${rest_key#*:}"
|
|
local d_port="${pp%%:*}"
|
|
local d_proto="${pp##*:}"
|
|
local spec="${d_ip}:${d_port}:${d_proto}"
|
|
local dest_display="${_DEST_RESOLVE_CACHE[$spec]:-${d_ip}:${d_port}/${d_proto}}"
|
|
ui::activity::accept_dest_row \
|
|
"$dest_display" "$d_bytes_orig" "$d_bytes_reply" \
|
|
"$d_count" "$drops_col" "$w_count"
|
|
done
|
|
}
|
|
|
|
declare -gA _DEST_RESOLVE_CACHE=()
|
|
local -a _dest_specs=()
|
|
for _dk in "${!_ACCEPT_DEST[@]}"; do
|
|
# key format: peer:ip:port:proto — strip peer prefix
|
|
local _rest="${_dk#*:}"
|
|
local _dip="${_rest%%:*}"
|
|
local _pp="${_rest#*:}"
|
|
local _dport="${_pp%%:*}"
|
|
local _dproto="${_pp##*:}"
|
|
local _spec="${_dip}:${_dport}:${_dproto}"
|
|
# Deduplicate
|
|
local _found=false
|
|
for _s in "${_dest_specs[@]:-}"; do
|
|
[[ "$_s" == "$_spec" ]] && _found=true && break
|
|
done
|
|
$_found || _dest_specs+=("$_spec")
|
|
done
|
|
|
|
if [[ ${#_dest_specs[@]} -gt 0 ]]; then
|
|
while IFS='|' read -r _spec _display; do
|
|
[[ -n "$_spec" ]] && _DEST_RESOLVE_CACHE["$_spec"]="$_display"
|
|
done < <(json::batch_resolve_dest "${_dest_specs[@]}" 2>/dev/null)
|
|
fi
|
|
|
|
local first_peer=true skip_peer=false current_name=""
|
|
local -a rendered_peers=()
|
|
|
|
# ── Main render loop (drop data) ──
|
|
while IFS='|' read -r record_type rest; do
|
|
case "$record_type" in
|
|
peer)
|
|
local name rx tx drops
|
|
IFS='|' read -r name rx tx drops <<< "$rest"
|
|
|
|
# Flush previous peer's accept dests
|
|
[[ -n "$current_name" ]] && ! $drop_only && \
|
|
_render_peer_accept_dests "$current_name"
|
|
|
|
skip_peer=false
|
|
current_name="$name"
|
|
local has_accept="${_ACCEPT_PEER[$name]:-}"
|
|
|
|
$first_peer || echo ""
|
|
first_peer=false
|
|
rendered_peers+=("$name")
|
|
|
|
local rx_fmt tx_fmt
|
|
rx_fmt=$(fmt::bytes "$rx")
|
|
tx_fmt=$(fmt::bytes "$tx")
|
|
local name_pad rx_pad tx_pad
|
|
name_pad=$(printf "%-${w_peer}s" "$name")
|
|
rx_pad=$(printf "%-10s" "$rx_fmt")
|
|
tx_pad=$(printf "%-10s" "$tx_fmt")
|
|
|
|
local drop_word="drops"
|
|
[[ "$drops" -eq 1 ]] && drop_word="drop"
|
|
|
|
# Always show peer name — either full row or name-only for accept_only
|
|
if $accept_only; then
|
|
printf " \033[1m%s\033[0m\n" "$name_pad"
|
|
else
|
|
ui::activity::peer_row \
|
|
"$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word" "$w_count"
|
|
fi
|
|
|
|
# Accept summary row
|
|
if [[ -n "$has_accept" ]] && ! $drop_only; then
|
|
local a_bi a_bo a_pi a_po a_conns
|
|
IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$has_accept"
|
|
ui::activity::accept_row \
|
|
"$name_pad" \
|
|
"$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
|
|
"$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
|
|
"$a_conns" "$w_count"
|
|
fi
|
|
;;
|
|
|
|
service)
|
|
$skip_peer && continue
|
|
$accept_only && continue
|
|
local peer dest_display drop_count
|
|
IFS='|' read -r peer dest_display drop_count <<< "$rest"
|
|
local svc_drop_word="drops"
|
|
[[ "$drop_count" -eq 1 ]] && svc_drop_word="drop"
|
|
ui::activity::service_row \
|
|
"$dest_display" "$drop_count" "$svc_drop_word" "$drops_col" "$w_count"
|
|
;;
|
|
esac
|
|
done <<< "$data"
|
|
|
|
# Flush last peer's accept dests
|
|
[[ -n "$current_name" ]] && ! $drop_only && \
|
|
_render_peer_accept_dests "$current_name"
|
|
|
|
# ── Accept-only peers (not in drop data) ──
|
|
if ! $drop_only; then
|
|
for a_name in $(echo "${!_ACCEPT_PEER[@]}" | tr ' ' '\n' | sort); do
|
|
# Skip already rendered
|
|
local already=false
|
|
for rp in "${rendered_peers[@]:-}"; do
|
|
[[ "$rp" == "$a_name" ]] && already=true && break
|
|
done
|
|
$already && continue
|
|
|
|
$first_peer || echo ""
|
|
first_peer=false
|
|
|
|
local a_stats="${_ACCEPT_PEER[$a_name]}"
|
|
local a_bi a_bo a_pi a_po a_conns
|
|
IFS='|' read -r a_bi a_bo a_pi a_po a_conns <<< "$a_stats"
|
|
local name_pad
|
|
name_pad=$(printf "%-${w_peer}s" "$a_name")
|
|
|
|
# Always show peer name
|
|
printf " \033[1m%s\033[0m\n" "$name_pad"
|
|
|
|
ui::activity::accept_row \
|
|
"$name_pad" \
|
|
"$(printf '%-10s' "$(fmt::bytes "$a_bi")")" \
|
|
"$(printf '%-10s' "$(fmt::bytes "$a_bo")")" \
|
|
"$a_conns" "$w_count"
|
|
_render_peer_accept_dests "$a_name"
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
function cmd::activity::_render_table() {
|
|
local data="${1:-}"
|
|
[[ -z "$data" ]] && return 0
|
|
|
|
ui::activity::header_table
|
|
local skip_peer=false
|
|
while IFS='|' read -r record_type rest; do
|
|
case "$record_type" in
|
|
peer)
|
|
local name rx tx drops
|
|
IFS='|' read -r name rx tx drops <<< "$rest"
|
|
skip_peer=false
|
|
local rx_fmt tx_fmt
|
|
rx_fmt=$(fmt::bytes "$rx")
|
|
tx_fmt=$(fmt::bytes "$tx")
|
|
ui::activity::peer_row_table "$name" "$rx_fmt" "$tx_fmt" "$drops" ""
|
|
;;
|
|
service)
|
|
$skip_peer && continue
|
|
local peer dest count
|
|
IFS='|' read -r peer dest count <<< "$rest"
|
|
ui::activity::service_row_table "$dest" "$count" "drops"
|
|
;;
|
|
esac
|
|
done <<< "$data"
|
|
}
|
|
|
|
|
|
function cmd::activity::_output_json() {
|
|
local hours="${1:-24}"
|
|
local data
|
|
data=$(json::activity_aggregate \
|
|
"$(ctx::fw_events_log)" "$(ctx::events_log)" \
|
|
"$(config::interface)" "$(ctx::net)" \
|
|
"$(ctx::clients)" "$(ctx::meta)" \
|
|
"$hours" "" "" 2>/dev/null)
|
|
|
|
local -a peers=()
|
|
local current_peer="" current_services=""
|
|
local -a current_svc_list=()
|
|
|
|
while IFS='|' read -r record_type rest; do
|
|
case "$record_type" in
|
|
peer)
|
|
# Flush previous peer
|
|
if [[ -n "$current_peer" ]]; then
|
|
local svc_array
|
|
svc_array=$(printf '%s\n' "${current_svc_list[@]:-}" | paste -sd ',' -)
|
|
peers+=("${current_peer},\"services\":[${svc_array:-}]}")
|
|
current_svc_list=()
|
|
fi
|
|
local name rx tx drops
|
|
IFS='|' read -r name rx tx drops <<< "$rest"
|
|
current_peer=$(printf '{"name":"%s","rx":%s,"tx":%s,"drops":%s' \
|
|
"$name" "$rx" "$tx" "$drops")
|
|
;;
|
|
service)
|
|
local peer dest count
|
|
IFS='|' read -r peer dest count <<< "$rest"
|
|
current_svc_list+=("$(printf '{"dest":"%s","drops":%s}' "$dest" "$count")")
|
|
;;
|
|
esac
|
|
done <<< "$data"
|
|
|
|
# Flush last peer
|
|
if [[ -n "$current_peer" ]]; then
|
|
local svc_array
|
|
svc_array=$(printf '%s\n' "${current_svc_list[@]:-}" | paste -sd ',' -)
|
|
peers+=("${current_peer},\"services\":[${svc_array:-}]}")
|
|
fi
|
|
|
|
local count=${#peers[@]}
|
|
local array
|
|
array=$(printf '%s\n' "${peers[@]:-}" | paste -sd ',' -)
|
|
printf '{"peers":[%s]}' "${array:-}" | json::envelope "activity" "$count"
|
|
}
|
|
|
|
function cmd::activity::_fetch_accept_data() {
|
|
local hours="${1:-24}" filter_peer="${2:-}" external_only="${3:-false}"
|
|
|
|
[[ ! -f "$(ctx::accept_events_log)" ]] && return 0
|
|
|
|
local since_arg=""
|
|
[[ "$hours" -gt 0 ]] && since_arg="${hours}h"
|
|
|
|
local ext_flag="0"
|
|
$external_only && ext_flag="1"
|
|
|
|
json::accept_aggregate \
|
|
"$(ctx::accept_events_log)" \
|
|
"$(ctx::net)" \
|
|
"$(ctx::clients)" \
|
|
"$since_arg" \
|
|
"$filter_peer" \
|
|
2>/dev/null
|
|
}
|
|
|
|
function cmd::activity::_build_accept_maps() {
|
|
local accept_data="${1:-}"
|
|
# Outputs to stdout as bash declare statements — use eval
|
|
# Sets: _ACCEPT_PEER[name]="bytes_in|bytes_out|packets_in|packets_out|conn_count"
|
|
# _ACCEPT_DEST[name:ip:port:proto]="bytes|count"
|
|
declare -gA _ACCEPT_PEER=()
|
|
declare -gA _ACCEPT_DEST=()
|
|
|
|
while IFS='|' read -r type rest; do
|
|
[[ -z "$type" ]] && continue
|
|
case "$type" in
|
|
peer)
|
|
local name bytes_in bytes_out packets_in packets_out conn_count
|
|
IFS='|' read -r name bytes_in bytes_out packets_in packets_out conn_count <<< "$rest"
|
|
_ACCEPT_PEER["$name"]="${bytes_in}|${bytes_out}|${packets_in}|${packets_out}|${conn_count}"
|
|
;;
|
|
dest)
|
|
local peer dst_ip dst_port proto bytes count
|
|
IFS='|' read -r peer dst_ip dst_port proto bytes count <<< "$rest"
|
|
_ACCEPT_DEST["${peer}:${dst_ip}:${dst_port}:${proto}"]="${bytes}|${count}"
|
|
;;
|
|
esac
|
|
done <<< "$accept_data"
|
|
} |