#!/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 --dropped command::mixin json_output } # ============================================ # Help # ============================================ function cmd::activity::help() { cat < Filter by peer name --service Filter by service (e.g. truenas, proxmox:web-ui) --ip Filter by destination IP --hours Time window in hours (default: 24, 0 = all time) --type Filter by device type (combined with --peer) --dropped Show only peers with at least one drop Examples: wgctl activity wgctl activity --dropped 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 dropped_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 ;; --dropped) dropped_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 # Resolve peer name if type provided if [[ -n "$filter_peer" && -n "$filter_type" ]]; then filter_peer=$(peers::resolve_and_require "$filter_peer" "$filter_type") || return 1 fi # Resolve --service to IP local service_ip="" if [[ -n "$filter_service" ]]; then service_ip=$(net::resolve "$filter_service" 2>/dev/null | head -1 | cut -d: -f1) || true if [[ -z "$service_ip" ]]; then log::error "Service not found: ${filter_service}" return 1 fi fi [[ -n "$filter_ip" ]] && service_ip="$filter_ip" # Fetch aggregated data local data 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) if [[ -z "$data" ]]; then log::wg_warning "No activity data found" return 0 fi # Measure column widths local w_peer=16 w_drops=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_drops )) && w_drops=${#drops} ;; service) local count count=$(echo "$rest" | cut -d'|' -f3) (( ${#count} > w_drops )) && w_drops=${#count} ;; esac done <<< "$data" (( w_peer += 2 )) # Compute column where drop count starts on peer row: # " " (2) + name (w_peer) + " ↓" (3) + rx (10) + " ↑" (3) + tx (10) + " " (2) # ↓ and ↑ are multi-byte (3 bytes, 1 visible) — 2 extra bytes each # Visible: 2 + w_peer + 2+1 + 10 + 2+1 + 10 + 2 = w_peer + 30 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 "" local first_peer=true 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 if $dropped_only && [[ "$drops" -eq 0 ]]; then skip_peer=true continue fi $first_peer || echo "" first_peer=false 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" ui::activity::peer_row \ "$name_pad" "$rx_pad" "$tx_pad" "$drops" "$drop_word" "$w_drops" ;; service) $skip_peer && 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_drops" ;; esac done <<< "$data" echo "" } 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" }