From 8ef8ea91b39c55ee5d0dbc3bedfe829443362187 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Wed, 13 May 2026 02:00:08 +0000 Subject: [PATCH] feat: group watch with peer filter, watch --peers flag, watch command cleanup --- commands/group.command.sh | 6 +- commands/watch.command.sh | 171 +++++++++++++++++++------------------- core/json_helper.py | 10 ++- 3 files changed, 96 insertions(+), 91 deletions(-) diff --git a/commands/group.command.sh b/commands/group.command.sh index 0e9145b..237eea7 100644 --- a/commands/group.command.sh +++ b/commands/group.command.sh @@ -845,8 +845,6 @@ function cmd::group::watch() { log::section "Live Monitor: Group '${name}'" printf " Monitoring: %s\n\n" "${peers_list[*]}" - load_command logs - # Use follow mode — it shows all peers but user knows context - # Future: add --peers flag to follow for multi-peer filter - cmd::logs::follow "" "" "" false false "$peer_filter" + load_command watch + cmd::watch::run --peers "$peer_filter" } diff --git a/commands/watch.command.sh b/commands/watch.command.sh index 3f74f21..17a166d 100644 --- a/commands/watch.command.sh +++ b/commands/watch.command.sh @@ -7,6 +7,7 @@ function cmd::watch::on_load() { flag::register --type flag::register --name + flag::register --peers flag::register --blocked flag::register --restricted flag::register --allowed @@ -26,6 +27,7 @@ Shows handshakes from active clients and connection attempts from blocked client Options: --type Filter by device type --name Filter by client name + --peers Filter by comma-separated peer names (used by group watch) --allowed Show only allowed client handshakes --restricted Show only restricted client events --blocked Show only blocked client attempts @@ -44,17 +46,14 @@ EOF # ============================================ function cmd::watch::format_event() { - local ts="$1" - local client="$2" - local endpoint="$3" - local event="$4" - local status="$5" + local ts="${1:-}" client="${2:-}" endpoint="${3:-}" + local event="${4:-}" status="${5:-}" local event_color case "$event" in - attempt) event_color="\033[1;31m" ;; # red - handshake) event_color="\033[1;32m" ;; # green - *) event_color="\033[0;37m" ;; # grey + attempt) event_color="\033[1;31m" ;; + handshake) event_color="\033[1;32m" ;; + *) event_color="\033[0;37m" ;; esac local status_str="" @@ -62,7 +61,6 @@ function cmd::watch::format_event() { case "$status" in blocked) status_str=" \033[1;31mblocked\033[0m" ;; allowed) status_str=" \033[1;32mallowed\033[0m" ;; - *) status_str="" ;; esac fi @@ -77,16 +75,28 @@ function cmd::watch::header() { 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 # ============================================ -declare -A _WATCH_LAST_HANDSHAKES=() - function cmd::watch::poll_handshakes() { - local filter_name="$1" - local filter_type="$2" - local allowed_only="$3" + local filter_name="${1:-}" filter_type="${2:-}" + local allowed_only="${3:-false}" + local filter_peers="${4:-}" + + local peer_set=() + [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers" while IFS= read -r line; do local public_key ts @@ -113,10 +123,12 @@ function cmd::watch::poll_handshakes() { # Apply filters [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue + cmd::watch::_peer_in_filter "$client_name" "${peer_set[@]}" || continue if [[ -n "$filter_type" ]]; then 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 subnet=$(config::subnet_for "$filter_type") 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="0" [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file") + if [[ "$ts" != "$prev_ts" ]]; then echo "$ts" > "$prev_ts_file" @@ -149,74 +162,63 @@ function cmd::watch::poll_handshakes() { # ============================================ function cmd::watch::tail_events() { - local filter_name="$1" - local filter_type="$2" - local blocked_only="$3" - local restricted_only="$4" - local allowed_only="$5" + local filter_name="${1:-}" filter_type="${2:-}" + local blocked_only="${3:-false}" restricted_only="${4:-false}" + local allowed_only="${5:-false}" filter_peers="${6:-}" + + local peer_set=() + [[ -n "$filter_peers" ]] && IFS=',' read -ra peer_set <<< "$filter_peers" declare -A _WATCH_LAST_ATTEMPT=() - tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do - [[ -z "$line" ]] && continue + tail -f "$(ctx::events_log)" 2>/dev/null | while IFS= read -r line; do + [[ -z "$line" ]] && continue - local event_data - event_data=$(json::parse_event "$line") + local event_data + event_data=$(json::parse_event "$line") + [[ -z "$event_data" ]] && continue - [[ -z "$event_data" ]] && continue + local ts client endpoint event + IFS="|" read -r ts client endpoint event <<< "$event_data" - local ts client endpoint event - IFS="|" read -r ts client endpoint event <<< "$event_data" + # Apply filters + [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue + cmd::watch::_peer_in_filter "$client" "${peer_set[@]}" || continue - if $restricted_only; then - local conf - conf="$(ctx::clients)/${client}.conf" - [[ -f "$conf" ]] || continue - cmd::list::is_restricted "$client" || continue - fi + if [[ -n "$filter_type" ]]; then + local conf + conf="$(ctx::clients)/${client}.conf" + [[ -f "$conf" ]] || continue + local ip + ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) + local subnet + subnet=$(config::subnet_for "$filter_type") + string::starts_with "$ip" "$subnet" || continue + fi - # Apply filters - [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue + if $restricted_only; then + local conf + conf="$(ctx::clients)/${client}.conf" + [[ -f "$conf" ]] || continue + cmd::list::is_restricted "$client" || continue + fi - if [[ -n "$filter_type" ]]; then - local conf - conf="$(ctx::clients)/${client}.conf" - [[ -f "$conf" ]] || continue - local ip - ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) - local subnet - subnet=$(config::subnet_for "$filter_type") - string::starts_with "$ip" "$subnet" || continue - fi + $allowed_only && [[ "$event" != "handshake" ]] && continue - # Filter by status - if $allowed_only && [[ "$event" != "handshake" ]]; then - continue - fi + local formatted_ts + formatted_ts=$(fmt::datetime_iso "$ts") - if $restricted_only; then - local conf - conf="$(ctx::clients)/${client}.conf" - [[ -f "$conf" ]] || continue - cmd::list::is_restricted "$client" || continue - fi + # Dedup attempts + local now + now=$(date +%s) + local safe_client="${client//[-.]/_}" + local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}" + local diff=$(( now - last )) + (( diff < 30 )) && continue + _WATCH_LAST_ATTEMPT[$safe_client]="$now" - local formatted_ts - formatted_ts=$(fmt::datetime_iso "$ts") - - # Before printing the event - local now - now=$(date +%s) - local safe_client="${client//[-.]/_}" - local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}" - local diff=$(( now - last )) - if (( diff < 30 )); then - continue - fi - _WATCH_LAST_ATTEMPT[$safe_client]="$now" - - cmd::watch::format_event \ - "$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked" + cmd::watch::format_event \ + "$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked" done } @@ -225,19 +227,16 @@ function cmd::watch::tail_events() { # ============================================ function cmd::watch::run() { - local filter_name="" - local filter_type="" - local blocked_only=false - local allowed_only=false - local restricted_only=false + local filter_name="" filter_type="" filter_peers="" + local blocked_only=false allowed_only=false restricted_only=false - # Clean up any stale temp files from previous runs rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true while [[ $# -gt 0 ]]; do case "$1" in --name) filter_name="$2"; shift 2 ;; --type) filter_type="$2"; shift 2 ;; + --peers) filter_peers="$2"; shift 2 ;; --blocked) blocked_only=true; shift ;; --allowed) allowed_only=true; shift ;; --restricted) restricted_only=true; shift ;; @@ -252,25 +251,25 @@ function cmd::watch::run() { cmd::watch::header - # Start event tailer in background unless --blocked not set if ! $blocked_only && ! $restricted_only; then - # Poll handshakes every 5 seconds in background ( 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 done ) & local poller_pid=$! fi - # Tail events log for blocked attempts - cmd::watch::tail_events "$filter_name" "$filter_type" "$blocked_only" "$restricted_only" "$allowed_only" & + cmd::watch::tail_events \ + "$filter_name" "$filter_type" \ + "$blocked_only" "$restricted_only" \ + "$allowed_only" "$filter_peers" & local tailer_pid=$! - # Trap Ctrl+C to clean up background processes - trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM + trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \ + rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM - # Keep main process alive wait -} +} \ No newline at end of file diff --git a/core/json_helper.py b/core/json_helper.py index 79798c1..c518fbf 100644 --- a/core/json_helper.py +++ b/core/json_helper.py @@ -355,7 +355,9 @@ def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_pe proto_map = {1: 'icmp', 6: 'tcp', 17: 'udp'} 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 ip_to_name = {} for conf in glob.glob(f"{clients_dir}/*.conf"): @@ -396,6 +398,12 @@ def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir, filter_pe continue if filter_ip and src != filter_ip: 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', '—') port = e.get('dest_port', '') proto_num = e.get('ip.protocol', 0)