diff --git a/commands/watch.command.sh b/commands/watch.command.sh index ba2f45b..8826a08 100644 --- a/commands/watch.command.sh +++ b/commands/watch.command.sh @@ -107,13 +107,16 @@ function cmd::watch::run() { 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 @@ -128,34 +131,48 @@ function cmd::watch::_poll_handshakes() { 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") - - # Resolve endpoint + + 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:-—}" - - ui::watch::wg_row "$ts_fmt" "$client_name" "$endpoint_display" "handshake" \ - "$w_client" "$w_dest" - + [[ -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 # ============================================ @@ -164,7 +181,7 @@ 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 @@ -174,41 +191,41 @@ function cmd::watch::_tail_events() { 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 @@ -217,52 +234,53 @@ function cmd::watch::_tail_events() { 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}" - - # Handshakes — only show if gap > 5min (new session) - # Attempts — shorter window (30s) since each attempt is meaningful + 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 - local endpoint_display - endpoint_display=$(resolve::ip "${endpoint:-}") - [[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:-—}" - - ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_display" "$event" \ + + # 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" } \ No newline at end of file diff --git a/core/lib/__pycache__/__init__.cpython-311.pyc b/core/lib/__pycache__/__init__.cpython-311.pyc index 16dca2f..ebc6111 100644 Binary files a/core/lib/__pycache__/__init__.cpython-311.pyc and b/core/lib/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/lib/__pycache__/events.cpython-311.pyc b/core/lib/__pycache__/events.cpython-311.pyc index 07f6b30..be3ab56 100644 Binary files a/core/lib/__pycache__/events.cpython-311.pyc and b/core/lib/__pycache__/events.cpython-311.pyc differ diff --git a/core/lib/__pycache__/peers.cpython-311.pyc b/core/lib/__pycache__/peers.cpython-311.pyc index 57cab2b..3884435 100644 Binary files a/core/lib/__pycache__/peers.cpython-311.pyc and b/core/lib/__pycache__/peers.cpython-311.pyc differ diff --git a/core/lib/__pycache__/util.cpython-311.pyc b/core/lib/__pycache__/util.cpython-311.pyc index 7468939..4e3a658 100644 Binary files a/core/lib/__pycache__/util.cpython-311.pyc and b/core/lib/__pycache__/util.cpython-311.pyc differ diff --git a/core/ui.sh b/core/ui.sh index 99bac80..9cdf630 100644 --- a/core/ui.sh +++ b/core/ui.sh @@ -3,6 +3,12 @@ UI_ROW_WIDTH=${UI_ROW_WIDTH:-20} UI_SECTION_WIDTH=${UI_SECTION_WIDTH:-44} +# UTF-8 multi-byte character extras (bash ${#} counts bytes, not chars) +# extra = byte_length - visible_char_length +_UI_EMDASH_EXTRA=2 # — (em dash) 3 bytes, 1 visible +_UI_ARROW_EXTRA=2 # → (right arrow) 3 bytes, 1 visible +_UI_BULLET_EXTRA=1 # · (middle dot) 2 bytes, 1 visible + function ui::row() { local label="$1" value="$2" width="${3:-$UI_ROW_WIDTH}" printf " %-${width}s %s\n" "${label}:" "$value" diff --git a/modules/monitor.module.sh b/modules/monitor.module.sh index 517b8f5..9bcee2f 100644 --- a/modules/monitor.module.sh +++ b/modules/monitor.module.sh @@ -89,7 +89,7 @@ function monitor::cache_endpoint() { } function monitor::get_cached_endpoint() { - local client="$1" + local client="${1:-}" json::get "$ENDPOINT_CACHE" "$client" } diff --git a/modules/ui/logs.module.sh b/modules/ui/logs.module.sh index 279a8f4..c028422 100644 --- a/modules/ui/logs.module.sh +++ b/modules/ui/logs.module.sh @@ -94,17 +94,16 @@ _UI_WATCH_WG_COLOR="\033[1;32m" function ui::watch::fw_row() { local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \ w_client="${4:-20}" w_dest="${5:-18}" - - local ts_pad + + # "fw" is always 2 visible chars — no padding needed + local src="${_UI_WATCH_FW_COLOR}fw\033[0m" + + local ts_pad client_pad dest_pad_n ts_pad=$(printf "%-11s" "$ts") - - local src - src=$(ui::pad_mb "${_UI_WATCH_FW_COLOR}fw\033[0m" 2) - local client_pad dest_pad_n client_pad=$(printf "%-${w_client}s" "$client") dest_pad_n=$(( w_dest - ${#dest_display} )) [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 - # echo "DEBUG fw: ts_bytes=${#ts} src_bytes=${#src} client='$client'(${#client}) client_pad_bytes=${#client_pad}" >&2 + printf " %s %b %s \033[1;31m→\033[0m %s%*s \033[1;31mdrop\033[0m\n" \ "$ts_pad" "$src" "$client_pad" "$dest_display" "$dest_pad_n" "" } @@ -112,35 +111,31 @@ function ui::watch::fw_row() { function ui::watch::wg_row() { local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ w_client="${5:-20}" w_endpoint="${6:-18}" - - local ts_pad - ts_pad=$(printf "%-11s" "$ts") - + local event_color case "$event" in handshake) event_color="\033[1;32m" ;; attempt) event_color="\033[1;31m" ;; *) event_color="\033[0;37m" ;; esac - local src - src=$(ui::pad_mb "${_UI_WATCH_WG_COLOR}wg\033[0m" 2) - - case "$event" in - handshake) src="\033[1;32m" ;; # green - attempt) src="\033[1;31m" ;; # red - *) src="\033[0;37m" ;; # gray - esac - local src_colored="${src}wg\033[0m" - - local client_pad endpoint_pad_n + + # "wg" is always 2 visible chars — no padding needed + local src="${_UI_WATCH_WG_COLOR}wg\033[0m" + + # Use "-" not "—" to avoid multi-byte padding issues + local endpoint_display="${endpoint:--}" + + local ts_pad client_pad endpoint_pad_n + ts_pad=$(printf "%-11s" "$ts") client_pad=$(printf "%-${w_client}s" "$client") - endpoint_pad_n=$(( w_endpoint - ${#endpoint} )) + endpoint_pad_n=$(( w_endpoint - ${#endpoint_display} )) [[ $endpoint_pad_n -lt 0 ]] && endpoint_pad_n=0 - # echo "DEBUG wg: ts_bytes=${#ts} src_bytes=${#src} client='$client'(${#client}) client_pad_bytes=${#client_pad}" >&2 + printf " %s %b %s %s%*s %b%s\033[0m\n" \ - "$ts_pad" "$src_colored" "$client_pad" "$endpoint" "$endpoint_pad_n" "" \ + "$ts_pad" "$src" "$client_pad" "$endpoint_display" "$endpoint_pad_n" "" \ "$event_color" "$event" } + function ui::watch::header_table() { printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \ diff --git a/modules/ui/rule.module.sh b/modules/ui/rule.module.sh index ba1f5e9..8a9dc57 100644 --- a/modules/ui/rule.module.sh +++ b/modules/ui/rule.module.sh @@ -331,20 +331,23 @@ function ui::rule::list_row() { extends_indicator=" \033[2m↳ ${extends_display}\033[0m" fi - # Allows column — green +N if >0, dim +0 if zero + # Allows — green +N, padded to 5 visible chars + # Append spaces after reset code so printf doesn't miscount local allows_str if [[ "$n_allows" -gt 0 ]]; then - allows_str=$(ui::pad_mb "\033[1;32m+${n_allows}\033[0m" 5) + printf -v allows_str "\033[1;32m+%s\033[0m" "$n_allows" + allows_str="${allows_str}$(printf '%*s' "$(( 4 - ${#n_allows} ))" '')" else - allows_str=$(ui::pad_mb "\033[2m+0\033[0m" 5) + printf -v allows_str "\033[2m+0\033[0m " # "+0" = 2 visible + 3 spaces = 5 fi - # Blocks column — red -N if >0, dim -0 if zero + # Blocks — red -N, padded to 5 visible chars local blocks_str if [[ "$n_blocks" -gt 0 ]]; then - blocks_str=$(ui::pad_mb "\033[1;31m-${n_blocks}\033[0m" 5) + printf -v blocks_str "\033[1;31m-%s\033[0m" "$n_blocks" + blocks_str="${blocks_str}$(printf '%*s' "$(( 4 - ${#n_blocks} ))" '')" else - blocks_str=$(ui::pad_mb "\033[2m-0\033[0m" 5) + printf -v blocks_str "\033[2m-0\033[0m " # "-0" = 2 visible + 3 spaces = 5 fi # Peers — dim if zero