#!/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 } # ============================================ # Help # ============================================ function cmd::watch::help() { cat < 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 Examples: wgctl watch wgctl watch --blocked wgctl watch --allowed wgctl watch --type phone wgctl watch --name phone-nuno EOF } # ============================================ # Helpers # ============================================ function cmd::watch::format_event() { local ts="${1:-}" client="${2:-}" endpoint="${3:-}" local event="${4:-}" status="${5:-}" local event_color case "$event" in attempt) event_color="\033[1;31m" ;; handshake) event_color="\033[1;32m" ;; *) event_color="\033[0;37m" ;; esac local status_str="" if [[ -n "$status" ]]; then case "$status" in blocked) status_str=" \033[1;31mblocked\033[0m" ;; allowed) status_str=" \033[1;32mallowed\033[0m" ;; esac fi printf " %-20s %-25s %-20s ${event_color}%-12s\033[0m%b\n" \ "$ts" "$client" "${endpoint:-—}" "$event" "$status_str" } function cmd::watch::header() { log::section "wgctl — Live Monitor (Ctrl+C to stop)" printf "\n %-20s %-25s %-20s %-12s %s\n" \ "TIME" "CLIENT" "ENDPOINT" "EVENT" "STATUS" 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 # ============================================ function cmd::watch::poll_handshakes() { 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 public_key=$(echo "$line" | awk '{print $1}') ts=$(echo "$line" | awk '{print $2}') [[ -z "$ts" || "$ts" == "0" ]] && continue # Find client by public key local client_name="" for conf in "$(ctx::clients)"/*.conf; do [[ -f "$conf" ]] || continue local name name=$(basename "$conf" .conf) local key key=$(keys::public "$name" 2>/dev/null || echo "") if [[ "$key" == "$public_key" ]]; then client_name="$name" break fi done [[ -z "$client_name" ]] && continue # 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) local subnet subnet=$(config::subnet_for "$filter_type") string::starts_with "$ip" "$subnet" || continue fi # Only emit if handshake is new 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") if [[ "$ts" != "$prev_ts" ]]; then echo "$ts" > "$prev_ts_file" local formatted_ts formatted_ts=$(fmt::datetime "$ts") local endpoint endpoint=$(monitor::endpoint_for_key "$public_key") cmd::watch::format_event \ "$formatted_ts" "$client_name" "${endpoint:-—}" "handshake" "allowed" fi done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) } # ============================================ # Event Tailer # ============================================ function cmd::watch::tail_events() { 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::events_log)" 2>/dev/null | while IFS= read -r line; do [[ -z "$line" ]] && continue local event_data event_data=$(json::parse_event "$line") [[ -z "$event_data" ]] && continue 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 [[ -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 if $restricted_only; then local conf conf="$(ctx::clients)/${client}.conf" [[ -f "$conf" ]] || continue cmd::list::is_restricted "$client" || continue fi $allowed_only && [[ "$event" != "handshake" ]] && continue local formatted_ts formatted_ts=$(fmt::datetime_iso "$ts") # 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" cmd::watch::format_event \ "$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked" done } # ============================================ # Run # ============================================ function cmd::watch::run() { local filter_name="" filter_type="" filter_peers="" local blocked_only=false allowed_only=false restricted_only=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 ;; --help) cmd::watch::help; return ;; *) log::error "Unknown flag: $1" cmd::watch::help return 1 ;; esac done cmd::watch::header if ! $blocked_only && ! $restricted_only; then ( while true; do cmd::watch::poll_handshakes \ "$filter_name" "$filter_type" "$allowed_only" "$filter_peers" sleep 5 done ) & local poller_pid=$! fi cmd::watch::tail_events \ "$filter_name" "$filter_type" \ "$blocked_only" "$restricted_only" \ "$allowed_only" "$filter_peers" & local tailer_pid=$! trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \ rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; echo ''; exit 0" INT TERM wait }