From c3cf5bc5721247b8da23882ba6e8fb39a45f32ac Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Tue, 26 May 2026 15:16:33 +0000 Subject: [PATCH] feat: watch/logs endpoint annotation, shared row primitives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ui::_render_endpoint_col: shared endpoint padding primitive - ui::_build_dest: shared destination display primitive - ui::watch::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 --- commands/identity.command.sh | 35 ++- commands/rule.command.sh | 26 +-- commands/shell.command.sh | 69 ++++-- commands/watch.command.sh | 49 ++-- modules/monitor.module.sh | 6 + modules/resolve.module.sh | 43 ++++ modules/ui/logs.module.sh | 436 ++++++++++++++++++++++++++--------- 7 files changed, 477 insertions(+), 187 deletions(-) diff --git a/commands/identity.command.sh b/commands/identity.command.sh index 75b8685..f498eb0 100644 --- a/commands/identity.command.sh +++ b/commands/identity.command.sh @@ -41,33 +41,32 @@ function cmd::identity::help() { cat < [options] -Manage peer identities. +Manage peer identities — group peers by person/device owner. Subcommands: - list List all identities - show --name Show identity details and device status - add --name Manually attach a peer to an identity - --peer - remove --name Remove identity and all associated peers - migrate [--dry-run] Create identities from existing peer names + list List all identities + show --name Show identity details with peers and rule tree + add --name Create a new identity + remove --name Remove an identity + migrate Migrate peers to identities - rule assign --name Assign a rule to an identity - --rule - rule unassign --name Remove rule from an identity - rule show --name Show current identity rule + rule assign --name --rule Assign rule to identity + Blocked if peer already has rule directly + [--migrate] Remove conflicting direct peer rules first + rule unassign --name --rule Remove rule from identity + rule unassign --name --all Remove all rules from identity - options --name Set identity options - [--policy ] - [--set-strict-rule | --unset-strict-rule] - [--set-auto-apply | --unset-auto-apply] + options --name --strict-rule Set strict rule mode + options --name --auto-apply Set auto apply Examples: wgctl identity list wgctl identity show --name nuno + wgctl identity add --name alice wgctl identity rule assign --name nuno --rule admin - wgctl identity rule unassign --name nuno - wgctl identity options --name guests-identity --policy guest - wgctl identity options --name nuno --set-strict-rule + wgctl identity rule assign --name nuno --rule user --migrate + wgctl identity rule unassign --name nuno --rule admin + wgctl identity options --name nuno --strict-rule true EOF } diff --git a/commands/rule.command.sh b/commands/rule.command.sh index 1f9cfc9..f592d83 100644 --- a/commands/rule.command.sh +++ b/commands/rule.command.sh @@ -46,15 +46,16 @@ Rules can extend base rules to compose reusable access policies. Service names from 'wgctl net' can be used instead of raw IPs/ports. Subcommands: - list, ls List all rules - show, inspect Show rule details and inheritance - add, new, create Create a new rule - update, edit Update a rule and re-apply to peers - remove, rm, del Remove a rule - assign Assign a rule to a peer - unassign Remove rule from a peer - reapply Re-apply rule to all assigned peers - migrate Apply default rules to unassigned peers + list, ls List all rules + list --detailed Show inheritance tree + show, inspect --name Show rule details and inheritance + add, new, create --name Create a new rule + update, edit --name Update a rule and re-apply to peers + remove, rm, del --name Remove a rule + assign --name Assign a rule to a peer + unassign --name --peer

Remove rule from a peer + reapply Re-apply rule to all assigned peers + migrate Apply default rules to unassigned peers Options for list: --base Show only base rules @@ -312,10 +313,9 @@ function cmd::rule::show() { local peer_count=${#peer_list[@]} ui::empty "$peer_count" && return 0 - local peer_word="peers" - [[ "$peer_count" -eq 1 ]] && peer_word="peer" - printf "\n \033[0;37m── Peers (%s %s) \033[0m%s\n\n" \ - "$peer_count" "$peer_word" "$(printf '\033[0;37m─%.0s' {1..30})" + [[ "$peer_count" -eq 1 ]] + printf "\n \033[0;37m── Peers (%s) \033[0m%s\n\n" \ + "$peer_count" "$(printf '\033[0;37m─%.0s' {1..30})" for peer_name in "${peer_list[@]}"; do local ip diff --git a/commands/shell.command.sh b/commands/shell.command.sh index 8737ba7..158a97d 100644 --- a/commands/shell.command.sh +++ b/commands/shell.command.sh @@ -19,6 +19,7 @@ function cmd::shell::_is_wgctl_command() { list add remove rm inspect block unblock rule group audit logs watch fw config qr rename keys ip net service shell help test + peer hosts identity subnet policy activity ) local c for c in "${known[@]}"; do @@ -82,28 +83,44 @@ function cmd::shell::_banner() { printf "\n" printf " Type wgctl commands directly (no 'wgctl' prefix).\n" printf " Bash commands work too: ls, cat, systemctl, vim...\n\n" - printf " \033[1;37mCommon commands:\033[0m\n" - printf " list List all peers\n" - printf " list --blocked Show blocked peers\n" - printf " list --restricted Show restricted peers\n" - printf " list --rule user Filter by rule\n" - printf " inspect --name Full peer details\n" - printf " block --name Block a peer entirely\n" - printf " block --name --service proxmox Restrict service\n" - printf " unblock --name Restore full access\n" - printf " rule list Show firewall rules\n" - printf " rule list --tree Show with inheritance\n" - printf " rule show --name Rule details\n" - printf " net list Show network services\n" - printf " net list --detailed Show services with ports\n" - printf " group list Show groups\n" - printf " group block --name Block all peers in group\n" - printf " logs --follow Live activity log\n" - printf " logs rotate Clean old log entries\n" - printf " watch Live WG + firewall monitor\n" - printf " fw list Show iptables rules\n" - printf " audit Verify firewall state\n" - printf " audit --fix Auto-repair firewall rules\n\n" + printf " \033[1;37mPeer management:\033[0m\n" + printf " list List all peers\n" + printf " list --blocked Show blocked peers\n" + printf " list --rule user Filter by rule\n" + printf " inspect --name Full peer details\n" + printf " add --identity --type phone Add a peer\n" + printf " block --name Block a peer\n" + printf " unblock --name Restore access\n" + printf " peer update-dns --all Update DNS on all peers\n" + printf " peer update-tunnel --name

--mode split\n\n" + printf " \033[1;37mRules & access:\033[0m\n" + printf " rule list Show firewall rules\n" + printf " rule list --tree Show with inheritance\n" + printf " rule assign --name --peer

Assign rule to peer\n" + printf " identity list Show identities\n" + printf " identity show --name Identity details + rule tree\n" + printf " identity rule assign --name --rule \n\n" + printf " \033[1;37mNetwork & services:\033[0m\n" + printf " net list Show network services\n" + printf " net list --detailed Show with ports\n" + printf " hosts list Show host annotations\n" + printf " subnet list Show subnets\n" + printf " group list Show groups\n" + printf " group block --name Block all in group\n\n" + printf " \033[1;37mMonitoring:\033[0m\n" + printf " logs Activity logs\n" + printf " logs --since 2h Logs from last 2h\n" + printf " logs --fw --service pihole FW drops to service\n" + printf " logs --wg --event handshake WG handshake events\n" + printf " logs --resolved Show resolved names only\n" + printf " logs --follow Live activity log\n" + printf " logs clean Remove keepalive entries\n" + printf " logs rotate Clean old log entries\n" + printf " watch Live WG + firewall monitor\n" + printf " activity Transfer + drop summary\n" + printf " fw list Show iptables rules\n" + printf " audit Verify firewall state\n" + printf " audit --fix Auto-repair firewall\n\n" printf " \033[1mexit\033[0m or \033[1mquit\033[0m to leave · \033[1mhelp\033[0m for full command list\n\n" } @@ -143,13 +160,15 @@ EOF # ============================================ function cmd::shell::_setup_completion() { - local commands="list add remove rm inspect block unblock rule group audit logs watch fw config qr rename service shell help test" - + local commands="list add remove rm inspect block unblock rule group audit \ +logs watch fw config qr rename service shell help test \ +peer hosts identity subnet policy activity" + function _wgctl_shell_complete() { local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=( $(compgen -W "$commands" -- "$cur") ) } - + bind 'set show-all-if-ambiguous on' 2>/dev/null || true bind 'set completion-ignore-case on' 2>/dev/null || true } diff --git a/commands/watch.command.sh b/commands/watch.command.sh index cc7c59c..d0c1fd3 100644 --- a/commands/watch.command.sh +++ b/commands/watch.command.sh @@ -133,14 +133,15 @@ function cmd::watch::_poll_handshakes() { 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") + 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) @@ -217,10 +218,18 @@ function cmd::watch::_tail_events() { 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" + 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 @@ -249,15 +258,15 @@ function cmd::watch::_tail_events() { 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" + 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 - [[ -z "$endpoint_resolved" ]] && endpoint_resolved="-" - - ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_resolved" "$event" \ - "$w_client" "$w_dest" + ui::watch::wg_row "$ts_fmt" "$client" "$wg_ep_raw" "$event" \ + "$wg_ep_resolved" "$w_client" "30" fi done diff --git a/modules/monitor.module.sh b/modules/monitor.module.sh index 2335478..7c84639 100644 --- a/modules/monitor.module.sh +++ b/modules/monitor.module.sh @@ -149,6 +149,12 @@ function monitor::live() { rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true local w_client=20 w_dest=18 + while IFS= read -r conf; do + local name + name=$(basename "$conf" .conf) + (( ${#name} > w_client )) && w_client=${#name} + done < <(find "$(ctx::clients)" -name "*.conf" 2>/dev/null) + (( w_client += 2 )) if ! $blocked_only && ! $restricted_only; then ( diff --git a/modules/resolve.module.sh b/modules/resolve.module.sh index 01b1c50..f4c6e36 100644 --- a/modules/resolve.module.sh +++ b/modules/resolve.module.sh @@ -60,6 +60,49 @@ function resolve::dest() { fi } +# resolve::service_name [port] [proto] +# Returns just the service/host name, empty string if no match (not raw IP). +# Use when you need to know IF something resolved, not what the raw fallback is. +function resolve::service_name() { + local ip="${1:-}" port="${2:-}" proto="${3:-}" + [[ -z "$ip" ]] && echo "" && return 0 + [[ "${_WGCTL_RAW:-false}" == "true" ]] && echo "" && return 0 + + local result="" + + # 1. hosts.json exact IP match + if [[ -f "$(ctx::hosts)" ]]; then + result=$(hosts::resolve_ip "$ip") + fi + + # 2. services.json match + if [[ -z "$result" ]]; then + result=$(net::reverse_lookup "$ip" "$port" "$proto" 2>/dev/null) || result="" + fi + + # Return empty if no match (caller handles raw fallback) + [[ "$result" == "$ip" ]] && result="" + echo "$result" +} + +# resolve::endpoint_parts +# Returns "raw_ip|resolved_name" — empty resolved if no match. +# Used by watch/logs rows to build "raw_ip → resolved" display. +function resolve::endpoint_parts() { + local ip="${1:-}" + [[ -z "$ip" ]] && echo "|" && return 0 + [[ "${_WGCTL_RAW:-false}" == "true" ]] && echo "${ip}|" && return 0 + + # Don't use cache for endpoint_parts — always resolve fresh + local resolved="" + [[ -f "$(ctx::hosts)" ]] && resolved=$(hosts::resolve_ip "$ip" 2>/dev/null || true) + if [[ -z "$resolved" ]]; then + resolved=$(net::reverse_lookup "$ip" "" "" 2>/dev/null) || resolved="" + fi + [[ "$resolved" == "$ip" ]] && resolved="" + echo "${ip}|${resolved}" +} + # resolve::clear_cache # Clears the resolution cache — call between commands if needed. function resolve::clear_cache() { diff --git a/modules/ui/logs.module.sh b/modules/ui/logs.module.sh index 8c3d72a..4ea2a4a 100644 --- a/modules/ui/logs.module.sh +++ b/modules/ui/logs.module.sh @@ -126,61 +126,61 @@ function ui::logs::fw_row_table() { printf " %-20s %-18s %-25s %s%s\n" "$ts" "$client" "$dst" "$proto" "$count_str" } -function ui::logs::wg_row() { - local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ - count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ - gap_seconds="${8:-}" resolved="${9:-}" +# function ui::logs::wg_row() { +# local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ +# count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ +# gap_seconds="${8:-}" resolved="${9:-}" - 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 event_color +# case "$event" in +# handshake) event_color="\033[1;32m" ;; +# attempt) event_color="\033[1;31m" ;; +# *) event_color="\033[0;37m" ;; +# esac - local count_suffix="" - [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" +# local count_suffix="" +# [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" - # Gap suffix — offline label only when gap > threshold * 2 - local gap_suffix="" - if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then - local gap_int="$gap_seconds" - local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" - local offline_label="" - [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" - if (( gap_int >= 3600 )); then - gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" - elif (( gap_int >= 60 )); then - gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" - fi - fi +# # Gap suffix — offline label only when gap > threshold * 2 +# local gap_suffix="" +# if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then +# local gap_int="$gap_seconds" +# local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" +# local offline_label="" +# [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" +# if (( gap_int >= 3600 )); then +# gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" +# elif (( gap_int >= 60 )); then +# gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" +# fi +# fi - # ── Endpoint — native padding, no ui::pad_mb ── - local endpoint_colored endpoint_visible_len - local endpoint_raw="${endpoint:--}" +# # ── Endpoint — native padding, no ui::pad_mb ── +# local endpoint_colored endpoint_visible_len +# local endpoint_raw="${endpoint:--}" - if [[ -n "$resolved" && -n "$endpoint" ]]; then - endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" - endpoint_visible_len=$(( ${#endpoint} + 4 + ${#resolved} - _UI_ARROW_EXTRA )) - else - endpoint_colored="$endpoint_raw" - # "-" is 1 char; endpoint may be empty - [[ -n "$endpoint" ]] && endpoint_visible_len=${#endpoint} \ - || endpoint_visible_len=1 - fi +# if [[ -n "$resolved" && -n "$endpoint" ]]; then +# endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" +# endpoint_visible_len=$(( ${#endpoint} + 4 + ${#resolved} - _UI_ARROW_EXTRA )) +# else +# endpoint_colored="$endpoint_raw" +# # "-" is 1 char; endpoint may be empty +# [[ -n "$endpoint" ]] && endpoint_visible_len=${#endpoint} \ +# || endpoint_visible_len=1 +# fi - local ep_pad_n=$(( w_endpoint - endpoint_visible_len )) - [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0 - local endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "") - local ts_pad client_pad - ts_pad=$(printf "%-11s" "$ts") - client_pad=$(printf "%-${w_client}s" "$client") +# local ep_pad_n=$(( w_endpoint - endpoint_visible_len )) +# [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0 +# local endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "") +# local ts_pad client_pad +# ts_pad=$(printf "%-11s" "$ts") +# client_pad=$(printf "%-${w_client}s" "$client") - printf " %s %s %b %b%s\033[0m%b%b\n" \ - "$ts_pad" "$client_pad" \ - "$endpoint_padded" \ - "$event_color" "$event" "$count_suffix" "$gap_suffix" -} +# printf " %s %s %b %b%s\033[0m%b%b\n" \ +# "$ts_pad" "$client_pad" \ +# "$endpoint_padded" \ +# "$event_color" "$event" "$count_suffix" "$gap_suffix" +# } function ui::logs::wg_row_table() { local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" count="${5:-1}" @@ -198,79 +198,106 @@ function ui::logs::wg_row_table() { _UI_WATCH_FW_COLOR="\033[1;31m" _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}" +# function ui::watch::fw_row() { +# local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \ +# w_client="${4:-20}" w_dest="${5:-18}" - # "fw" is always 2 visible chars — no padding needed - local src="${_UI_WATCH_FW_COLOR}fw\033[0m" +# # "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") - client_pad=$(printf "%-${w_client}s" "$client") - dest_pad_n=$(( w_dest - ${#dest_display} )) - [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 +# local ts_pad client_pad dest_pad_n +# ts_pad=$(printf "%-11s" "$ts") +# client_pad=$(printf "%-${w_client}s" "$client") +# dest_pad_n=$(( w_dest - ${#dest_display} )) +# [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 - 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" "" -} +# 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" "" +# } -function ui::logs::wg_row() { - local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ - count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ - gap_seconds="${8:-}" resolved="${9:-}" + +# function ui::watch::wg_row() { +# local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ +# w_client="${5:-20}" w_endpoint="${6:-18}" + +# 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 endpoint_display="${endpoint:--}" + +# local ts_pad client_pad ep_pad_n +# ts_pad=$(printf "%-11s" "$ts") +# client_pad=$(printf "%-${w_client}s" "$client") +# ep_pad_n=$(( w_endpoint - ${#endpoint_display} )) +# [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0 + +# printf " %s %s %s%*s %b%s\033[0m\n" \ +# "$ts_pad" "$client_pad" \ +# "$endpoint_display" "$ep_pad_n" "" \ +# "$event_color" "$event" +# } + + +# function ui::logs::wg_row() { +# local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ +# count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ +# gap_seconds="${8:-}" resolved="${9:-}" - 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 event_color +# case "$event" in +# handshake) event_color="\033[1;32m" ;; +# attempt) event_color="\033[1;31m" ;; +# *) event_color="\033[0;37m" ;; +# esac - local count_suffix="" - [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" +# local count_suffix="" +# [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" - local gap_suffix="" - if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then - local gap_int="$gap_seconds" - local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" - local offline_label="" - [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" - if (( gap_int >= 3600 )); then - gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" - elif (( gap_int >= 60 )); then - gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" - fi - fi +# local gap_suffix="" +# if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then +# local gap_int="$gap_seconds" +# local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" +# local offline_label="" +# [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" +# if (( gap_int >= 3600 )); then +# gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" +# elif (( gap_int >= 60 )); then +# gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" +# fi +# fi - # ── Endpoint padding ── - # " → " = 5 bytes, 3 visible → visible = endpoint + 3 + resolved - local endpoint_colored endpoint_visible_len - if [[ -n "$resolved" && -n "$endpoint" ]]; then - endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" - endpoint_visible_len=$(( ${#endpoint} + 3 + ${#resolved} )) - elif [[ -n "$endpoint" ]]; then - endpoint_colored="${endpoint}" - endpoint_visible_len=${#endpoint} - else - endpoint_colored="-" - endpoint_visible_len=1 - fi +# # ── Endpoint padding ── +# # " → " = 5 bytes, 3 visible → visible = endpoint + 3 + resolved +# local endpoint_colored endpoint_visible_len +# if [[ -n "$resolved" && -n "$endpoint" ]]; then +# endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" +# endpoint_visible_len=$(( ${#endpoint} + 3 + ${#resolved} )) +# elif [[ -n "$endpoint" ]]; then +# endpoint_colored="${endpoint}" +# endpoint_visible_len=${#endpoint} +# else +# endpoint_colored="-" +# endpoint_visible_len=1 +# fi - local ep_pad_n=$(( w_endpoint - endpoint_visible_len )) - [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0 - local endpoint_padded - endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "") +# local ep_pad_n=$(( w_endpoint - endpoint_visible_len )) +# [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0 +# local endpoint_padded +# endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "") - local ts_pad client_pad - ts_pad=$(printf "%-11s" "$ts") - client_pad=$(printf "%-${w_client}s" "$client") +# local ts_pad client_pad +# ts_pad=$(printf "%-11s" "$ts") +# client_pad=$(printf "%-${w_client}s" "$client") - printf " %s %s %b %b%s\033[0m%b%b\n" \ - "$ts_pad" "$client_pad" \ - "$endpoint_padded" \ - "$event_color" "$event" "$count_suffix" "$gap_suffix" -} +# printf " %s %s %b %b%s\033[0m%b%b\n" \ +# "$ts_pad" "$client_pad" \ +# "$endpoint_padded" \ +# "$event_color" "$event" "$count_suffix" "$gap_suffix" +# } function ui::watch::header_table() { printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \ @@ -294,4 +321,191 @@ function ui::watch::wg_row_table() { esac printf " %-20s %-8s %-22s %-28s %b%-14s\033[0m %s\n" \ "$ts" "wireguard" "$client" "$endpoint" "$event_color" "$event" "$status" -} \ No newline at end of file +} + + +# ui::_render_endpoint_col +# Builds padded endpoint string: "raw_ip → resolved" or "raw_ip" or "-" +# Args: endpoint resolved w_endpoint +# Returns: padded colored string via stdout +function ui::_render_endpoint_col() { + local endpoint="${1:-}" resolved="${2:-}" w_endpoint="${3:-20}" + local colored visible_len pad_n + + if [[ -n "$endpoint" ]]; then + if [[ -n "$resolved" ]]; then + colored="${endpoint} \033[2m→ ${resolved}\033[0m" + visible_len=$(( ${#endpoint} + 3 + ${#resolved} )) + else + colored="$endpoint" + visible_len=${#endpoint} + fi + else + colored="\033[2m—\033[0m" + visible_len=1 + fi + + pad_n=$(( w_endpoint - visible_len )) + [[ $pad_n -lt 0 ]] && pad_n=0 + printf "%b%*s" "$colored" "$pad_n" "" +} + +# ui::_render_dest_col +# Builds padded destination string: "svc/proto (ip:port)" or raw +# Args: dest_ip dest_port proto svc_name w_dest resolved_only +# Returns: two vars via nameref — dest_colored dest_pad_n +function ui::_build_dest() { + local dest_ip="${1:-}" dest_port="${2:-}" proto="${3:-}" \ + svc_name="${4:-}" w_dest="${5:-20}" resolved_only="${6:-false}" + local svc_display raw_suffix="" raw_plain="" + + if [[ -n "$svc_name" ]]; then + [[ -n "$dest_port" ]] && svc_display="${svc_name}/${proto}" \ + || svc_display="${svc_name} (${proto})" + if ! $resolved_only; then + [[ -n "$dest_port" ]] && raw_suffix=" \033[2m(${dest_ip}:${dest_port})\033[0m" \ + || raw_suffix=" \033[2m(${dest_ip})\033[0m" + [[ -n "$dest_port" ]] && raw_plain=" (${dest_ip}:${dest_port})" \ + || raw_plain=" (${dest_ip})" + fi + else + [[ -n "$dest_port" ]] && svc_display="${dest_ip}:${dest_port}/${proto}" \ + || svc_display="${dest_ip} (${proto})" + fi + + local full_len=$(( ${#svc_display} + ${#raw_plain} )) + local dest_pad_n=$(( w_dest - full_len )) + [[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 + + # Output: svc_display|raw_suffix|dest_pad_n + printf "%s\t%s\t%s" "$svc_display" "$raw_suffix" "$dest_pad_n" +} + +function ui::logs::fw_row() { + local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \ + proto="${5:-}" svc_name="${6:-}" count="${7:-1}" \ + w_client="${8:-20}" w_dest="${9:-30}" \ + src_endpoint="${10:-}" src_resolved="${11:-}" \ + w_endpoint="${12:-0}" resolved_only="${13:-false}" + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") + client_pad=$(printf "%-${w_client}s" "$client") + + local count_suffix="" + [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" + + # Dest + local dest_parts + dest_parts=$(ui::_build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name" "$w_dest" "$resolved_only") + local svc_display raw_suffix dest_pad_n + IFS=$'\t' read -r svc_display raw_suffix dest_pad_n <<< "$dest_parts" + + if [[ "$w_endpoint" -gt 0 ]]; then + local src_padded + src_padded=$(ui::_render_endpoint_col "$src_endpoint" "$src_resolved" "$w_endpoint") + printf " %s %s %b \033[1;31m→\033[0m %s%b%*s%b\n" \ + "$ts_pad" "$client_pad" "$src_padded" \ + "$svc_display" "$raw_suffix" "$dest_pad_n" "" "$count_suffix" + else + printf " %s %s \033[1;31m→\033[0m %s%b%*s%b\n" \ + "$ts_pad" "$client_pad" \ + "$svc_display" "$raw_suffix" "$dest_pad_n" "" "$count_suffix" + fi +} + +function ui::logs::wg_row() { + local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ + count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ + gap_seconds="${8:-}" resolved="${9:-}" + + 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 count_suffix="" + [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" + + local gap_suffix="" + if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then + local gap_int="$gap_seconds" + local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" + local offline_label="" + [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" + if (( gap_int >= 3600 )); then + gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" + elif (( gap_int >= 60 )); then + gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" + fi + fi + + local ep_padded + ep_padded=$(ui::_render_endpoint_col "$endpoint" "$resolved" "$w_endpoint") + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") + client_pad=$(printf "%-${w_client}s" "$client") + + printf " %s %s %b %b%s\033[0m%b%b\n" \ + "$ts_pad" "$client_pad" "$ep_padded" \ + "$event_color" "$event" "$count_suffix" "$gap_suffix" +} + +function ui::watch::fw_row() { + local ts="${1:-}" client="${2:-}" dest_ip="${3:-}" dest_port="${4:-}" \ + proto="${5:-}" svc_name="${6:-}" \ + src_endpoint="${7:-}" src_resolved="${8:-}" \ + w_client="${9:-20}" w_dest="${10:-18}" w_endpoint="${11:-0}" + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") + client_pad=$(printf "%-${w_client}s" "$client") + + local dest_parts + dest_parts=$(ui::_build_dest "$dest_ip" "$dest_port" "$proto" "$svc_name" "$w_dest" "false") + local svc_display raw_suffix dest_pad_n + IFS=$'\t' read -r svc_display raw_suffix dest_pad_n <<< "$dest_parts" + + if [[ "$w_endpoint" -gt 0 ]]; then + local src_padded + src_padded=$(ui::_render_endpoint_col "$src_endpoint" "$src_resolved" "$w_endpoint") + printf " %s %s %s \033[1;31m→\033[0m %s%b%*s \033[1;31mdrop\033[0m\n" \ + "$ts_pad" "$client_pad" "$src_padded" \ + "$svc_display" "$raw_suffix" "$dest_pad_n" "" + else + printf " %s %s \033[1;31m→\033[0m %s%b%*s \033[1;31mdrop\033[0m\n" \ + "$ts_pad" "$client_pad" \ + "$svc_display" "$raw_suffix" "$dest_pad_n" "" + fi +} + + +function ui::watch::wg_row() { + local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ + src_resolved="${5:-}" \ + w_client="${6:-20}" w_endpoint="${7:-18}" + + 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 ep_padded + ep_padded=$(ui::_render_endpoint_col "$endpoint" "$src_resolved" "$w_endpoint") + + local ts_pad client_pad + ts_pad=$(printf "%-11s" "$ts") + client_pad=$(printf "%-${w_client}s" "$client") + + # echo "DEBUG: ts='$ts_pad' client='$client_pad'(${#client_pad}) ep='$ep_padded'(${#ep_padded}) event='$event'" >&2 + + printf " %s %s %s %b%s\033[0m\n" \ + "$ts_pad" "$client_pad" "$ep_padded" \ + "$event_color" "$event" +} + \ No newline at end of file