feat: list migration to command framework
- commands/list/list.sh: router + command::define show [*] - commands/list/show.sh: flag::define + flag::parse, all helpers - flag::exclusive: register under top-level command name (strip ::subcmd) - flag::parse: validate exclusive groups, error on conflicting user flags - --help: intercept in command::run before routing - help::auto: don't show default subcommand in usage line
This commit is contained in:
parent
8ed491313d
commit
de22dbeec7
6 changed files with 727 additions and 10 deletions
8
commands/list/list.sh
Normal file
8
commands/list/list.sh
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/list/list.sh — router only
|
||||||
|
|
||||||
|
function cmd::list::on_load() {
|
||||||
|
command::define show "List WireGuard clients" [*]
|
||||||
|
}
|
||||||
|
|
||||||
|
hook::on "command:help:list" command::help::auto
|
||||||
682
commands/list/show.sh
Normal file
682
commands/list/show.sh
Normal file
|
|
@ -0,0 +1,682 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# commands/list/show.sh
|
||||||
|
|
||||||
|
function cmd::list::show::on_load() {
|
||||||
|
command::mixin json_output [section="Output"]
|
||||||
|
|
||||||
|
help::section "Filters"
|
||||||
|
flag::define --name value "Show single peer" [label="name", section="Filters"]
|
||||||
|
flag::define --type value "Filter by device type" [label="type", section="Filters"]
|
||||||
|
flag::define --rule value "Filter by rule" [label="rule", section="Filters"]
|
||||||
|
flag::define --group value "Filter by group" [label="group", section="Filters"]
|
||||||
|
flag::define --identity value "Filter by identity" [label="identity", section="Filters"]
|
||||||
|
flag::define --online bool "Show online peers only" [section="Filters"]
|
||||||
|
flag::define --offline bool "Show offline peers only" [section="Filters"]
|
||||||
|
flag::define --restricted bool "Show restricted peers" [section="Filters"]
|
||||||
|
flag::define --blocked bool "Show blocked peers" [section="Filters"]
|
||||||
|
flag::define --allowed bool "Show allowed peers" [section="Filters"]
|
||||||
|
|
||||||
|
help::section "Output"
|
||||||
|
flag::define --detailed bool "Show detailed view" [section="Output"]
|
||||||
|
|
||||||
|
flag::exclusive --online --offline --blocked --restricted --allowed
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::list::show::run() {
|
||||||
|
flag::parse "$@" || return 1
|
||||||
|
|
||||||
|
local filter_type; filter_type=$(flag::value --type)
|
||||||
|
local filter_rule; filter_rule=$(flag::value --rule)
|
||||||
|
local filter_group; filter_group=$(flag::value --group)
|
||||||
|
local filter_identity; filter_identity=$(flag::value --identity)
|
||||||
|
local single_name; single_name=$(flag::value --name)
|
||||||
|
local online_only=false offline_only=false
|
||||||
|
local restricted_only=false blocked_only=false allowed_only=false
|
||||||
|
local detailed=false
|
||||||
|
|
||||||
|
flag::bool --online && online_only=true
|
||||||
|
flag::bool --offline && offline_only=true
|
||||||
|
flag::bool --restricted && restricted_only=true
|
||||||
|
flag::bool --blocked && blocked_only=true
|
||||||
|
flag::bool --allowed && allowed_only=true
|
||||||
|
flag::bool --detailed && detailed=true
|
||||||
|
|
||||||
|
if [[ -n "$single_name" ]]; then
|
||||||
|
cmd::list::show_client "$single_name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local dir
|
||||||
|
dir="$(ctx::clients)"
|
||||||
|
local confs=("${dir}"/*.conf)
|
||||||
|
if [[ ! -f "${confs[0]}" ]]; then
|
||||||
|
log::wg_warning "No clients configured"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
cmd::list::_precompute_all
|
||||||
|
|
||||||
|
declare -gA p_identity_filter=()
|
||||||
|
if [[ -n "$filter_identity" ]]; then
|
||||||
|
identity::require_exists "$filter_identity" || return 1
|
||||||
|
while IFS= read -r peer_name; do
|
||||||
|
[[ -n "$peer_name" ]] && p_identity_filter["$peer_name"]=1
|
||||||
|
done < <(identity::peers "$filter_identity")
|
||||||
|
if [[ ${#p_identity_filter[@]} -eq 0 ]]; then
|
||||||
|
log::wg_warning "Identity '${filter_identity}' has no peers"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
log::section "WireGuard Clients"
|
||||||
|
|
||||||
|
local collected_rows=""
|
||||||
|
collected_rows=$(cmd::list::_collect_all_rows | ui::sort_rows)
|
||||||
|
if [[ -z "$collected_rows" ]]; then
|
||||||
|
log::wg_warning "No results found"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command::json; then
|
||||||
|
cmd::list::_output_json "$collected_rows"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if $detailed; then
|
||||||
|
cmd::list::_render_detailed "$collected_rows"
|
||||||
|
cmd::list::_render_summary_from_rows "$collected_rows"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
display::render "peer_list" "$collected_rows" \
|
||||||
|
"cmd::list::_render_compact" "cmd::list::_render_table"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Detail Card
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::show_client() {
|
||||||
|
local name="${1:-}"
|
||||||
|
local conf
|
||||||
|
conf="$(ctx::clients)/${name}.conf"
|
||||||
|
|
||||||
|
if [[ ! -f "$conf" ]]; then
|
||||||
|
log::error "Client not found: ${name}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ip
|
||||||
|
ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
|
|
||||||
|
local allowed_ips
|
||||||
|
allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs)
|
||||||
|
|
||||||
|
local public_key
|
||||||
|
public_key=$(keys::public "$name" 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
local type
|
||||||
|
type=$(peers::get_meta "$name" "type" 2>/dev/null)
|
||||||
|
[[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip")
|
||||||
|
|
||||||
|
local endpoint="—"
|
||||||
|
local ep
|
||||||
|
ep=$(monitor::endpoint_for_key "$public_key")
|
||||||
|
[[ -z "$ep" ]] && ep=$(monitor::last_endpoint "$name")
|
||||||
|
[[ -n "$ep" ]] && endpoint="$ep"
|
||||||
|
|
||||||
|
local is_blocked="false"
|
||||||
|
peers::is_blocked "$name" && is_blocked="true"
|
||||||
|
|
||||||
|
local is_restricted="false"
|
||||||
|
peers::is_restricted "$name" && is_restricted="true"
|
||||||
|
|
||||||
|
local handshake_ts
|
||||||
|
handshake_ts=$(monitor::get_handshake_ts "$public_key")
|
||||||
|
|
||||||
|
local last_ts
|
||||||
|
last_ts=$(monitor::last_attempt "$name")
|
||||||
|
|
||||||
|
local status
|
||||||
|
status=$(peers::format_status_verbose "$name" "$public_key" \
|
||||||
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
|
|
||||||
|
local last_seen
|
||||||
|
last_seen=$(peers::format_last_seen "$name" "$public_key" \
|
||||||
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
|
|
||||||
|
local blocks=""
|
||||||
|
if block::has_file "$name" && block::is_blocked "$name"; then
|
||||||
|
if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then
|
||||||
|
blocks="all traffic blocked"
|
||||||
|
fi
|
||||||
|
local rule_lines
|
||||||
|
rule_lines=$(block::format_rules "$name")
|
||||||
|
[[ -n "$rule_lines" ]] && blocks+="$rule_lines"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui::section "Client: ${name}"
|
||||||
|
ui::row "IP" "$ip"
|
||||||
|
ui::row "Type" "$(peers::display_type "$type")"
|
||||||
|
ui::row "Status" "$(echo -e "$status")"
|
||||||
|
ui::row "Endpoint" "$endpoint"
|
||||||
|
ui::row "Last seen" "$last_seen"
|
||||||
|
ui::row "AllowedIPs" "$allowed_ips"
|
||||||
|
ui::row "Public key" "$public_key"
|
||||||
|
|
||||||
|
if [[ -z "$blocks" ]]; then
|
||||||
|
ui::row "Blocks" "none"
|
||||||
|
elif [[ "$blocks" == *"all traffic blocked"* ]]; then
|
||||||
|
ui::row "Blocks" "$(echo -e "\033[1;31mAll\033[0m")"
|
||||||
|
else
|
||||||
|
printf " %-20s\n" "Blocks:"
|
||||||
|
echo -e "$blocks"
|
||||||
|
fi
|
||||||
|
printf "\n"
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Row collection (single pass, all filters)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_collect_all_rows() {
|
||||||
|
local dir
|
||||||
|
dir="$(ctx::clients)"
|
||||||
|
local _verbose_status="${LIST_VERBOSE_STATUS:-true}"
|
||||||
|
|
||||||
|
for conf in "${dir}"/*.conf; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
local client_name
|
||||||
|
client_name=$(basename "$conf" .conf)
|
||||||
|
[[ -z "$client_name" ]] && continue
|
||||||
|
|
||||||
|
if [[ ${#p_identity_filter[@]} -gt 0 && \
|
||||||
|
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ip="${p_ips[$client_name]:-}"
|
||||||
|
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
|
[[ -z "$ip" ]] && continue
|
||||||
|
|
||||||
|
local type="${p_types[$client_name]:-unknown}"
|
||||||
|
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||||
|
|
||||||
|
local pubkey="${p_pubkeys[$client_name]:-}"
|
||||||
|
local handshake_ts="${wg_handshakes[$pubkey]:-0}"
|
||||||
|
local is_blocked="${p_blocked[$client_name]:-false}"
|
||||||
|
local is_restricted="${p_restricted[$client_name]:-false}"
|
||||||
|
local last_ts="${p_last_ts[$client_name]:-}"
|
||||||
|
local rule="${p_rules[$client_name]:-}"
|
||||||
|
local group="${p_main_groups[$client_name]:-}"
|
||||||
|
|
||||||
|
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || continue; fi
|
||||||
|
if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || continue; fi
|
||||||
|
if $restricted_only && [[ "$is_restricted" != "true" ]]; then continue; fi
|
||||||
|
if $blocked_only && [[ "$is_blocked" != "true" ]]; then continue; fi
|
||||||
|
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||||
|
[[ "$is_restricted" == "true" ]]; }; then continue; fi
|
||||||
|
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then continue; fi
|
||||||
|
if [[ -n "$filter_group" ]]; then
|
||||||
|
local all_groups="${peer_group_map[$client_name]:-}"
|
||||||
|
[[ "$all_groups" != *"$filter_group"* ]] && continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve status — verbose or simple
|
||||||
|
local status
|
||||||
|
if [[ "$_verbose_status" == "true" ]]; then
|
||||||
|
status=$(peers::format_status_verbose "$client_name" "$pubkey" \
|
||||||
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts" | \
|
||||||
|
sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
else
|
||||||
|
local state
|
||||||
|
state=$(peers::connection_state "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
|
status="${state%%|*}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Resolve last seen
|
||||||
|
local last_seen="-"
|
||||||
|
if [[ "$is_blocked" == "true" && -n "$last_ts" && "$last_ts" != "0" ]]; then
|
||||||
|
local attempt_ts
|
||||||
|
attempt_ts=$(json::iso_to_ts "$last_ts")
|
||||||
|
last_seen="$(fmt::datetime_short "$attempt_ts") (dropped)"
|
||||||
|
elif [[ -n "$handshake_ts" && "$handshake_ts" != "0" ]]; then
|
||||||
|
local ts_display
|
||||||
|
ts_display=$(fmt::datetime_short "$handshake_ts")
|
||||||
|
if [[ "$status" == "online"* ]]; then
|
||||||
|
last_seen="${ts_display} (handshake)"
|
||||||
|
else
|
||||||
|
last_seen="$ts_display"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "%s|%s|%s|%s|%s|%s|%s|%s|%s\n" \
|
||||||
|
"$client_name" "$ip" "$type" \
|
||||||
|
"${rule:--}" "${group:--}" \
|
||||||
|
"$status" "$last_seen" \
|
||||||
|
"$is_blocked" "$is_restricted"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Compact render
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_render_compact() {
|
||||||
|
local rows="${1:-}"
|
||||||
|
|
||||||
|
# Measure column widths from pure values (fields 1-5, no labels)
|
||||||
|
local w_name w_ip w_type w_rule w_group
|
||||||
|
w_name=$(ui::measure_col "$rows" 1 14)
|
||||||
|
w_ip=$(ui::measure_col "$rows" 2 13)
|
||||||
|
w_type=$(ui::measure_col "$rows" 3 7)
|
||||||
|
w_rule=$(ui::measure_col "$rows" 4 4)
|
||||||
|
w_group=$(ui::measure_col "$rows" 5 4)
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
ui::peer::list_row_compact \
|
||||||
|
"$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" \
|
||||||
|
"$name" "$ip" "$type" "$rule" "$group" \
|
||||||
|
"$status" "$last_seen" "$is_blocked" "$is_restricted"
|
||||||
|
done <<< "$rows"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cmd::list::_render_summary_from_rows "$rows"
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Table render (kept for config switching)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_render_table() {
|
||||||
|
local rows="${1:-}"
|
||||||
|
[[ -z "$rows" ]] && log::wg_warning "No results found" && return 0
|
||||||
|
|
||||||
|
# Measure column widths from data (same as compact)
|
||||||
|
local w_name=16 w_ip=13 w_type=8 w_rule=10 w_group=10 w_status=10 w_last=20
|
||||||
|
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
(( ${#name} > w_name )) && w_name=${#name}
|
||||||
|
(( ${#ip} > w_ip )) && w_ip=${#ip}
|
||||||
|
(( ${#type} > w_type )) && w_type=${#type}
|
||||||
|
(( ${#rule} > w_rule )) && w_rule=${#rule}
|
||||||
|
(( ${#group} > w_group )) && w_group=${#group}
|
||||||
|
(( ${#last_seen} > w_last )) && w_last=${#last_seen}
|
||||||
|
local cs
|
||||||
|
cs=$(printf "%s" "$status" | sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
(( ${#cs} > w_status )) && w_status=${#cs}
|
||||||
|
done <<< "$rows"
|
||||||
|
(( w_name += 2 )); (( w_ip += 2 ))
|
||||||
|
(( w_type += 2 )); (( w_rule += 2 ))
|
||||||
|
(( w_group += 2 )); (( w_last += 2 ))
|
||||||
|
|
||||||
|
# Header
|
||||||
|
printf "\n %-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %-${w_status}s %s\n" \
|
||||||
|
"NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..115})"
|
||||||
|
|
||||||
|
# Rows
|
||||||
|
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
local clean_status
|
||||||
|
clean_status=$(echo "$status" | sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
local status_pad_n=$(( w_status - ${#clean_status} ))
|
||||||
|
[[ $status_pad_n -lt 0 ]] && status_pad_n=0
|
||||||
|
|
||||||
|
local row_color status_color
|
||||||
|
row_color=$(ui::peer::_row_color "$is_blocked" "$is_restricted" "$clean_status")
|
||||||
|
status_color=$(ui::peer::status_color "$is_blocked" "$is_restricted" "$clean_status")
|
||||||
|
|
||||||
|
local status_colored="${status_color}${clean_status}\033[0m"
|
||||||
|
|
||||||
|
local last_seen_colored="$last_seen"
|
||||||
|
[[ -n "$row_color" ]] && last_seen_colored="${row_color}${last_seen}\033[0m" \
|
||||||
|
|| last_seen_colored="${status_color}${last_seen}\033[0m"
|
||||||
|
|
||||||
|
if [[ -n "$row_color" ]]; then
|
||||||
|
printf " %b%-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %-${w_status}s %s\033[0m\n" \
|
||||||
|
"$row_color" "$name" "$ip" "$type" "$rule" "$group" "$clean_status" "$last_seen"
|
||||||
|
else
|
||||||
|
printf " %-${w_name}s %-${w_ip}s %-${w_type}s %-${w_rule}s %-${w_group}s %b%*s\033[0m %b\n" \
|
||||||
|
"$name" "$ip" "$type" "$rule" "$group" \
|
||||||
|
"$status_color${clean_status}" "$status_pad_n" "" \
|
||||||
|
"$last_seen_colored"
|
||||||
|
fi
|
||||||
|
done <<< "$rows"
|
||||||
|
|
||||||
|
printf " %s\n" "$(printf '─%.0s' {1..115})"
|
||||||
|
cmd::list::_render_summary_from_rows "$rows"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::list::_iter_confs_table() {
|
||||||
|
local dir
|
||||||
|
dir="$(ctx::clients)"
|
||||||
|
for conf in "${dir}"/*.conf; do
|
||||||
|
[[ -f "$conf" ]] || continue
|
||||||
|
local client_name
|
||||||
|
client_name=$(basename "$conf" .conf)
|
||||||
|
[[ -z "$client_name" ]] && continue
|
||||||
|
|
||||||
|
if [[ ${#p_identity_filter[@]} -gt 0 && \
|
||||||
|
-z "${p_identity_filter[$client_name]:-}" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
local ip="${p_ips[$client_name]:-}"
|
||||||
|
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
|
||||||
|
|
||||||
|
local type="${p_types[$client_name]:-unknown}"
|
||||||
|
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
|
||||||
|
|
||||||
|
cmd::list::_render_row "$client_name" "$ip" "$type"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Detailed render (grouped by identity)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_render_detailed() {
|
||||||
|
local rows="${1:-}"
|
||||||
|
|
||||||
|
# Measure widths
|
||||||
|
local w_name w_ip w_type w_rule w_group w_subnet
|
||||||
|
w_name=$(ui::measure_col "$rows" 1 14)
|
||||||
|
w_ip=$(ui::measure_col "$rows" 2 13)
|
||||||
|
w_type=$(ui::measure_col "$rows" 3 7)
|
||||||
|
w_rule=$(ui::measure_col "$rows" 4 4)
|
||||||
|
w_group=$(ui::measure_col "$rows" 5 4)
|
||||||
|
# subnet not in rows — use fixed width
|
||||||
|
w_subnet=10
|
||||||
|
|
||||||
|
# Group by identity
|
||||||
|
declare -A identity_rows=()
|
||||||
|
local no_identity_rows=""
|
||||||
|
|
||||||
|
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
local id_name
|
||||||
|
id_name=$(identity::get_name "$name")
|
||||||
|
local row="${name}|${ip}|${type}|${rule}|${group}|${status}|${last_seen}|${is_blocked}|${is_restricted}"
|
||||||
|
if [[ -n "$id_name" ]]; then
|
||||||
|
identity_rows["$id_name"]+="${row}"$'\n'
|
||||||
|
else
|
||||||
|
no_identity_rows+="${row}"$'\n'
|
||||||
|
fi
|
||||||
|
done <<< "$rows"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Render identity groups (sorted)
|
||||||
|
for id_name in $(echo "${!identity_rows[@]}" | tr ' ' '\n' | sort); do
|
||||||
|
ui::peer::list_identity_header "$id_name"
|
||||||
|
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
|
local peer_type="${p_types[$name]:-}"
|
||||||
|
subnet=$(peers::get_display_subnet "$name" "$peer_type")
|
||||||
|
|
||||||
|
ui::peer::list_row_detailed \
|
||||||
|
"$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \
|
||||||
|
"$name" "$ip" "$type" "$rule" "$group" "$subnet" \
|
||||||
|
"$status" "$last_seen" "$is_blocked" "$is_restricted"
|
||||||
|
done < <(echo "${identity_rows[$id_name]}" | ui::sort_rows)
|
||||||
|
done
|
||||||
|
|
||||||
|
# Render peers without identity (no "other" header if empty)
|
||||||
|
if [[ -n "$no_identity_rows" ]]; then
|
||||||
|
local trimmed
|
||||||
|
trimmed=$(echo "$no_identity_rows" | grep -v '^$')
|
||||||
|
if [[ -n "$trimmed" ]]; then
|
||||||
|
ui::peer::list_identity_header "other"
|
||||||
|
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
local subnet
|
||||||
|
subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null)
|
||||||
|
if [[ -z "$subnet" ]]; then
|
||||||
|
local peer_type="${p_types[$name]:-}"
|
||||||
|
[[ -n "$peer_type" ]] && subnet="$peer_type"
|
||||||
|
fi
|
||||||
|
[[ -z "$subnet" ]] && subnet="-"
|
||||||
|
ui::peer::list_row_detailed \
|
||||||
|
"$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \
|
||||||
|
"$name" "$ip" "$type" "$rule" "$group" "$subnet" \
|
||||||
|
"$status" "$last_seen" "$is_blocked" "$is_restricted"
|
||||||
|
done <<< "$trimmed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Summary
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_render_summary_from_rows() {
|
||||||
|
local rows="${1:-}"
|
||||||
|
declare -A rule_counts=() group_counts=()
|
||||||
|
local total=0
|
||||||
|
|
||||||
|
while IFS='|' read -r name ip type rule group rest; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
(( total++ )) || true
|
||||||
|
rule_counts["${rule:--}"]=$(( ${rule_counts[${rule:--}]:-0} + 1 )) || true
|
||||||
|
[[ "$group" != "-" && -n "$group" ]] && \
|
||||||
|
group_counts["$group"]=$(( ${group_counts[$group]:-0} + 1 )) || true
|
||||||
|
done <<< "$rows"
|
||||||
|
|
||||||
|
local rule_summary=""
|
||||||
|
for r in $(echo "${!rule_counts[@]}" | tr ' ' '\n' | sort); do
|
||||||
|
rule_summary+="${rule_counts[$r]} ${r}, "
|
||||||
|
done
|
||||||
|
rule_summary="${rule_summary%, }"
|
||||||
|
|
||||||
|
local group_summary=""
|
||||||
|
for g in $(echo "${!group_counts[@]}" | tr ' ' '\n' | sort); do
|
||||||
|
group_summary+="${group_counts[$g]} in ${g}, "
|
||||||
|
done
|
||||||
|
group_summary="${group_summary%, }"
|
||||||
|
|
||||||
|
if [[ -n "$group_summary" ]]; then
|
||||||
|
printf " Showing %s peers [%s] — %s\n\n" "$total" "$rule_summary" "$group_summary"
|
||||||
|
else
|
||||||
|
printf " Showing %s peers [%s]\n\n" "$total" "$rule_summary"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Table row rendering
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_render_row() {
|
||||||
|
local client_name="$1" ip="$2" type="$3"
|
||||||
|
|
||||||
|
local pubkey="${p_pubkeys[$client_name]:-}"
|
||||||
|
local handshake_ts="${wg_handshakes[$pubkey]:-0}"
|
||||||
|
local is_blocked="${p_blocked[$client_name]:-false}"
|
||||||
|
local is_restricted="${p_restricted[$client_name]:-false}"
|
||||||
|
local last_ts="${p_last_ts[$client_name]:-}"
|
||||||
|
|
||||||
|
if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
|
if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi
|
||||||
|
if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi
|
||||||
|
if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi
|
||||||
|
if $allowed_only && { [[ "$is_blocked" == "true" ]] || \
|
||||||
|
[[ "$is_restricted" == "true" ]]; }; then return 0; fi
|
||||||
|
|
||||||
|
if [[ -n "$filter_group" ]]; then
|
||||||
|
local all_groups="${peer_group_map[$client_name]:-}"
|
||||||
|
[[ "$all_groups" != *"$filter_group"* ]] && return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local status last_seen display_type rule
|
||||||
|
status=$(peers::format_status_verbose "$client_name" "$pubkey" \
|
||||||
|
"$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
|
||||||
|
last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \
|
||||||
|
"$is_blocked" "$last_ts" "" "$handshake_ts")
|
||||||
|
display_type=$(peers::display_type "$type")
|
||||||
|
rule="${p_rules[$client_name]:-—}"
|
||||||
|
|
||||||
|
if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi
|
||||||
|
|
||||||
|
if [[ "${_list_header_printed:-false}" == "false" ]]; then
|
||||||
|
cmd::list::_render_header $has_groups
|
||||||
|
_list_header_printed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true
|
||||||
|
|
||||||
|
local padded_status
|
||||||
|
padded_status=$(ui::pad_status "$status" 25)
|
||||||
|
|
||||||
|
if $has_groups; then
|
||||||
|
local main_group="${p_main_groups[$client_name]:-}"
|
||||||
|
local group_display="${main_group:-${peer_group_map[$client_name]:-—}}"
|
||||||
|
printf " %-28s %-15s %-13s %-12s %-12s %s %s\n" \
|
||||||
|
"$client_name" "$ip" "$display_type" "$rule" \
|
||||||
|
"$group_display" "$padded_status" "$last_seen"
|
||||||
|
else
|
||||||
|
printf " %-28s %-15s %-13s %-12s %s %s\n" \
|
||||||
|
"$client_name" "$ip" "$display_type" "$rule" \
|
||||||
|
"$padded_status" "$last_seen"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Precompute
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_precompute_all() {
|
||||||
|
declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=()
|
||||||
|
while IFS="|" read -r name ip rule type last_ts last_evt main_group; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
p_ips["$name"]="$ip"
|
||||||
|
p_rules["$name"]="${rule:-}"
|
||||||
|
p_types["$name"]="${type:-}"
|
||||||
|
p_last_ts["$name"]="$last_ts"
|
||||||
|
p_last_evt["$name"]="$last_evt"
|
||||||
|
p_main_groups["$name"]="${main_group:-}"
|
||||||
|
done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)")
|
||||||
|
|
||||||
|
for name in "${!p_ips[@]}"; do
|
||||||
|
[[ -n "${p_types[$name]:-}" ]] && continue
|
||||||
|
p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
|
||||||
|
done
|
||||||
|
|
||||||
|
declare -gA wg_handshakes=() wg_endpoints=()
|
||||||
|
while IFS=$'\t' read -r pubkey ts; do
|
||||||
|
[[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts"
|
||||||
|
done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
|
||||||
|
while IFS=$'\t' read -r pubkey endpoint; do
|
||||||
|
[[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint"
|
||||||
|
done < <(wg show "$(config::interface)" endpoints 2>/dev/null)
|
||||||
|
|
||||||
|
declare -gA p_blocked=() p_restricted=()
|
||||||
|
cmd::list::_precompute_block_status p_blocked p_restricted
|
||||||
|
|
||||||
|
declare -gA p_pubkeys=()
|
||||||
|
local dir
|
||||||
|
dir="$(ctx::clients)"
|
||||||
|
for kf in "${dir}"/*_public.key; do
|
||||||
|
[[ -f "$kf" ]] || continue
|
||||||
|
local kname
|
||||||
|
kname=$(basename "$kf" _public.key)
|
||||||
|
p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "")
|
||||||
|
done
|
||||||
|
|
||||||
|
has_groups=false
|
||||||
|
declare -gA peer_group_map=()
|
||||||
|
local groups_dir
|
||||||
|
groups_dir="$(ctx::groups)"
|
||||||
|
local group_files=("${groups_dir}"/*.group)
|
||||||
|
if [[ -f "${group_files[0]}" ]]; then
|
||||||
|
has_groups=true
|
||||||
|
while IFS=":" read -r peer_name group_name; do
|
||||||
|
[[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name"
|
||||||
|
done < <(json::peer_group_map "$groups_dir")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Identity precompute (for --identity filter)
|
||||||
|
declare -gA p_identity_filter=()
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::list::_precompute_block_status() {
|
||||||
|
local -n _blocked="$1"
|
||||||
|
local -n _restricted="$2"
|
||||||
|
|
||||||
|
local wg_peers
|
||||||
|
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
|
||||||
|
|
||||||
|
while IFS= read -r name; do
|
||||||
|
if block::has_specific_rules "$name" 2>/dev/null; then
|
||||||
|
_restricted["$name"]=true
|
||||||
|
else
|
||||||
|
_restricted["$name"]=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pubkey
|
||||||
|
pubkey=$(keys::public "$name" 2>/dev/null || echo "")
|
||||||
|
if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then
|
||||||
|
_blocked["$name"]=true
|
||||||
|
else
|
||||||
|
_blocked["$name"]=false
|
||||||
|
fi
|
||||||
|
done < <(peers::all)
|
||||||
|
}
|
||||||
|
# ============================================
|
||||||
|
# Header / Footer (table layout)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_render_header() {
|
||||||
|
ui::peer::list_header_table "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::list::_render_footer() {
|
||||||
|
ui::peer::list_footer_table "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::list::_build_group_summary() {
|
||||||
|
group_summary=""
|
||||||
|
if $has_groups; then
|
||||||
|
declare -A _gs=()
|
||||||
|
for peer in "${!peer_group_map[@]}"; do
|
||||||
|
local g="${peer_group_map[$peer]}"
|
||||||
|
_gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true
|
||||||
|
done
|
||||||
|
for g in "${!_gs[@]}"; do
|
||||||
|
group_summary+="${_gs[$g]} in ${g}, "
|
||||||
|
done
|
||||||
|
group_summary="${group_summary%, }"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmd::list::_show_client_safe() {
|
||||||
|
local name="$1"
|
||||||
|
cmd::list::show_client "$name" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# JSON (API consumption)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
function cmd::list::_output_json() {
|
||||||
|
local rows="${1:-}"
|
||||||
|
local -a peers=()
|
||||||
|
|
||||||
|
while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do
|
||||||
|
[[ -z "$name" ]] && continue
|
||||||
|
|
||||||
|
# Escape strings for JSON
|
||||||
|
local peer_json
|
||||||
|
peer_json=$(printf '{"name":"%s","ip":"%s","type":"%s","rule":"%s","group":"%s","status":"%s","last_seen":"%s","is_blocked":%s,"is_restricted":%s}' \
|
||||||
|
"$name" "$ip" "$type" \
|
||||||
|
"${rule}" "${group}" \
|
||||||
|
"$status" "$last_seen" \
|
||||||
|
"$is_blocked" "$is_restricted")
|
||||||
|
peers+=("$peer_json")
|
||||||
|
done <<< "$rows"
|
||||||
|
|
||||||
|
local count=${#peers[@]}
|
||||||
|
local array
|
||||||
|
# Join array with commas
|
||||||
|
array=$(printf '%s\n' "${peers[@]}" | paste -sd ',' -)
|
||||||
|
printf '{"peers":[%s]}' "$array" | json::envelope "list" "$count"
|
||||||
|
}
|
||||||
|
|
@ -324,6 +324,13 @@ function command::run() {
|
||||||
source "$subcmd_file"
|
source "$subcmd_file"
|
||||||
core::call_if_exists "cmd::${cmd}::${subcmd}::on_load"
|
core::call_if_exists "cmd::${cmd}::${subcmd}::on_load"
|
||||||
_CURRENT_LOADING_CMD=""
|
_CURRENT_LOADING_CMD=""
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
[[ "$arg" == "--help" || "$arg" == "-h" ]] && {
|
||||||
|
hook::fire "command:help:${cmd}" "$cmd" "$_ROUTED_SUBCMD"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
command::run_routed "$cmd" "$subcmd" "${routed_args[@]:-}"
|
command::run_routed "$cmd" "$subcmd" "${routed_args[@]:-}"
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,7 @@ declare -gA _FLAG_EXCLUSIVE_GROUPS=()
|
||||||
function flag::exclusive() {
|
function flag::exclusive() {
|
||||||
local cmd="${_CURRENT_LOADING_CMD:-}"
|
local cmd="${_CURRENT_LOADING_CMD:-}"
|
||||||
[[ -z "$cmd" ]] && return 0
|
[[ -z "$cmd" ]] && return 0
|
||||||
|
cmd="${cmd%%::*}"
|
||||||
|
|
||||||
# Join flags with comma as one group
|
# Join flags with comma as one group
|
||||||
local group
|
local group
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,24 @@ function flag::parse() {
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
local groups="${_FLAG_EXCLUSIVE_GROUPS[${_CURRENT_COMMAND%%::*}]:-}"
|
||||||
|
if [[ -n "$groups" ]]; then
|
||||||
|
local group
|
||||||
|
while IFS= read -r group; do
|
||||||
|
[[ -z "$group" ]] && continue
|
||||||
|
local -a members=()
|
||||||
|
IFS=',' read -ra members <<< "$group"
|
||||||
|
local found_count=0 found_flags=""
|
||||||
|
for member in "${members[@]}"; do
|
||||||
|
flag::set "$member" && (( found_count++ )) && found_flags+=" $member"
|
||||||
|
done
|
||||||
|
if [[ $found_count -gt 1 ]]; then
|
||||||
|
log::error "Flags${found_flags} are mutually exclusive"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done < <(echo "$groups" | tr '|' '\n')
|
||||||
|
fi
|
||||||
|
|
||||||
# Validate required flags
|
# Validate required flags
|
||||||
for key in "${!_FLAG_REGISTRY[@]}"; do
|
for key in "${!_FLAG_REGISTRY[@]}"; do
|
||||||
[[ "$key" != "${ctx}:"* ]] && continue
|
[[ "$key" != "${ctx}:"* ]] && continue
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,9 @@ function command::help::auto() {
|
||||||
|
|
||||||
# Print usage
|
# Print usage
|
||||||
local cmd_path="${cmd}"
|
local cmd_path="${cmd}"
|
||||||
[[ -n "$subcmd" ]] && cmd_path="${cmd} ${subcmd}"
|
local default_subcmd="${_COMMAND_DEFAULT[$cmd]:-}"
|
||||||
|
# Only show subcmd in usage if it's not the default
|
||||||
|
[[ -n "$subcmd" && "$subcmd" != "$default_subcmd" ]] && cmd_path="${cmd} ${subcmd}"
|
||||||
printf "\nUsage: wgctl %s" "$cmd_path"
|
printf "\nUsage: wgctl %s" "$cmd_path"
|
||||||
for part in "${usage_parts[@]:-}"; do
|
for part in "${usage_parts[@]:-}"; do
|
||||||
printf " %s" "$part"
|
printf " %s" "$part"
|
||||||
|
|
@ -148,14 +149,14 @@ function command::help::auto() {
|
||||||
for sc in "${subcmds[@]}"; do
|
for sc in "${subcmds[@]}"; do
|
||||||
local sc_key="${cmd}:${sc}"
|
local sc_key="${cmd}:${sc}"
|
||||||
local sc_desc="${_COMMAND_DEFS[$sc_key]:-}"
|
local sc_desc="${_COMMAND_DEFS[$sc_key]:-}"
|
||||||
local sc_type
|
|
||||||
sc_type=$(echo "$sc_desc" | cut -d'|' -f1)
|
local sc_text sc_aliases sc_default
|
||||||
local sc_text
|
sc_text=$(echo "$sc_desc" | cut -d'|' -f1)
|
||||||
sc_text=$(echo "$sc_desc" | cut -d'|' -f2)
|
sc_aliases=$(echo "$sc_desc" | cut -d'|' -f2)
|
||||||
local sc_aliases
|
sc_default=$(echo "$sc_desc" | cut -d'|' -f3)
|
||||||
sc_aliases=$(echo "$sc_desc" | cut -d'|' -f3)
|
|
||||||
local sc_default
|
# Clean up empty aliases
|
||||||
sc_default=$(echo "$sc_desc" | cut -d'|' -f4)
|
[[ "$sc_aliases" == "false" || "$sc_aliases" == "true" ]] && sc_aliases=""
|
||||||
|
|
||||||
local suffix=""
|
local suffix=""
|
||||||
[[ "$sc_default" == "true" ]] && suffix=" (default)"
|
[[ "$sc_default" == "true" ]] && suffix=" (default)"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue