#!/usr/bin/env bash # ============================================ # Lifecycle # ============================================ function cmd::watch::on_load() { flag::register --type flag::register --name flag::register --peers flag::register --blocked flag::register --restricted flag::register --allowed flag::register --raw } # ============================================ # Help # ============================================ function cmd::watch::help() { cat < Filter by client name --type Filter by device type --blocked Show only blocked peer attempts --allowed Show only handshakes --restricted Show only firewall drop events --raw Show raw IPs without host/service resolution Examples: wgctl watch wgctl watch --name phone-nuno wgctl watch --blocked wgctl watch --type phone EOF } # ============================================ # Run # ============================================ function cmd::watch::run() { local filter_name="" filter_type="" filter_peers="" local blocked_only=false allowed_only=false restricted_only=false local raw=false 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 ;; --raw) _WGCTL_RAW=true; shift ;; --help) cmd::watch::help; return ;; *) log::error "Unknown flag: $1" cmd::watch::help return 1 ;; esac done log::section "wgctl — Live Monitor (Ctrl+C to stop)" printf "\n" monitor::live "$filter_name" "$filter_type" "$filter_peers" \ "$blocked_only" "$restricted_only" "$allowed_only" "$raw" } # ============================================ # Handshake Poller # ============================================ function cmd::watch::_poll_handshakes() { local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" local w_client="${4:-20}" w_dest="${5:-18}" # Collect rows with sort key before printing local -a rows=() while IFS= read -r line; do local public_key ts public_key=$(echo "$line" | awk '{print $1}') ts=$(echo "$line" | awk '{print $2}') [[ -z "$ts" || "$ts" == "0" ]] && continue local client_name="" for conf in "$(ctx::clients)"/*.conf; do [[ -f "$conf" ]] || continue local cname cname=$(basename "$conf" .conf) local key key=$(keys::public "$cname" 2>/dev/null || echo "") if [[ "$key" == "$public_key" ]]; then client_name="$cname" break fi done [[ -z "$client_name" ]] && continue [[ -n "$filter_name" && "$client_name" != "$filter_name" ]] && continue local safe_key safe_key=$(echo "$public_key" | md5sum | cut -d' ' -f1) local prev_ts_file="/tmp/wgctl_hs_${safe_key}" local prev_ts="0" [[ -f "$prev_ts_file" ]] && prev_ts=$(cat "$prev_ts_file") [[ "$ts" == "$prev_ts" ]] && continue local gap=$(( ts - ${prev_ts:-0} )) echo "$ts" > "$prev_ts_file" (( gap < ${WG_HANDSHAKE_CHECK_TIME_SEC:-300} )) && continue local ts_fmt ts_fmt=$(fmt::datetime_short "$ts") # Resolve endpoint — try wg show first, fall back to endpoint cache local endpoint endpoint=$(monitor::endpoint_for_key "$public_key") if [[ -z "$endpoint" ]]; then endpoint=$(monitor::get_cached_endpoint "$client_name") fi local endpoint_display endpoint_display=$(resolve::ip "${endpoint:-}") [[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:--}" # Build row with ts prefix for sorting local row row=$(ui::watch::wg_row "$ts_fmt" "$client_name" "$endpoint_display" "handshake" \ "$w_client" "$w_dest") rows+=("${ts}|${row}") done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) # Sort by ts descending (most recent first) and print if [[ ${#rows[@]} -gt 0 ]]; then printf '%s\n' "${rows[@]}" | sort -t'|' -k1,1rn | while IFS= read -r entry; do printf "%s\n" "${entry#*|}" done fi } # ============================================ # Event Tailer # ============================================ function cmd::watch::_tail_events() { local filter_name="${1:-}" filter_type="${2:-}" filter_peers="${3:-}" local blocked_only="${4:-false}" restricted_only="${5:-false}" allowed_only="${6:-false}" local w_client="${7:-20}" w_dest="${8:-18}" # Build ip->name map declare -A ip_to_name=() while IFS= read -r conf; do local cname cname=$(basename "$conf" .conf) local ip ip=$(grep "^Address" "$conf" 2>/dev/null | awk '{print $3}' | cut -d'/' -f1) [[ -n "$ip" && -n "$cname" ]] && ip_to_name["$ip"]="$cname" done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null) declare -A _WATCH_LAST_FW=() declare -A _WATCH_LAST_WG=() local source_file source_file=$(mktemp) echo "wg" > "$source_file" trap "rm -f '$source_file'" EXIT tail -f "$(ctx::events_log)" "$(ctx::fw_events_log)" 2>/dev/null \ | while IFS= read -r line; do [[ -z "$line" ]] && continue if [[ "$line" == "==> "* ]]; then [[ "$line" == *"fw_events"* ]] && echo "fw" > "$source_file" || echo "wg" > "$source_file" continue fi local source source=$(cat "$source_file") if [[ "$source" == "fw" ]]; then $allowed_only && continue local fw_data fw_data=$(python3 "$(ctx::json_helper)" parse_fw_event "$line" 2>/dev/null) || continue [[ -z "$fw_data" ]] && continue local ts src_ip dest_ip dest_port proto IFS='|' read -r ts src_ip dest_ip dest_port proto <<< "$fw_data" [[ -z "$src_ip" ]] && continue local client="${ip_to_name[$src_ip]:-$src_ip}" [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue local fw_key="${src_ip}:${dest_ip}:${dest_port}:${proto}" local now; now=$(date +%s) local window=30 [[ "$proto" == "17" || "$proto" == "udp" ]] && window=10 [[ "$proto" == "1" || "$proto" == "icmp" ]] && window=5 local last="${_WATCH_LAST_FW[$fw_key]:-0}" (( now - last < window )) && continue _WATCH_LAST_FW["$fw_key"]="$now" local ts_fmt ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") local dest_display dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto") ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest" else $restricted_only && continue local ev_data ev_data=$(python3 "$(ctx::json_helper)" parse_event "$line" 2>/dev/null) || continue [[ -z "$ev_data" ]] && continue local ts client endpoint event IFS='|' read -r ts client endpoint event <<< "$ev_data" [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue $blocked_only && [[ "$event" != "attempt" ]] && continue $allowed_only && [[ "$event" != "handshake" ]] && continue local wg_key="${client}:${endpoint}:${event}" local now; now=$(date +%s) local last="${_WATCH_LAST_WG[$wg_key]:-0}" local window=30 [[ "$event" == "handshake" ]] && window="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" (( now - last < window )) && continue _WATCH_LAST_WG["$wg_key"]="$now" local ts_fmt ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") # Resolve endpoint — fall back to endpoint cache if empty local endpoint_resolved endpoint_resolved=$(resolve::ip "${endpoint:-}") if [[ -z "$endpoint_resolved" && -n "$endpoint" ]]; then endpoint_resolved="$endpoint" fi [[ -z "$endpoint_resolved" ]] && endpoint_resolved="-" ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_resolved" "$event" \ "$w_client" "$w_dest" fi done rm -f "$source_file" }