feat: watch/logs endpoint annotation, shared row primitives

- 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
This commit is contained in:
Nuno Duque Nunes 2026-05-26 15:16:33 +00:00
parent c6883c6801
commit c3cf5bc572
7 changed files with 477 additions and 187 deletions

View file

@ -41,33 +41,32 @@ function cmd::identity::help() {
cat <<EOF cat <<EOF
Usage: wgctl identity <subcommand> [options] Usage: wgctl identity <subcommand> [options]
Manage peer identities. Manage peer identities — group peers by person/device owner.
Subcommands: Subcommands:
list List all identities list List all identities
show --name <name> Show identity details and device status show --name <n> Show identity details with peers and rule tree
add --name <name> Manually attach a peer to an identity add --name <n> Create a new identity
--peer <peer> remove --name <n> Remove an identity
remove --name <name> Remove identity and all associated peers migrate Migrate peers to identities
migrate [--dry-run] Create identities from existing peer names
rule assign --name <name> Assign a rule to an identity rule assign --name <n> --rule <r> Assign rule to identity
--rule <rule> Blocked if peer already has rule directly
rule unassign --name <name> Remove rule from an identity [--migrate] Remove conflicting direct peer rules first
rule show --name <name> Show current identity rule rule unassign --name <n> --rule <r> Remove rule from identity
rule unassign --name <n> --all Remove all rules from identity
options --name <name> Set identity options options --name <n> --strict-rule <bool> Set strict rule mode
[--policy <policy>] options --name <n> --auto-apply <bool> Set auto apply
[--set-strict-rule | --unset-strict-rule]
[--set-auto-apply | --unset-auto-apply]
Examples: Examples:
wgctl identity list wgctl identity list
wgctl identity show --name nuno wgctl identity show --name nuno
wgctl identity add --name alice
wgctl identity rule assign --name nuno --rule admin wgctl identity rule assign --name nuno --rule admin
wgctl identity rule unassign --name nuno wgctl identity rule assign --name nuno --rule user --migrate
wgctl identity options --name guests-identity --policy guest wgctl identity rule unassign --name nuno --rule admin
wgctl identity options --name nuno --set-strict-rule wgctl identity options --name nuno --strict-rule true
EOF EOF
} }

View file

