wgctl/commands/watch.command.sh
Nuno Duque Nunes 3058750c3d cleanup: ui::pad_mb removal, watch alignment fixes, endpoint cache fallback
- ui::rule::list_row: inline padding math replaces ui::pad_mb (major perf gain)
- ui::fw_row/wg_row: drop ui::pad_mb for fw/wg labels (always 2 chars)
- watch: endpoint fallback via monitor::get_cached_endpoint
- watch: _poll_handshakes sorts by ts descending (most recent first)
- watch: empty endpoint uses - not — (avoids multi-byte padding issues)
- ui.sh: UTF-8 extra byte constants (_UI_EMDASH_EXTRA, _UI_ARROW_EXTRA, _UI_BULLET_EXTRA)
2026-05-25 15:09:13 +00:00

286 lines
No EOL
8.7 KiB
Bash

#!/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 <<EOF
Usage: wgctl watch [options]
Live monitor of WireGuard activity.
- Handshakes from connected peers (green)
- Connection attempts from blocked peers (red)
- Firewall drops from restricted peers (red)
Options:
--name <name> Filter by client name
--type <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"
local w_client=20 w_dest=18
if ! $blocked_only && ! $restricted_only; then
(
while true; do
cmd::watch::_poll_handshakes \
"$filter_name" "$filter_type" "$filter_peers" "$w_client" "$w_dest"
sleep 5
done
) &
local poller_pid=$!
fi
cmd::watch::_tail_events \
"$filter_name" "$filter_type" "$filter_peers" \
"$blocked_only" "$restricted_only" "$allowed_only" \
"$w_client" "$w_dest" &
local tailer_pid=$!
trap "kill $tailer_pid ${poller_pid:-} 2>/dev/null; \
rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_*; printf '\n'; exit 0" INT TERM
wait
}
# ============================================
# 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"
}