gistfile1.txt
· 20 KiB · Text
Ham
#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::list::on_load() {
load_module identity
load_module ui
flag::register --type
flag::register --rule
flag::register --group
flag::register --identity
flag::register --online
flag::register --offline
flag::register --restricted
flag::register --blocked
flag::register --allowed
flag::register --detailed
flag::register --name
}
# ============================================
# Help
# ============================================
function cmd::list::help() {
cat <<EOF
Usage: wgctl list [options]
List all WireGuard clients.
Options:
--type <type> Filter by device type
--rule <rule> Filter by assigned rule
--group <group> Filter by group membership
--identity <name> Filter by identity
--online Show only connected clients
--offline Show only disconnected clients
--blocked Show only fully blocked clients
--restricted Show only restricted clients
--allowed Show only unrestricted clients
--detailed Show detailed view grouped by identity
--name <name> Show detail card for a single client
Examples:
wgctl list
wgctl list --type phone
wgctl list --identity nuno
wgctl list --online
wgctl list --blocked
wgctl list --detailed
wgctl list --name phone-nuno
EOF
}
# ============================================
# 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"
}
# ============================================
# Run
# ============================================
function cmd::list::run() {
local filter_type="" filter_rule="" filter_group="" filter_identity=""
local online_only=false offline_only=false
local restricted_only=false blocked_only=false allowed_only=false
local detailed=false single_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--type) filter_type="$2"; shift 2 ;;
--rule) filter_rule="$2"; shift 2 ;;
--group) filter_group="$2"; shift 2 ;;
--identity) filter_identity="$2"; shift 2 ;;
--online) online_only=true; shift ;;
--offline) offline_only=true; shift ;;
--restricted) restricted_only=true; shift ;;
--blocked) blocked_only=true; shift ;;
--allowed) allowed_only=true; shift ;;
--detailed) detailed=true; shift ;;
--name) single_name="$2"; shift 2 ;;
--help) cmd::list::help; return ;;
*)
log::error "Unknown flag: $1"
cmd::list::help
return 1
;;
esac
done
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
# Resolve identity filter
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"
# Collect all filtered rows first (needed for dynamic column widths)
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 $detailed; then
cmd::list::_render_detailed "$collected_rows"
cmd::list::_render_summary_from_rows "$collected_rows"
return 0
fi
local style
style=$(ui::peer::list_style)
case "$style" in
table) cmd::list::_render_table ;;
compact) cmd::list::_render_compact "$collected_rows" ;;
*) cmd::list::_render_compact "$collected_rows" ;;
esac
}
# ============================================
# Row collection (single pass, all filters)
# ============================================
function cmd::list::_collect_all_rows() {
# Outputs pipe-delimited rows for peers that pass all filters
# Fields: name|ip|type|rule|group|status|last_seen|is_blocked|is_restricted
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
# Identity filter
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]:-}"
# Apply status filters
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
# Apply rule/group filters
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
local state
state=$(peers::connection_state "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
local status="${state%%|*}"
# 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() {
declare -A rule_counts=() group_counts=()
_list_header_printed=false
cmd::list::_iter_confs_table
if [[ "$_list_header_printed" == "true" ]]; then
cmd::list::_render_footer $has_groups
local group_summary=""
cmd::list::_build_group_summary
printf "\n Showing peers\n\n"
else
log::wg_warning "No results found"
fi
}
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 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 < <(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=()
local total=0
while IFS='|' read -r name ip type rule rest; do
[[ -z "$name" ]] && continue
(( total++ )) || true
rule_counts["${rule:-—}"]=$(( ${rule_counts[${rule:-—}]:-0} + 1 )) || true
done <<< "$rows"
local summary=""
for r in "${!rule_counts[@]}"; do
summary+="${rule_counts[$r]} ${r}, "
done
summary="${summary%, }"
printf " Showing %s peers [%s]\n\n" "$total" "$summary"
}
# ============================================
# 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
}
| 1 | #!/usr/bin/env bash |
| 2 | |
| 3 | # ============================================ |
| 4 | # Lifecycle |
| 5 | # ============================================ |
| 6 | |
| 7 | function cmd::list::on_load() { |
| 8 | load_module identity |
| 9 | load_module ui |
| 10 | |
| 11 | flag::register --type |
| 12 | flag::register --rule |
| 13 | flag::register --group |
| 14 | flag::register --identity |
| 15 | flag::register --online |
| 16 | flag::register --offline |
| 17 | flag::register --restricted |
| 18 | flag::register --blocked |
| 19 | flag::register --allowed |
| 20 | flag::register --detailed |
| 21 | flag::register --name |
| 22 | } |
| 23 | |
| 24 | # ============================================ |
| 25 | # Help |
| 26 | # ============================================ |
| 27 | |
| 28 | function cmd::list::help() { |
| 29 | cat <<EOF |
| 30 | Usage: wgctl list [options] |
| 31 | |
| 32 | List all WireGuard clients. |
| 33 | |
| 34 | Options: |
| 35 | --type <type> Filter by device type |
| 36 | --rule <rule> Filter by assigned rule |
| 37 | --group <group> Filter by group membership |
| 38 | --identity <name> Filter by identity |
| 39 | --online Show only connected clients |
| 40 | --offline Show only disconnected clients |
| 41 | --blocked Show only fully blocked clients |
| 42 | --restricted Show only restricted clients |
| 43 | --allowed Show only unrestricted clients |
| 44 | --detailed Show detailed view grouped by identity |
| 45 | --name <name> Show detail card for a single client |
| 46 | |
| 47 | Examples: |
| 48 | wgctl list |
| 49 | wgctl list --type phone |
| 50 | wgctl list --identity nuno |
| 51 | wgctl list --online |
| 52 | wgctl list --blocked |
| 53 | wgctl list --detailed |
| 54 | wgctl list --name phone-nuno |
| 55 | EOF |
| 56 | } |
| 57 | |
| 58 | # ============================================ |
| 59 | # Detail Card |
| 60 | # ============================================ |
| 61 | |
| 62 | function cmd::list::show_client() { |
| 63 | local name="${1:-}" |
| 64 | local conf |
| 65 | conf="$(ctx::clients)/${name}.conf" |
| 66 | |
| 67 | if [[ ! -f "$conf" ]]; then |
| 68 | log::error "Client not found: ${name}" |
| 69 | return 1 |
| 70 | fi |
| 71 | |
| 72 | local ip |
| 73 | ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 74 | |
| 75 | local allowed_ips |
| 76 | allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs) |
| 77 | |
| 78 | local public_key |
| 79 | public_key=$(keys::public "$name" 2>/dev/null || echo "unknown") |
| 80 | |
| 81 | local type |
| 82 | type=$(peers::get_meta "$name" "type" 2>/dev/null) |
| 83 | [[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip") |
| 84 | |
| 85 | local endpoint="—" |
| 86 | local ep |
| 87 | ep=$(monitor::endpoint_for_key "$public_key") |
| 88 | [[ -z "$ep" ]] && ep=$(monitor::last_endpoint "$name") |
| 89 | [[ -n "$ep" ]] && endpoint="$ep" |
| 90 | |
| 91 | local is_blocked="false" |
| 92 | peers::is_blocked "$name" && is_blocked="true" |
| 93 | |
| 94 | local is_restricted="false" |
| 95 | peers::is_restricted "$name" && is_restricted="true" |
| 96 | |
| 97 | local handshake_ts |
| 98 | handshake_ts=$(monitor::get_handshake_ts "$public_key") |
| 99 | |
| 100 | local last_ts |
| 101 | last_ts=$(monitor::last_attempt "$name") |
| 102 | |
| 103 | local status |
| 104 | status=$(peers::format_status_verbose "$name" "$public_key" \ |
| 105 | "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") |
| 106 | |
| 107 | local last_seen |
| 108 | last_seen=$(peers::format_last_seen "$name" "$public_key" \ |
| 109 | "$is_blocked" "$last_ts" "" "$handshake_ts") |
| 110 | |
| 111 | local blocks="" |
| 112 | if block::has_file "$name" && block::is_blocked "$name"; then |
| 113 | if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then |
| 114 | blocks="all traffic blocked" |
| 115 | fi |
| 116 | local rule_lines |
| 117 | rule_lines=$(block::format_rules "$name") |
| 118 | [[ -n "$rule_lines" ]] && blocks+="$rule_lines" |
| 119 | fi |
| 120 | |
| 121 | ui::section "Client: ${name}" |
| 122 | ui::row "IP" "$ip" |
| 123 | ui::row "Type" "$(peers::display_type "$type")" |
| 124 | ui::row "Status" "$(echo -e "$status")" |
| 125 | ui::row "Endpoint" "$endpoint" |
| 126 | ui::row "Last seen" "$last_seen" |
| 127 | ui::row "AllowedIPs" "$allowed_ips" |
| 128 | ui::row "Public key" "$public_key" |
| 129 | |
| 130 | if [[ -z "$blocks" ]]; then |
| 131 | ui::row "Blocks" "none" |
| 132 | elif [[ "$blocks" == *"all traffic blocked"* ]]; then |
| 133 | ui::row "Blocks" "$(echo -e "\033[1;31mAll\033[0m")" |
| 134 | else |
| 135 | printf " %-20s\n" "Blocks:" |
| 136 | echo -e "$blocks" |
| 137 | fi |
| 138 | printf "\n" |
| 139 | } |
| 140 | |
| 141 | # ============================================ |
| 142 | # Run |
| 143 | # ============================================ |
| 144 | |
| 145 | function cmd::list::run() { |
| 146 | local filter_type="" filter_rule="" filter_group="" filter_identity="" |
| 147 | local online_only=false offline_only=false |
| 148 | local restricted_only=false blocked_only=false allowed_only=false |
| 149 | local detailed=false single_name="" |
| 150 | |
| 151 | while [[ $# -gt 0 ]]; do |
| 152 | case "$1" in |
| 153 | --type) filter_type="$2"; shift 2 ;; |
| 154 | --rule) filter_rule="$2"; shift 2 ;; |
| 155 | --group) filter_group="$2"; shift 2 ;; |
| 156 | --identity) filter_identity="$2"; shift 2 ;; |
| 157 | --online) online_only=true; shift ;; |
| 158 | --offline) offline_only=true; shift ;; |
| 159 | --restricted) restricted_only=true; shift ;; |
| 160 | --blocked) blocked_only=true; shift ;; |
| 161 | --allowed) allowed_only=true; shift ;; |
| 162 | --detailed) detailed=true; shift ;; |
| 163 | --name) single_name="$2"; shift 2 ;; |
| 164 | --help) cmd::list::help; return ;; |
| 165 | *) |
| 166 | log::error "Unknown flag: $1" |
| 167 | cmd::list::help |
| 168 | return 1 |
| 169 | ;; |
| 170 | esac |
| 171 | done |
| 172 | |
| 173 | if [[ -n "$single_name" ]]; then |
| 174 | cmd::list::show_client "$single_name" |
| 175 | return 0 |
| 176 | fi |
| 177 | |
| 178 | local dir |
| 179 | dir="$(ctx::clients)" |
| 180 | local confs=("${dir}"/*.conf) |
| 181 | if [[ ! -f "${confs[0]}" ]]; then |
| 182 | log::wg_warning "No clients configured" |
| 183 | return 0 |
| 184 | fi |
| 185 | |
| 186 | cmd::list::_precompute_all |
| 187 | |
| 188 | # Resolve identity filter |
| 189 | declare -gA p_identity_filter=() |
| 190 | if [[ -n "$filter_identity" ]]; then |
| 191 | identity::require_exists "$filter_identity" || return 1 |
| 192 | while IFS= read -r peer_name; do |
| 193 | [[ -n "$peer_name" ]] && p_identity_filter["$peer_name"]=1 |
| 194 | done < <(identity::peers "$filter_identity") |
| 195 | if [[ ${#p_identity_filter[@]} -eq 0 ]]; then |
| 196 | log::wg_warning "Identity '${filter_identity}' has no peers" |
| 197 | return 0 |
| 198 | fi |
| 199 | fi |
| 200 | |
| 201 | log::section "WireGuard Clients" |
| 202 | |
| 203 | # Collect all filtered rows first (needed for dynamic column widths) |
| 204 | local collected_rows="" |
| 205 | collected_rows=$(cmd::list::_collect_all_rows | ui::sort_rows) |
| 206 | |
| 207 | if [[ -z "$collected_rows" ]]; then |
| 208 | log::wg_warning "No results found" |
| 209 | return 0 |
| 210 | fi |
| 211 | |
| 212 | if $detailed; then |
| 213 | cmd::list::_render_detailed "$collected_rows" |
| 214 | cmd::list::_render_summary_from_rows "$collected_rows" |
| 215 | return 0 |
| 216 | fi |
| 217 | |
| 218 | local style |
| 219 | style=$(ui::peer::list_style) |
| 220 | |
| 221 | case "$style" in |
| 222 | table) cmd::list::_render_table ;; |
| 223 | compact) cmd::list::_render_compact "$collected_rows" ;; |
| 224 | *) cmd::list::_render_compact "$collected_rows" ;; |
| 225 | esac |
| 226 | } |
| 227 | |
| 228 | # ============================================ |
| 229 | # Row collection (single pass, all filters) |
| 230 | # ============================================ |
| 231 | |
| 232 | function cmd::list::_collect_all_rows() { |
| 233 | # Outputs pipe-delimited rows for peers that pass all filters |
| 234 | # Fields: name|ip|type|rule|group|status|last_seen|is_blocked|is_restricted |
| 235 | local dir |
| 236 | dir="$(ctx::clients)" |
| 237 | |
| 238 | for conf in "${dir}"/*.conf; do |
| 239 | [[ -f "$conf" ]] || continue |
| 240 | local client_name |
| 241 | client_name=$(basename "$conf" .conf) |
| 242 | [[ -z "$client_name" ]] && continue |
| 243 | |
| 244 | # Identity filter |
| 245 | if [[ ${#p_identity_filter[@]} -gt 0 && \ |
| 246 | -z "${p_identity_filter[$client_name]:-}" ]]; then |
| 247 | continue |
| 248 | fi |
| 249 | |
| 250 | local ip="${p_ips[$client_name]:-}" |
| 251 | [[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 252 | [[ -z "$ip" ]] && continue |
| 253 | |
| 254 | local type="${p_types[$client_name]:-unknown}" |
| 255 | [[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue |
| 256 | |
| 257 | local pubkey="${p_pubkeys[$client_name]:-}" |
| 258 | local handshake_ts="${wg_handshakes[$pubkey]:-0}" |
| 259 | local is_blocked="${p_blocked[$client_name]:-false}" |
| 260 | local is_restricted="${p_restricted[$client_name]:-false}" |
| 261 | local last_ts="${p_last_ts[$client_name]:-}" |
| 262 | local rule="${p_rules[$client_name]:-}" |
| 263 | local group="${p_main_groups[$client_name]:-}" |
| 264 | |
| 265 | # Apply status filters |
| 266 | if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || continue; fi |
| 267 | if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || continue; fi |
| 268 | if $restricted_only && [[ "$is_restricted" != "true" ]]; then continue; fi |
| 269 | if $blocked_only && [[ "$is_blocked" != "true" ]]; then continue; fi |
| 270 | if $allowed_only && { [[ "$is_blocked" == "true" ]] || \ |
| 271 | [[ "$is_restricted" == "true" ]]; }; then continue; fi |
| 272 | |
| 273 | # Apply rule/group filters |
| 274 | if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then continue; fi |
| 275 | if [[ -n "$filter_group" ]]; then |
| 276 | local all_groups="${peer_group_map[$client_name]:-}" |
| 277 | [[ "$all_groups" != *"$filter_group"* ]] && continue |
| 278 | fi |
| 279 | |
| 280 | # Resolve status |
| 281 | local state |
| 282 | state=$(peers::connection_state "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") |
| 283 | local status="${state%%|*}" |
| 284 | |
| 285 | # Resolve last seen |
| 286 | local last_seen="-" |
| 287 | if [[ "$is_blocked" == "true" && -n "$last_ts" && "$last_ts" != "0" ]]; then |
| 288 | local attempt_ts |
| 289 | attempt_ts=$(json::iso_to_ts "$last_ts") |
| 290 | last_seen="$(fmt::datetime_short "$attempt_ts") (dropped)" |
| 291 | elif [[ -n "$handshake_ts" && "$handshake_ts" != "0" ]]; then |
| 292 | local ts_display |
| 293 | ts_display=$(fmt::datetime_short "$handshake_ts") |
| 294 | if [[ "$status" == "online" ]]; then |
| 295 | last_seen="${ts_display} (handshake)" |
| 296 | else |
| 297 | last_seen="$ts_display" |
| 298 | fi |
| 299 | fi |
| 300 | |
| 301 | printf "%s|%s|%s|%s|%s|%s|%s|%s|%s\n" \ |
| 302 | "$client_name" "$ip" "$type" \ |
| 303 | "${rule:--}" "${group:--}" \ |
| 304 | "$status" "$last_seen" \ |
| 305 | "$is_blocked" "$is_restricted" |
| 306 | done |
| 307 | } |
| 308 | |
| 309 | # ============================================ |
| 310 | # Compact render |
| 311 | # ============================================ |
| 312 | |
| 313 | function cmd::list::_render_compact() { |
| 314 | local rows="${1:-}" |
| 315 | |
| 316 | # Measure column widths from pure values (fields 1-5, no labels) |
| 317 | local w_name w_ip w_type w_rule w_group |
| 318 | w_name=$(ui::measure_col "$rows" 1 14) |
| 319 | w_ip=$(ui::measure_col "$rows" 2 13) |
| 320 | w_type=$(ui::measure_col "$rows" 3 7) |
| 321 | w_rule=$(ui::measure_col "$rows" 4 4) |
| 322 | w_group=$(ui::measure_col "$rows" 5 4) |
| 323 | |
| 324 | echo "" |
| 325 | while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do |
| 326 | [[ -z "$name" ]] && continue |
| 327 | ui::peer::list_row_compact \ |
| 328 | "$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" \ |
| 329 | "$name" "$ip" "$type" "$rule" "$group" \ |
| 330 | "$status" "$last_seen" "$is_blocked" "$is_restricted" |
| 331 | done <<< "$rows" |
| 332 | echo "" |
| 333 | |
| 334 | cmd::list::_render_summary_from_rows "$rows" |
| 335 | } |
| 336 | |
| 337 | # ============================================ |
| 338 | # Table render (kept for config switching) |
| 339 | # ============================================ |
| 340 | |
| 341 | function cmd::list::_render_table() { |
| 342 | declare -A rule_counts=() group_counts=() |
| 343 | _list_header_printed=false |
| 344 | |
| 345 | cmd::list::_iter_confs_table |
| 346 | |
| 347 | if [[ "$_list_header_printed" == "true" ]]; then |
| 348 | cmd::list::_render_footer $has_groups |
| 349 | local group_summary="" |
| 350 | cmd::list::_build_group_summary |
| 351 | printf "\n Showing peers\n\n" |
| 352 | else |
| 353 | log::wg_warning "No results found" |
| 354 | fi |
| 355 | } |
| 356 | |
| 357 | function cmd::list::_iter_confs_table() { |
| 358 | local dir |
| 359 | dir="$(ctx::clients)" |
| 360 | for conf in "${dir}"/*.conf; do |
| 361 | [[ -f "$conf" ]] || continue |
| 362 | local client_name |
| 363 | client_name=$(basename "$conf" .conf) |
| 364 | [[ -z "$client_name" ]] && continue |
| 365 | |
| 366 | if [[ ${#p_identity_filter[@]} -gt 0 && \ |
| 367 | -z "${p_identity_filter[$client_name]:-}" ]]; then |
| 368 | continue |
| 369 | fi |
| 370 | |
| 371 | local ip="${p_ips[$client_name]:-}" |
| 372 | [[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 373 | |
| 374 | local type="${p_types[$client_name]:-unknown}" |
| 375 | [[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue |
| 376 | |
| 377 | cmd::list::_render_row "$client_name" "$ip" "$type" |
| 378 | done |
| 379 | } |
| 380 | |
| 381 | # ============================================ |
| 382 | # Detailed render (grouped by identity) |
| 383 | # ============================================ |
| 384 | |
| 385 | function cmd::list::_render_detailed() { |
| 386 | local rows="${1:-}" |
| 387 | |
| 388 | # Measure widths |
| 389 | local w_name w_ip w_type w_rule w_group w_subnet |
| 390 | w_name=$(ui::measure_col "$rows" 1 14) |
| 391 | w_ip=$(ui::measure_col "$rows" 2 13) |
| 392 | w_type=$(ui::measure_col "$rows" 3 7) |
| 393 | w_rule=$(ui::measure_col "$rows" 4 4) |
| 394 | w_group=$(ui::measure_col "$rows" 5 4) |
| 395 | # subnet not in rows — use fixed width |
| 396 | w_subnet=10 |
| 397 | |
| 398 | # Group by identity |
| 399 | declare -A identity_rows=() |
| 400 | local no_identity_rows="" |
| 401 | |
| 402 | while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do |
| 403 | [[ -z "$name" ]] && continue |
| 404 | local id_name |
| 405 | id_name=$(identity::get_name "$name") |
| 406 | local row="${name}|${ip}|${type}|${rule}|${group}|${status}|${last_seen}|${is_blocked}|${is_restricted}" |
| 407 | if [[ -n "$id_name" ]]; then |
| 408 | identity_rows["$id_name"]+="${row}"$'\n' |
| 409 | else |
| 410 | no_identity_rows+="${row}"$'\n' |
| 411 | fi |
| 412 | done <<< "$rows" |
| 413 | |
| 414 | echo "" |
| 415 | |
| 416 | # Render identity groups (sorted) |
| 417 | for id_name in $(echo "${!identity_rows[@]}" | tr ' ' '\n' | sort); do |
| 418 | ui::peer::list_identity_header "$id_name" |
| 419 | while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do |
| 420 | [[ -z "$name" ]] && continue |
| 421 | local subnet |
| 422 | subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null) |
| 423 | if [[ -z "$subnet" ]]; then |
| 424 | local peer_type="${p_types[$name]:-}" |
| 425 | [[ -n "$peer_type" ]] && subnet="$peer_type" |
| 426 | fi |
| 427 | [[ -z "$subnet" ]] && subnet="-" |
| 428 | ui::peer::list_row_detailed \ |
| 429 | "$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \ |
| 430 | "$name" "$ip" "$type" "$rule" "$group" "$subnet" \ |
| 431 | "$status" "$last_seen" "$is_blocked" "$is_restricted" |
| 432 | done < <(echo "${identity_rows[$id_name]}" | ui::sort_rows) |
| 433 | done |
| 434 | |
| 435 | # Render peers without identity (no "other" header if empty) |
| 436 | if [[ -n "$no_identity_rows" ]]; then |
| 437 | local trimmed |
| 438 | trimmed=$(echo "$no_identity_rows" | grep -v '^$') |
| 439 | if [[ -n "$trimmed" ]]; then |
| 440 | ui::peer::list_identity_header "other" |
| 441 | while IFS='|' read -r name ip type rule group status last_seen is_blocked is_restricted; do |
| 442 | [[ -z "$name" ]] && continue |
| 443 | local subnet |
| 444 | subnet=$(peers::get_meta "$name" "subnet" 2>/dev/null) |
| 445 | if [[ -z "$subnet" ]]; then |
| 446 | local peer_type="${p_types[$name]:-}" |
| 447 | [[ -n "$peer_type" ]] && subnet="$peer_type" |
| 448 | fi |
| 449 | [[ -z "$subnet" ]] && subnet="-" |
| 450 | ui::peer::list_row_detailed \ |
| 451 | "$w_name" "$w_ip" "$w_type" "$w_rule" "$w_group" "$w_subnet" \ |
| 452 | "$name" "$ip" "$type" "$rule" "$group" "$subnet" \ |
| 453 | "$status" "$last_seen" "$is_blocked" "$is_restricted" |
| 454 | done <<< "$trimmed" |
| 455 | fi |
| 456 | fi |
| 457 | |
| 458 | echo "" |
| 459 | } |
| 460 | |
| 461 | # ============================================ |
| 462 | # Summary |
| 463 | # ============================================ |
| 464 | |
| 465 | function cmd::list::_render_summary_from_rows() { |
| 466 | local rows="${1:-}" |
| 467 | declare -A rule_counts=() |
| 468 | local total=0 |
| 469 | |
| 470 | while IFS='|' read -r name ip type rule rest; do |
| 471 | [[ -z "$name" ]] && continue |
| 472 | (( total++ )) || true |
| 473 | rule_counts["${rule:-—}"]=$(( ${rule_counts[${rule:-—}]:-0} + 1 )) || true |
| 474 | done <<< "$rows" |
| 475 | |
| 476 | local summary="" |
| 477 | for r in "${!rule_counts[@]}"; do |
| 478 | summary+="${rule_counts[$r]} ${r}, " |
| 479 | done |
| 480 | summary="${summary%, }" |
| 481 | |
| 482 | printf " Showing %s peers [%s]\n\n" "$total" "$summary" |
| 483 | } |
| 484 | |
| 485 | # ============================================ |
| 486 | # Table row rendering |
| 487 | # ============================================ |
| 488 | |
| 489 | function cmd::list::_render_row() { |
| 490 | local client_name="$1" ip="$2" type="$3" |
| 491 | |
| 492 | local pubkey="${p_pubkeys[$client_name]:-}" |
| 493 | local handshake_ts="${wg_handshakes[$pubkey]:-0}" |
| 494 | local is_blocked="${p_blocked[$client_name]:-false}" |
| 495 | local is_restricted="${p_restricted[$client_name]:-false}" |
| 496 | local last_ts="${p_last_ts[$client_name]:-}" |
| 497 | |
| 498 | if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi |
| 499 | if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi |
| 500 | if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi |
| 501 | if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi |
| 502 | if $allowed_only && { [[ "$is_blocked" == "true" ]] || \ |
| 503 | [[ "$is_restricted" == "true" ]]; }; then return 0; fi |
| 504 | |
| 505 | if [[ -n "$filter_group" ]]; then |
| 506 | local all_groups="${peer_group_map[$client_name]:-}" |
| 507 | [[ "$all_groups" != *"$filter_group"* ]] && return 0 |
| 508 | fi |
| 509 | |
| 510 | local status last_seen display_type rule |
| 511 | status=$(peers::format_status_verbose "$client_name" "$pubkey" \ |
| 512 | "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") |
| 513 | last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \ |
| 514 | "$is_blocked" "$last_ts" "" "$handshake_ts") |
| 515 | display_type=$(peers::display_type "$type") |
| 516 | rule="${p_rules[$client_name]:-—}" |
| 517 | |
| 518 | if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi |
| 519 | |
| 520 | if [[ "${_list_header_printed:-false}" == "false" ]]; then |
| 521 | cmd::list::_render_header $has_groups |
| 522 | _list_header_printed=true |
| 523 | fi |
| 524 | |
| 525 | rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true |
| 526 | |
| 527 | local padded_status |
| 528 | padded_status=$(ui::pad_status "$status" 25) |
| 529 | |
| 530 | if $has_groups; then |
| 531 | local main_group="${p_main_groups[$client_name]:-}" |
| 532 | local group_display="${main_group:-${peer_group_map[$client_name]:-—}}" |
| 533 | printf " %-28s %-15s %-13s %-12s %-12s %s %s\n" \ |
| 534 | "$client_name" "$ip" "$display_type" "$rule" \ |
| 535 | "$group_display" "$padded_status" "$last_seen" |
| 536 | else |
| 537 | printf " %-28s %-15s %-13s %-12s %s %s\n" \ |
| 538 | "$client_name" "$ip" "$display_type" "$rule" \ |
| 539 | "$padded_status" "$last_seen" |
| 540 | fi |
| 541 | } |
| 542 | |
| 543 | # ============================================ |
| 544 | # Precompute |
| 545 | # ============================================ |
| 546 | |
| 547 | function cmd::list::_precompute_all() { |
| 548 | declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=() |
| 549 | while IFS="|" read -r name ip rule type last_ts last_evt main_group; do |
| 550 | [[ -z "$name" ]] && continue |
| 551 | p_ips["$name"]="$ip" |
| 552 | p_rules["$name"]="${rule:-}" |
| 553 | p_types["$name"]="${type:-}" |
| 554 | p_last_ts["$name"]="$last_ts" |
| 555 | p_last_evt["$name"]="$last_evt" |
| 556 | p_main_groups["$name"]="${main_group:-}" |
| 557 | done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)") |
| 558 | |
| 559 | for name in "${!p_ips[@]}"; do |
| 560 | [[ -n "${p_types[$name]:-}" ]] && continue |
| 561 | p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}") |
| 562 | done |
| 563 | |
| 564 | declare -gA wg_handshakes=() wg_endpoints=() |
| 565 | while IFS=$'\t' read -r pubkey ts; do |
| 566 | [[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts" |
| 567 | done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) |
| 568 | while IFS=$'\t' read -r pubkey endpoint; do |
| 569 | [[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint" |
| 570 | done < <(wg show "$(config::interface)" endpoints 2>/dev/null) |
| 571 | |
| 572 | declare -gA p_blocked=() p_restricted=() |
| 573 | cmd::list::_precompute_block_status p_blocked p_restricted |
| 574 | |
| 575 | declare -gA p_pubkeys=() |
| 576 | local dir |
| 577 | dir="$(ctx::clients)" |
| 578 | for kf in "${dir}"/*_public.key; do |
| 579 | [[ -f "$kf" ]] || continue |
| 580 | local kname |
| 581 | kname=$(basename "$kf" _public.key) |
| 582 | p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "") |
| 583 | done |
| 584 | |
| 585 | has_groups=false |
| 586 | declare -gA peer_group_map=() |
| 587 | local groups_dir |
| 588 | groups_dir="$(ctx::groups)" |
| 589 | local group_files=("${groups_dir}"/*.group) |
| 590 | if [[ -f "${group_files[0]}" ]]; then |
| 591 | has_groups=true |
| 592 | while IFS=":" read -r peer_name group_name; do |
| 593 | [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name" |
| 594 | done < <(json::peer_group_map "$groups_dir") |
| 595 | fi |
| 596 | |
| 597 | # Identity precompute (for --identity filter) |
| 598 | declare -gA p_identity_filter=() |
| 599 | } |
| 600 | |
| 601 | function cmd::list::_precompute_block_status() { |
| 602 | local -n _blocked="$1" |
| 603 | local -n _restricted="$2" |
| 604 | |
| 605 | local wg_peers |
| 606 | wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null) |
| 607 | |
| 608 | while IFS= read -r name; do |
| 609 | if block::has_specific_rules "$name" 2>/dev/null; then |
| 610 | _restricted["$name"]=true |
| 611 | else |
| 612 | _restricted["$name"]=false |
| 613 | fi |
| 614 | |
| 615 | local pubkey |
| 616 | pubkey=$(keys::public "$name" 2>/dev/null || echo "") |
| 617 | if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then |
| 618 | _blocked["$name"]=true |
| 619 | else |
| 620 | _blocked["$name"]=false |
| 621 | fi |
| 622 | done < <(peers::all) |
| 623 | } |
| 624 | |
| 625 | # ============================================ |
| 626 | # Header / Footer (table layout) |
| 627 | # ============================================ |
| 628 | |
| 629 | function cmd::list::_render_header() { |
| 630 | ui::peer::list_header_table "$1" |
| 631 | } |
| 632 | |
| 633 | function cmd::list::_render_footer() { |
| 634 | ui::peer::list_footer_table "$1" |
| 635 | } |
| 636 | |
| 637 | function cmd::list::_build_group_summary() { |
| 638 | group_summary="" |
| 639 | if $has_groups; then |
| 640 | declare -A _gs=() |
| 641 | for peer in "${!peer_group_map[@]}"; do |
| 642 | local g="${peer_group_map[$peer]}" |
| 643 | _gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true |
| 644 | done |
| 645 | for g in "${!_gs[@]}"; do |
| 646 | group_summary+="${_gs[$g]} in ${g}, " |
| 647 | done |
| 648 | group_summary="${group_summary%, }" |
| 649 | fi |
| 650 | } |
| 651 | |
| 652 | function cmd::list::_show_client_safe() { |
| 653 | local name="$1" |
| 654 | cmd::list::show_client "$name" || true |
| 655 | } |