@ -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. Service names from 'wgctl net' can be used instead of raw IPs/ports.
Subcommands: Subcommands:
list, ls List all rules list, ls List all rules
show, inspect Show rule details and inheritance list --detailed Show inheritance tree
add, new, create Create a new rule show, inspect --name <r> Show rule details and inheritance
update, edit Update a rule and re-apply to peers add, new, create --name <r> Create a new rule
remove, rm, del Remove a rule update, edit --name <r> Update a rule and re-apply to peers
assign Assign a rule to a peer remove, rm, del --name <r> Remove a rule
unassign Remove rule from a peer assign --name <r> Assign a rule to a peer
reapply Re-apply rule to all assigned peers unassign --name <r> --peer <p> Remove rule from a peer
migrate Apply default rules to unassigned peers reapply Re-apply rule to all assigned peers
migrate Apply default rules to unassigned peers
Options for list: Options for list:
--base Show only base rules --base Show only base rules
@ -312,10 +313,9 @@ function cmd::rule::show() {
local peer_count=${#peer_list[@]} local peer_count=${#peer_list[@]}
ui::empty "$peer_count" && return 0 ui::empty "$peer_count" && return 0
local peer_word="peers" [[ "$peer_count" -eq 1 ]]
[[ "$peer_count" -eq 1 ]] && peer_word="peer" printf "\n \033[0;37m── Peers (%s) \033[0m%s\n\n" \
printf "\n \033[0;37m── Peers (%s %s) \033[0m%s\n\n" \ "$peer_count" "$(printf '\033[0;37m─%.0s' {1..30})"
"$peer_count" "$peer_word" "$(printf '\033[0;37m─%.0s' {1..30})"
for peer_name in "${peer_list[@]}"; do for peer_name in "${peer_list[@]}"; do
local ip local ip

View file

@ -19,6 +19,7 @@ function cmd::shell::_is_wgctl_command() {
list add remove rm inspect block unblock list add remove rm inspect block unblock
rule group audit logs watch fw config qr rule group audit logs watch fw config qr
rename keys ip net service shell help test rename keys ip net service shell help test
peer hosts identity subnet policy activity
) )
local c local c
for c in "${known[@]}"; do for c in "${known[@]}"; do
@ -82,28 +83,44 @@ function cmd::shell::_banner() {
printf "\n" printf "\n"
printf " Type wgctl commands directly (no 'wgctl' prefix).\n" printf " Type wgctl commands directly (no 'wgctl' prefix).\n"
printf " Bash commands work too: ls, cat, systemctl, vim...\n\n" printf " Bash commands work too: ls, cat, systemctl, vim...\n\n"
printf " \033[1;37mCommon commands:\033[0m\n" printf " \033[1;37mPeer management:\033[0m\n"
printf " list List all peers\n" printf " list List all peers\n"
printf " list --blocked Show blocked 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 " list --rule user Filter by rule\n" printf " inspect --name <peer> Full peer details\n"
printf " inspect --name <peer> Full peer details\n" printf " add --identity <id> --type phone Add a peer\n"
printf " block --name <peer> Block a peer entirely\n" printf " block --name <peer> Block a peer\n"
printf " block --name <peer> --service proxmox Restrict service\n" printf " unblock --name <peer> Restore access\n"
printf " unblock --name <peer> Restore full access\n" printf " peer update-dns --all Update DNS on all peers\n"
printf " rule list Show firewall rules\n" printf " peer update-tunnel --name <p> --mode split\n\n"
printf " rule list --tree Show with inheritance\n" printf " \033[1;37mRules & access:\033[0m\n"
printf " rule show --name <rule> Rule details\n" printf " rule list Show firewall rules\n"
printf " net list Show network services\n" printf " rule list --tree Show with inheritance\n"
printf " net list --detailed Show services with ports\n" printf " rule assign --name <r> --peer <p> Assign rule to peer\n"
printf " group list Show groups\n" printf " identity list Show identities\n"
printf " group block --name <group> Block all peers in group\n" printf " identity show --name <id> Identity details + rule tree\n"
printf " logs --follow Live activity log\n" printf " identity rule assign --name <id> --rule <r>\n\n"
printf " logs rotate Clean old log entries\n" printf " \033[1;37mNetwork & services:\033[0m\n"
printf " watch Live WG + firewall monitor\n" printf " net list Show network services\n"
printf " fw list Show iptables rules\n" printf " net list --detailed Show with ports\n"
printf " audit Verify firewall state\n" printf " hosts list Show host annotations\n"
printf " audit --fix Auto-repair firewall rules\n\n" printf " subnet list Show subnets\n"
printf " group list Show groups\n"
printf " group block --name <group> 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" printf " \033[1mexit\033[0m or \033[1mquit\033[0m to leave · \033[1mhelp\033[0m for full command list\n\n"
} }
@ -143,7 +160,9 @@ EOF
# ============================================ # ============================================
function cmd::shell::_setup_completion() { 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() { function _wgctl_shell_complete() {
local cur="${COMP_WORDS[COMP_CWORD]}" local cur="${COMP_WORDS[COMP_CWORD]}"

View file

@ -133,14 +133,15 @@ function cmd::watch::_poll_handshakes() {
endpoint=$(monitor::get_cached_endpoint "$client_name") endpoint=$(monitor::get_cached_endpoint "$client_name")
fi fi
local endpoint_display local ep_raw ep_resolved=""
endpoint_display=$(resolve::ip "${endpoint:-}") ep_raw="${endpoint:-}"
[[ -z "$endpoint_display" ]] && endpoint_display="${endpoint:--}" if [[ -n "$ep_raw" ]]; then
local ep_parts
# Build row with ts prefix for sorting ep_parts=$(resolve::endpoint_parts "$ep_raw")
local row ep_resolved="${ep_parts#*|}"
row=$(ui::watch::wg_row "$ts_fmt" "$client_name" "$endpoint_display" "handshake" \ fi
"$w_client" "$w_dest") row=$(ui::watch::wg_row "$ts_fmt" "$client_name" "$ep_raw" "handshake" \
"$ep_resolved" "$w_client" "30")
rows+=("${ts}|${row}") rows+=("${ts}|${row}")
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
@ -217,10 +218,18 @@ function cmd::watch::_tail_events() {
local ts_fmt local ts_fmt
ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)") ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
local dest_display local fw_svc_name
dest_display=$(resolve::dest "$dest_ip" "$dest_port" "$proto") fw_svc_name=$(resolve::service_name "$dest_ip" "$dest_port" "$proto")
local fw_src_ep fw_src_resolved=""
ui::watch::fw_row "$ts_fmt" "$client" "$dest_display" "$w_client" "$w_dest" 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 else
$restricted_only && continue $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)") ts_fmt=$(fmt::datetime_short "$(json::iso_to_ts "$ts" 2>/dev/null || echo 0)")
# Resolve endpoint — fall back to endpoint cache if empty # Resolve endpoint — fall back to endpoint cache if empty
local endpoint_resolved local wg_ep_raw wg_ep_resolved=""
endpoint_resolved=$(resolve::ip "${endpoint:-}") wg_ep_raw="${endpoint:-}"
if [[ -z "$endpoint_resolved" && -n "$endpoint" ]]; then if [[ -n "$wg_ep_raw" ]]; then
endpoint_resolved="$endpoint" local wg_ep_parts
wg_ep_parts=$(resolve::endpoint_parts "$wg_ep_raw")
wg_ep_resolved="${wg_ep_parts#*|}"
fi fi
[[ -z "$endpoint_resolved" ]] && endpoint_resolved="-" ui::watch::wg_row "$ts_fmt" "$client" "$wg_ep_raw" "$event" \
"$wg_ep_resolved" "$w_client" "30"
ui::watch::wg_row "$ts_fmt" "$client" "$endpoint_resolved" "$event" \
"$w_client" "$w_dest"
fi fi
done done

View file

@ -149,6 +149,12 @@ function monitor::live() {
rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true rm -f /tmp/wgctl_hs_* /tmp/wgctl_attempt_* 2>/dev/null || true
local w_client=20 w_dest=18 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 if ! $blocked_only && ! $restricted_only; then
( (

View file

@ -60,6 +60,49 @@ function resolve::dest() {
fi fi
} }
# resolve::service_name <ip> [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 <endpoint_ip>
# 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 # resolve::clear_cache
# Clears the resolution cache — call between commands if needed. # Clears the resolution cache — call between commands if needed.
function resolve::clear_cache() { function resolve::clear_cache() {

View file

@ -126,61 +126,61 @@ function ui::logs::fw_row_table() {
printf " %-20s %-18s %-25s %s%s\n" "$ts" "$client" "$dst" "$proto" "$count_str" printf " %-20s %-18s %-25s %s%s\n" "$ts" "$client" "$dst" "$proto" "$count_str"
} }
function ui::logs::wg_row() { # function ui::logs::wg_row() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \ # local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \ # count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \
gap_seconds="${8:-}" resolved="${9:-}" # gap_seconds="${8:-}" resolved="${9:-}"
local event_color # local event_color
case "$event" in # case "$event" in
handshake) event_color="\033[1;32m" ;; # handshake) event_color="\033[1;32m" ;;
attempt) event_color="\033[1;31m" ;; # attempt) event_color="\033[1;31m" ;;
*) event_color="\033[0;37m" ;; # *) event_color="\033[0;37m" ;;
esac # esac
local count_suffix="" # local count_suffix=""
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" # [[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m"
# Gap suffix — offline label only when gap > threshold * 2 # # Gap suffix — offline label only when gap > threshold * 2
local gap_suffix="" # local gap_suffix=""
if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then # if [[ "$event" == "handshake" && -n "$gap_seconds" && "$gap_seconds" -gt 0 ]]; then
local gap_int="$gap_seconds" # local gap_int="$gap_seconds"
local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}" # local threshold="${WG_HANDSHAKE_CHECK_TIME_SEC:-300}"
local offline_label="" # local offline_label=""
[[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline" # [[ "$gap_int" -gt $(( threshold * 2 )) ]] && offline_label=" offline"
if (( gap_int >= 3600 )); then # if (( gap_int >= 3600 )); then
gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m" # gap_suffix=" \033[2m↑ $(( gap_int / 3600 ))h${offline_label}\033[0m"
elif (( gap_int >= 60 )); then # elif (( gap_int >= 60 )); then
gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m" # gap_suffix=" \033[2m↑ $(( gap_int / 60 ))m${offline_label}\033[0m"
fi # fi
fi # fi
# ── Endpoint — native padding, no ui::pad_mb ── # # ── Endpoint — native padding, no ui::pad_mb ──
local endpoint_colored endpoint_visible_len # local endpoint_colored endpoint_visible_len
local endpoint_raw="${endpoint:--}" # local endpoint_raw="${endpoint:--}"
if [[ -n "$resolved" && -n "$endpoint" ]]; then # if [[ -n "$resolved" && -n "$endpoint" ]]; then
endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" # endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m"
endpoint_visible_len=$(( ${#endpoint} + 4 + ${#resolved} - _UI_ARROW_EXTRA )) # endpoint_visible_len=$(( ${#endpoint} + 4 + ${#resolved} - _UI_ARROW_EXTRA ))
else # else
endpoint_colored="$endpoint_raw" # endpoint_colored="$endpoint_raw"
# "-" is 1 char; endpoint may be empty # # "-" is 1 char; endpoint may be empty
[[ -n "$endpoint" ]] && endpoint_visible_len=${#endpoint} \ # [[ -n "$endpoint" ]] && endpoint_visible_len=${#endpoint} \
|| endpoint_visible_len=1 # || endpoint_visible_len=1
fi # fi
local ep_pad_n=$(( w_endpoint - endpoint_visible_len )) # local ep_pad_n=$(( w_endpoint - endpoint_visible_len ))
[[ $ep_pad_n -lt 0 ]] && ep_pad_n=0 # [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0
local endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "") # local endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "")
local ts_pad client_pad # local ts_pad client_pad
ts_pad=$(printf "%-11s" "$ts") # ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client") # client_pad=$(printf "%-${w_client}s" "$client")
printf " %s %s %b %b%s\033[0m%b%b\n" \ # printf " %s %s %b %b%s\033[0m%b%b\n" \
"$ts_pad" "$client_pad" \ # "$ts_pad" "$client_pad" \
"$endpoint_padded" \ # "$endpoint_padded" \
"$event_color" "$event" "$count_suffix" "$gap_suffix" # "$event_color" "$event" "$count_suffix" "$gap_suffix"
} # }
function ui::logs::wg_row_table() { function ui::logs::wg_row_table() {
local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" count="${5:-1}" 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_FW_COLOR="\033[1;31m"
_UI_WATCH_WG_COLOR="\033[1;32m" _UI_WATCH_WG_COLOR="\033[1;32m"
function ui::watch::fw_row() { # function ui::watch::fw_row() {
local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \ # local ts="${1:-}" client="${2:-}" dest_display="${3:-}" \
w_client="${4:-20}" w_dest="${5:-18}" # w_client="${4:-20}" w_dest="${5:-18}"
# "fw" is always 2 visible chars — no padding needed # # "fw" is always 2 visible chars — no padding needed
local src="${_UI_WATCH_FW_COLOR}fw\033[0m" # local src="${_UI_WATCH_FW_COLOR}fw\033[0m"
local ts_pad client_pad dest_pad_n # local ts_pad client_pad dest_pad_n
ts_pad=$(printf "%-11s" "$ts") # ts_pad=$(printf "%-11s" "$ts")
client_pad=$(printf "%-${w_client}s" "$client") # client_pad=$(printf "%-${w_client}s" "$client")
dest_pad_n=$(( w_dest - ${#dest_display} )) # dest_pad_n=$(( w_dest - ${#dest_display} ))
[[ $dest_pad_n -lt 0 ]] && dest_pad_n=0 # [[ $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" \ # 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" "" # "$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:-}"
local event_color # function ui::watch::wg_row() {
case "$event" in # local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
handshake) event_color="\033[1;32m" ;; # w_client="${5:-20}" w_endpoint="${6:-18}"
attempt) event_color="\033[1;31m" ;;
*) event_color="\033[0;37m" ;;
esac
local count_suffix="" # local event_color
[[ "$count" -gt 1 ]] && count_suffix=" \033[2m(x${count})\033[0m" # case "$event" in
# handshake) event_color="\033[1;32m" ;;
# attempt) event_color="\033[1;31m" ;;
# *) event_color="\033[0;37m" ;;
# esac
local gap_suffix="" # local endpoint_display="${endpoint:--}"
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 ── # local ts_pad client_pad ep_pad_n
# " → " = 5 bytes, 3 visible → visible = endpoint + 3 + resolved # ts_pad=$(printf "%-11s" "$ts")
local endpoint_colored endpoint_visible_len # client_pad=$(printf "%-${w_client}s" "$client")
if [[ -n "$resolved" && -n "$endpoint" ]]; then # ep_pad_n=$(( w_endpoint - ${#endpoint_display} ))
endpoint_colored="${endpoint} \033[2m→ ${resolved}\033[0m" # [[ $ep_pad_n -lt 0 ]] && ep_pad_n=0
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 )) # printf " %s %s %s%*s %b%s\033[0m\n" \
[[ $ep_pad_n -lt 0 ]] && ep_pad_n=0 # "$ts_pad" "$client_pad" \
local endpoint_padded # "$endpoint_display" "$ep_pad_n" "" \
endpoint_padded=$(printf "%b%*s" "$endpoint_colored" "$ep_pad_n" "") # "$event_color" "$event"
# }
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" \ # function ui::logs::wg_row() {
"$ts_pad" "$client_pad" \ # local ts="${1:-}" client="${2:-}" endpoint="${3:-}" event="${4:-}" \
"$endpoint_padded" \ # count="${5:-1}" w_client="${6:-20}" w_endpoint="${7:-20}" \
"$event_color" "$event" "$count_suffix" "$gap_suffix" # 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
# # ── 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 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"
# }
function ui::watch::header_table() { function ui::watch::header_table() {
printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \ printf "\n %-20s %-8s %-22s %-28s %-14s %s\n" \
@ -295,3 +322,190 @@ function ui::watch::wg_row_table() {
printf " %-20s %-8s %-22s %-28s %b%-14s\033[0m %s\n" \ printf " %-20s %-8s %-22s %-28s %b%-14s\033[0m %s\n" \
"$ts" "wireguard" "$client" "$endpoint" "$event_color" "$event" "$status" "$ts" "wireguard" "$client" "$endpoint" "$event_color" "$event" "$status"
} }
# 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"
}