wgctl/commands/watch.command.sh

283 lines
7.6 KiB
Bash

#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::watch::on_load() {
flag::register --type
flag::register --name
flag::register --blocked
flag::register --restricted
flag::register --allowed
}
# ============================================
# Help
# ============================================
function cmd::watch::help() {
cat <<EOF
Usage: wgctl watch [options]
Live monitor of WireGuard activity.
Shows handshakes from active clients and connection attempts from blocked clients.
Options:
--type <type> Filter by device type
--name <name> Filter by client name
--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"
local client="$2"
local endpoint="$3"
local event="$4"
local 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
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" ;;
*) status_str="" ;;
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})"
}
# ============================================
# Handshake Poller
# ============================================
declare -A _WATCH_LAST_HANDSHAKES=()
function cmd::watch::poll_handshakes() {
local filter_name="$1"
local filter_type="$2"
local allowed_only="$3"
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
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"
local filter_type="$2"
local blocked_only="$3"
local restricted_only="$4"
local allowed_only="$5"
declare -A _WATCH_LAST_ATTEMPT=()
tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do
[[ -z "$line" ]] && continue
local event_data
event_data=$(python3 -c "
import json, sys
try:
e = json.loads('${line//\'/\'\\\'\'}')
print(e.get('timestamp',''), e.get('client',''), e.get('endpoint',''), e.get('event',''))
except:
pass
" 2>/dev/null)
[[ -z "$event_data" ]] && continue
if $restricted_only; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
cmd::list::is_restricted "$client" || continue
fi
local ts client endpoint event
read -r ts client endpoint event <<< "$event_data"
# Apply filters
[[ -n "$filter_name" && "$client" != "$filter_name" ]] && 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
# Filter by status
if $allowed_only && [[ "$event" != "handshake" ]]; then
continue
fi
if $restricted_only; then
local conf
conf="$(ctx::clients)/${client}.conf"
[[ -f "$conf" ]] || continue
cmd::list::is_restricted "$client" || continue
fi
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"
done
}
# ============================================
# Run
# ============================================
function cmd::watch::run() {
local filter_name=""
local filter_type=""
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
while [[ $# -gt 0 ]]; do
case "$1" in
--name) filter_name="$2"; shift 2 ;;
--type) filter_type="$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
# 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"
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" &
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
# Keep main process alive
wait
}