- ui::_render_endpoint_col: shared endpoint padding primitive
- ui::_build_dest: shared destination display primitive
- ui:⌚:wg_row/fw_row: endpoint annotation (raw_ip → resolved)
- resolve::endpoint_parts: fresh resolution, no stale cache
- resolve::service_name: returns service name or empty (no raw fallback)
- monitor::live: pre-measure w_client from peer names
- watch: fixed w_endpoint=30 for consistent live alignment
- shell: add peer/hosts/identity/subnet/policy/activity to known commands
- shell: updated banner with new commands
- identity/rule help: updated with new features
274 lines
No EOL
8.6 KiB
Bash
274 lines
No EOL
8.6 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"
|
|
|
|
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 ep_raw ep_resolved=""
|
|
ep_raw="${endpoint:-}"
|
|
if [[ -n "$ep_raw" ]]; then
|
|
local ep_parts
|
|
ep_parts=$(resolve::endpoint_parts "$ep_raw")
|
|
ep_resolved="${ep_parts#*|}"
|
|
fi
|
|
row=$(ui::watch::wg_row "$ts_fmt" "$client_name" "$ep_raw" "handshake" \
|
|
"$ep_resolved" "$w_client" "30")
|
|
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 fw_svc_name
|
|
fw_svc_name=$(resolve::service_name "$dest_ip" "$dest_port" "$proto")
|
|
local fw_src_ep fw_src_resolved=""
|
|
fw_src_ep=$(monitor::get_cached_endpoint "$client")
|
|
if [[ -n "$fw_src_ep" ]]; then
|
|
local fw_ep_parts
|
|
fw_ep_parts=$(resolve::endpoint_parts "$fw_src_ep")
|
|
fw_src_resolved="${fw_ep_parts#*|}"
|
|
fi
|
|
ui::watch::fw_row "$ts_fmt" "$client" "$dest_ip" "$dest_port" "$proto" \
|
|
"$fw_svc_name" "$fw_src_ep" "$fw_src_resolved" \
|
|
"$w_client" "$w_dest" "30"
|
|
|
|
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 wg_ep_raw wg_ep_resolved=""
|
|
wg_ep_raw="${endpoint:-}"
|
|
if [[ -n "$wg_ep_raw" ]]; then
|
|
local wg_ep_parts
|
|
wg_ep_parts=$(resolve::endpoint_parts "$wg_ep_raw")
|
|
wg_ep_resolved="${wg_ep_parts#*|}"
|
|
fi
|
|
ui::watch::wg_row "$ts_fmt" "$client" "$wg_ep_raw" "$event" \
|
|
"$wg_ep_resolved" "$w_client" "30"
|
|
fi
|
|
done
|
|
|
|
rm -f "$source_file"
|
|
} |