feat: group watch with peer filter, watch --peers flag, watch command cleanup

This commit is contained in:
Nuno Duque Nunes 2026-05-13 02:00:08 +00:00
parent b1bca613de
commit 8ef8ea91b3
3 changed files with 96 additions and 91 deletions

View file

@ -845,8 +845,6 @@ function cmd::group::watch() {
log::section "Live Monitor: Group '${name}'" log::section "Live Monitor: Group '${name}'"
printf " Monitoring: %s\n\n" "${peers_list[*]}" printf " Monitoring: %s\n\n" "${peers_list[*]}"
load_command logs load_command watch
# Use follow mode — it shows all peers but user knows context cmd::watch::run --peers "$peer_filter"
# Future: add --peers flag to follow for multi-peer filter
cmd::logs::follow "" "" "" false false "$peer_filter"
} }

View file

@ -7,6 +7,7 @@
function cmd::watch::on_load() { function cmd::watch::on_load() {
flag::register --type flag::register --type
flag::register --name flag::register --name
flag::register --peers
flag::register --blocked flag::register --blocked
flag::register --restricted flag::register --restricted
flag::register --allowed flag::register --allowed
@ -26,6 +27,7 @@ Shows handshakes from active clients and connection attempts from blocked client
Options: Options:
--type <type> Filter by device type --type <type> Filter by device type
--name <name> Filter by client name --name <name> Filter by client name
--peers <list> Filter by comma-separated peer names (used by group watch)
--allowed Show only allowed client handshakes --allowed Show only allowed client handshakes
--restricted Show only restricted client events --restricted Show only restricted client events
--blocked Show only blocked client attempts --blocked Show only blocked client attempts
@ -44,17 +46,14 @@ EOF
# ============================================ # ============================================
function cmd::watch::format_event() { function cmd::watch::format_event() {
local ts="$1" local ts="${1:-}" client="${2:-}" endpoint="${3:-}"
local client="$2" local event="${4:-}" status="${5:-}"
local endpoint="$3"
local event="$4"
local status="$5"
local event_color local event_color
case "$event" in case "$event" in
attempt) event_color="\033[1;31m" ;; # red attempt) event_color="\033[1;31m" ;;
handshake) event_color="\033[1;32m" ;; # green handshake) event_color="\033[1;32m" ;;
*) event_color="\033[0;37m" ;; # grey *) event_color="\033[0;37m" ;;
esac esac
local status_str="" local status_str=""
@ -62,7 +61,6 @@ function cmd::watch::format_event() {
case "$status" in case "$status" in
blocked) status_str=" \033[1;31mblocked\033[0m" ;; blocked) status_str=" \033[1;31mblocked\033[0m" ;;
allowed) status_str=" \033[1;32mallowed\033[0m" ;; allowed) status_str=" \033[1;32mallowed\033[0m" ;;
*) status_str="" ;;
esac esac
fi fi
@ -77,16 +75,28 @@ function cmd::watch::header() {
printf " %s\n\n" "$(printf '─%.0s' {1..85})" printf " %s\n\n" "$(printf '─%.0s' {1..85})"
} }
function cmd::watch::_peer_in_filter() {
local peer="$1"
shift
local peer_set=("$@")
[[ ${#peer_set[@]} -eq 0 ]] && return 0 # no filter = all pass
for p in "${peer_set[@]}"; do
[[ "$p" == "$peer" ]] && return 0
done
return 1
}
# ============================================ # ============================================
# Handshake Poller # Handshake Poller
# ============================================ # ============================================
declare -A _WATCH_LAST_HANDSHAKES=()
function cmd::watch::poll_handshakes() { function cmd::watch::poll_handshakes() {
local filter_name="$1" local filter_name="${1:-}" filter_type="${2:-}"
local filter_type="$2" local allowed_only="${3:-false}"
local allowed_only="$3" local filter_peers="${4:-}"
local peer_set=()
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
while IFS= read -r line; do while IFS= read -r line; do
local public_key ts local public_key ts
@ -113,10 +123,12 @@ function cmd::watch::poll_handshakes() {
# Apply filters # Apply filters
[[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue
cmd::watch::_peer_in_filter "$client_name" "${peer_set[@]}" || continue
if [[ -n "$filter_type" ]]; then if [[ -n "$filter_type" ]]; then
local ip local ip
ip=$(grep "^Address" "$(ctx::clients)/${client_name}.conf" | awk '{print $3}' | cut -d'/' -f1) ip=$(grep "^Address" "$(ctx::clients)/${client_name}.conf" \
| awk '{print $3}' | cut -d'/' -f1)
local subnet local subnet
subnet=$(config::subnet_for "$filter_type") subnet=$(config::subnet_for "$filter_type")
string::starts_with "$ip" "$subnet" || continue string::starts_with "$ip" "$subnet" || continue
@ -128,6 +140,7 @@ function cmd::watch::poll_handshakes() {
local prev_ts_file="/tmp/wgctl_hs_${safe_key}" local prev_ts_file="/tmp/wgctl_hs_${safe_key}"
local prev_ts="0" local prev_ts="0"
[[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file") [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file")
if [[ "$ts" != "$prev_ts" ]]; then if [[ "$ts" != "$prev_ts" ]]; then
echo "$ts" > "$prev_ts_file" echo "$ts" > "$prev_ts_file"
@ -149,34 +162,28 @@ function cmd::watch::poll_handshakes() {
# ============================================ # ============================================
function cmd::watch::tail_events() { function cmd::watch::tail_events() {
local filter_name="$1" local filter_name="${1:-}" filter_type="${2:-}"
local filter_type="$2" local blocked_only="${3:-false}" restricted_only="${4:-false}"
local blocked_only="$3" local allowed_only="${5:-false}" filter_peers="${6:-}"
local restricted_only="$4"
local allowed_only="$5" local peer_set=()
[[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers"
declare -A _WATCH_LAST_ATTEMPT=() declare -A _WATCH_LAST_ATTEMPT=()
tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do tail -f "$(ctx::events_log)" 2>/dev/null | while IFS= read -r line; do
[[ -z "$line" ]] && continue [[ -z "$line" ]] && continue
local event_data local event_data
event_data=$(json::parse_event "$line") event_data=$(json::parse_event "$line")
[[ -z "$event_data" ]] && continue [[ -z "$event_data" ]] && continue
local ts client endpoint event local ts client endpoint event
IFS="|" read -r ts client endpoint event <<< "$event_data" IFS="|" read -r ts client endpoint event <<< "$event_data"
if $restricted_only; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
cmd::list::is_restricted "$client" || continue
fi
# Apply filters # Apply filters
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue
cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue
if [[ -n "$filter_type" ]]; then if [[ -n "$filter_type" ]]; then
local conf local conf
@ -189,11 +196,6 @@ function cmd::watch::tail_events() {
string::starts_with "$ip" "$subnet" || continue string::starts_with "$ip" "$subnet" || continue
fi fi
# Filter by status
if $allowed_only && [[ "$event" != "handshake" ]]; then
continue
fi
if $restricted_only; then if $restricted_only; then
local conf local conf
conf="$(ctx::clients)/${client}.conf" conf="$(ctx::clients)/${client}.conf"
@ -201,18 +203,18 @@ function cmd::watch::tail_events() {
cmd::list::is_restricted "$client" || continue cmd::list::is_restricted "$client" || continue
fi fi
$allowed_only && [[ "$event" != "handshake" ]] && continue
local formatted_ts local formatted_ts
formatted_ts=$(fmt::datetime_iso "$ts") formatted_ts=$(fmt::datetime_iso "$ts")
# Before printing the event # Dedup attempts
local now local now
now=$(date +%s) now=$(date +%s)
local safe_client="${client//[-.]/_}" local safe_client="${client//[-.]/_}"
local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}" local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}"
local diff=$(( now - last )) local diff=$(( now - last ))
if (( diff < 30 )); then (( diff < 30 )) && continue
continue
fi
_WATCH_LAST_ATTEMPT[$safe_client]="$now" _WATCH_LAST_ATTEMPT[$safe_client]="$now"
cmd::watch::format_event \ cmd::watch::format_event \
@ -225,19 +227,16 @@ function cmd::watch::tail_events() {
# ============================================ # ============================================
function cmd::watch::run() { function cmd::watch::run() {
local filter_name="" local filter_name="" filter_type="" filter_peers=""
local filter_type="" local blocked_only=false allowed_only=false restricted_only=false
local blocked_only=false
local allowed_only=false
local restricted_only=false
# Clean up any stale temp files from previous runs
rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--name) filter_name="$2"; shift 2 ;; --name) filter_name="$2"; shift 2 ;;
--type) filter_type="$2"; shift 2 ;; --type) filter_type="$2"; shift 2 ;;
--peers) filter_peers="$2"; shift 2 ;;
--blocked) blocked_only=true; shift ;; --blocked) blocked_only=true; shift ;;
--allowed) allowed_only=true; shift ;; --allowed) allowed_only=true; shift ;;
--restricted) restricted_only=true; shift ;; --restricted) restricted_only=true; shift ;;
@ -252,25 +251,25 @@ function cmd::watch::run() {
cmd::watch::header cmd::watch::header
# Start event tailer in background unless --blocked not set
if ! $blocked_only && ! $restricted_only; then if ! $blocked_only && ! $restricted_only; then
# Poll handshakes every 5 seconds in background
( (
while true; do while true; do
cmd::watch::poll_handshakes "$filter_name" "$filter_type" "$allowed_only" cmd::watch::poll_handshakes \
"$filter_name" "$filter_type" "$allowed_only" "$filter_peers"
sleep 5 sleep 5
done done
) & ) &
local poller_pid=$! local poller_pid=$!
fi fi
# Tail events log for blocked attempts cmd::watch::tail_events \
cmd::watch::tail_events "$filter_name" "$filter_type" "$blocked_only" "$restricted_only" "$allowed_only" & "$filter_name" "$filter_type" \
"$blocked_only" "$restricted_only" \
"$allowed_only" "$filter_peers" &
local tailer_pid=$! local tailer_pid=$!
# Trap Ctrl+C to clean up background processes trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM
# Keep main process alive
wait wait
} }

View file

@ -355,6 +355,8 @@ def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_pe
proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'} proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'}
peer_filter = set(filter_peers.split(',')) if filter_peers else set() peer_filter = set(filter_peers.split(',')) if filter_peers else set()
import sys
print(f"DEBUG follow_logs: peer_filter={peer_filter}", file=sys.stderr, flush=True)
# Build ip->name map # Build ip->name map
ip_to_name = {} ip_to_name = {}
@ -396,6 +398,12 @@ def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_pe
continue continue
if filter_ip and src != filter_ip: if filter_ip and src != filter_ip:
continue continue
# Filter by peer names if specified
if peer_filter:
client_name = ip_to_name.get(src, '')
if client_name not in peer_filter:
continue
dst = e.get('dest_ip', '') dst = e.get('dest_ip', '')
port = e.get('dest_port', '') port = e.get('dest_port', '')
proto_num = e.get('ip.protocol', 0) proto_num = e.get('ip.protocol', 0)