gistfile1.txt
· 15 KiB · Text
Sin formato
#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::list::on_load() {
flag::register --type
flag::register --rule
flag::register --group
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 (desktop, laptop, phone, tablet)
--rule <rule> Filter by assigned rule
--group <group> Filter by group membership
--online Show only connected clients
--offline Show only disconnected clients
--blocked Show only fully blocked clients (removed from WireGuard)
--restricted Show only restricted clients (specific IP/port blocks applied)
--allowed Show only unrestricted clients
--detailed Show full detail cards for all clients
--name <name> Show detail card for a single client
Status values:
online Connected (recent handshake)
offline Not connected
blocked Removed from WireGuard server (wgctl block --name)
restricted In WireGuard but with specific access rules (wgctl block --ip/--service)
Examples:
wgctl list
wgctl list --type phone
wgctl list --rule user
wgctl list --group family
wgctl list --online
wgctl list --blocked
wgctl list --restricted
wgctl list --detailed
wgctl list --name phone-nuno
EOF
}
# ============================================
# Header / Footer
# ============================================
function cmd::list::_render_header() {
local has_groups="$1"
if $has_groups; then
printf "\n %-28s %-15s %-13s %-12s %-12s %-22s %s\n" \
"NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN"
printf " %s\n" "$(printf '─%.0s' {1..135})"
else
printf "\n %-28s %-15s %-13s %-12s %-22s %s\n" \
"NAME" "IP" "TYPE" "RULE" "STATUS" "LAST SEEN"
printf " %s\n" "$(printf '─%.0s' {1..107})"
fi
}
function cmd::list::_render_footer() {
local has_groups="$1"
if $has_groups; then
printf " %s\n" "$(printf '─%.0s' {1..135})"
else
printf " %s\n" "$(printf '─%.0s' {1..107})"
fi
}
function cmd::list::_render_summary() {
local group_summary="${1:-}"
local -n _rule_counts="$2"
local filter_desc="${3:-}"
local total=0
for r in "${!_rule_counts[@]}"; do
(( total += _rule_counts[$r] )) || true
done
local summary=""
for r in "${!_rule_counts[@]}"; do
summary+="${_rule_counts[$r]} ${r}, "
done
summary="${summary%, }"
if [[ -n "$group_summary" ]]; then
printf "\n Showing %s peers [%s] — %s\n\n" "$total" "$summary" "$group_summary"
else
printf "\n Showing %s peers [%s]\n\n" "$total" "$summary"
fi
}
# ============================================
# 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")
# Meta type is authoritative; IP reverse lookup is fallback for pre-migration peers
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=""
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 ;;
--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
if $detailed; then
log::section "WireGuard Clients"
cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe
return 0
fi
local filter_desc=""
cmd::list::_build_filter_desc
declare -A rule_counts=() group_counts=()
_list_header_printed=false
cmd::list::_iter_confs "$filter_type" cmd::list::_render_row
if [[ "$_list_header_printed" == "true" ]]; then
cmd::list::_render_footer $has_groups
local group_summary=""
cmd::list::_build_group_summary
cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc"
else
log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}"
fi
}
# ============================================
# Iteration
# ============================================
function cmd::list::_iter_confs() {
local filter_type="$1" callback="$2"
local dir
dir="$(ctx::clients)"
for conf in "${dir}"/*.conf; do
[[ -f "$conf" ]] || continue
local client_name
client_name=$(basename "$conf" .conf)
local ip="${p_ips[$client_name]:-}"
[[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1)
# p_types is authoritative — set during precompute from meta + IP fallback
local type="${p_types[$client_name]:-unknown}"
[[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue
"$callback" "$client_name" "$ip" "$type"
done
}
# ============================================
# 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 group_display
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
log::section "WireGuard Clients"
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]:-}"
if [[ -n "$main_group" ]]; then
group_display="$main_group"
else
group_display="${peer_group_map[$client_name]:-—}"
fi
if [[ -n "${peer_group_map[$client_name]:-}" ]]; then
group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true
fi
local rule_col_width=12 group_col_width=12
[[ "$rule" == "—" ]] && rule_col_width=14
[[ "$group_display" == "—" ]] && group_col_width=14
printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \
"$client_name" "$ip" "$display_type" "$rule" \
"$group_display" "$padded_status" "$last_seen"
else
local rule_col_width=12
[[ "$rule" == "—" ]] && rule_col_width=14
printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \
"$client_name" "$ip" "$display_type" "$rule" \
"$padded_status" "$last_seen"
fi
}
# ============================================
# Precompute
# ============================================
function cmd::list::_precompute_all() {
# Peer data — field 4 is 'type' from peer_data_v2
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)")
# Fill type from IP for peers missing meta type (pre-migration peers)
for name in "${!p_ips[@]}"; do
[[ -n "${p_types[$name]:-}" ]] && continue
p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}")
done
# WireGuard handshakes + endpoints
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)
# Block/restricted status
declare -gA p_blocked=() p_restricted=()
cmd::list::_precompute_block_status p_blocked p_restricted
# Public keys
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
# Groups + main group
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
# Transfer/activity data — keyed by pubkey
declare -gA p_rx=() p_tx=() p_activity=()
while IFS="|" read -r pubkey rx tx level; do
[[ -z "$pubkey" ]] && continue
p_rx["$pubkey"]="$rx"
p_tx["$pubkey"]="$tx"
p_activity["$pubkey"]="$level"
done < <(json::peer_transfer "$(config::interface)")
}
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)
}
# ============================================
# Filter helpers
# ============================================
function cmd::list::_build_filter_desc() {
filter_desc=""
[[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} "
[[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} "
[[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} "
$online_only && filter_desc+="online "
$offline_only && filter_desc+="offline "
$blocked_only && filter_desc+="blocked "
filter_desc="${filter_desc% }"
}
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 | flag::register --type |
| 9 | flag::register --rule |
| 10 | flag::register --group |
| 11 | flag::register --online |
| 12 | flag::register --offline |
| 13 | flag::register --restricted |
| 14 | flag::register --blocked |
| 15 | flag::register --allowed |
| 16 | flag::register --detailed |
| 17 | flag::register --name |
| 18 | } |
| 19 | |
| 20 | # ============================================ |
| 21 | # Help |
| 22 | # ============================================ |
| 23 | |
| 24 | function cmd::list::help() { |
| 25 | cat <<EOF |
| 26 | Usage: wgctl list [options] |
| 27 | |
| 28 | List all WireGuard clients. |
| 29 | |
| 30 | Options: |
| 31 | --type <type> Filter by device type (desktop, laptop, phone, tablet) |
| 32 | --rule <rule> Filter by assigned rule |
| 33 | --group <group> Filter by group membership |
| 34 | --online Show only connected clients |
| 35 | --offline Show only disconnected clients |
| 36 | --blocked Show only fully blocked clients (removed from WireGuard) |
| 37 | --restricted Show only restricted clients (specific IP/port blocks applied) |
| 38 | --allowed Show only unrestricted clients |
| 39 | --detailed Show full detail cards for all clients |
| 40 | --name <name> Show detail card for a single client |
| 41 | |
| 42 | Status values: |
| 43 | online Connected (recent handshake) |
| 44 | offline Not connected |
| 45 | blocked Removed from WireGuard server (wgctl block --name) |
| 46 | restricted In WireGuard but with specific access rules (wgctl block --ip/--service) |
| 47 | |
| 48 | Examples: |
| 49 | wgctl list |
| 50 | wgctl list --type phone |
| 51 | wgctl list --rule user |
| 52 | wgctl list --group family |
| 53 | wgctl list --online |
| 54 | wgctl list --blocked |
| 55 | wgctl list --restricted |
| 56 | wgctl list --detailed |
| 57 | wgctl list --name phone-nuno |
| 58 | EOF |
| 59 | } |
| 60 | |
| 61 | # ============================================ |
| 62 | # Header / Footer |
| 63 | # ============================================ |
| 64 | |
| 65 | function cmd::list::_render_header() { |
| 66 | local has_groups="$1" |
| 67 | if $has_groups; then |
| 68 | printf "\n %-28s %-15s %-13s %-12s %-12s %-22s %s\n" \ |
| 69 | "NAME" "IP" "TYPE" "RULE" "GROUP" "STATUS" "LAST SEEN" |
| 70 | printf " %s\n" "$(printf '─%.0s' {1..135})" |
| 71 | else |
| 72 | printf "\n %-28s %-15s %-13s %-12s %-22s %s\n" \ |
| 73 | "NAME" "IP" "TYPE" "RULE" "STATUS" "LAST SEEN" |
| 74 | printf " %s\n" "$(printf '─%.0s' {1..107})" |
| 75 | fi |
| 76 | } |
| 77 | |
| 78 | function cmd::list::_render_footer() { |
| 79 | local has_groups="$1" |
| 80 | if $has_groups; then |
| 81 | printf " %s\n" "$(printf '─%.0s' {1..135})" |
| 82 | else |
| 83 | printf " %s\n" "$(printf '─%.0s' {1..107})" |
| 84 | fi |
| 85 | } |
| 86 | |
| 87 | function cmd::list::_render_summary() { |
| 88 | local group_summary="${1:-}" |
| 89 | local -n _rule_counts="$2" |
| 90 | local filter_desc="${3:-}" |
| 91 | |
| 92 | local total=0 |
| 93 | for r in "${!_rule_counts[@]}"; do |
| 94 | (( total += _rule_counts[$r] )) || true |
| 95 | done |
| 96 | |
| 97 | local summary="" |
| 98 | for r in "${!_rule_counts[@]}"; do |
| 99 | summary+="${_rule_counts[$r]} ${r}, " |
| 100 | done |
| 101 | summary="${summary%, }" |
| 102 | |
| 103 | if [[ -n "$group_summary" ]]; then |
| 104 | printf "\n Showing %s peers [%s] — %s\n\n" "$total" "$summary" "$group_summary" |
| 105 | else |
| 106 | printf "\n Showing %s peers [%s]\n\n" "$total" "$summary" |
| 107 | fi |
| 108 | } |
| 109 | |
| 110 | # ============================================ |
| 111 | # Detail Card |
| 112 | # ============================================ |
| 113 | |
| 114 | function cmd::list::show_client() { |
| 115 | local name="${1:-}" |
| 116 | local conf |
| 117 | conf="$(ctx::clients)/${name}.conf" |
| 118 | |
| 119 | if [[ ! -f "$conf" ]]; then |
| 120 | log::error "Client not found: ${name}" |
| 121 | return 1 |
| 122 | fi |
| 123 | |
| 124 | local ip |
| 125 | ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 126 | |
| 127 | local allowed_ips |
| 128 | allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs) |
| 129 | |
| 130 | local public_key |
| 131 | public_key=$(keys::public "$name" 2>/dev/null || echo "unknown") |
| 132 | |
| 133 | # Meta type is authoritative; IP reverse lookup is fallback for pre-migration peers |
| 134 | local type |
| 135 | type=$(peers::get_meta "$name" "type" 2>/dev/null) |
| 136 | [[ -z "$type" ]] && type=$(peers::get_type_from_ip "$ip") |
| 137 | |
| 138 | local endpoint="—" |
| 139 | local ep |
| 140 | ep=$(monitor::endpoint_for_key "$public_key") |
| 141 | [[ -z "$ep" ]] && ep=$(monitor::last_endpoint "$name") |
| 142 | [[ -n "$ep" ]] && endpoint="$ep" |
| 143 | |
| 144 | local is_blocked="false" |
| 145 | peers::is_blocked "$name" && is_blocked="true" |
| 146 | |
| 147 | local is_restricted="false" |
| 148 | peers::is_restricted "$name" && is_restricted="true" |
| 149 | |
| 150 | local handshake_ts |
| 151 | handshake_ts=$(monitor::get_handshake_ts "$public_key") |
| 152 | |
| 153 | local last_ts |
| 154 | last_ts=$(monitor::last_attempt "$name") |
| 155 | |
| 156 | local status |
| 157 | status=$(peers::format_status_verbose "$name" "$public_key" \ |
| 158 | "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") |
| 159 | |
| 160 | local last_seen |
| 161 | last_seen=$(peers::format_last_seen "$name" "$public_key" \ |
| 162 | "$is_blocked" "$last_ts" "" "$handshake_ts") |
| 163 | |
| 164 | local blocks="" |
| 165 | if block::has_file "$name" && block::is_blocked "$name"; then |
| 166 | if [[ "$(block::is_blocked_direct "$name")" == "true" ]]; then |
| 167 | blocks="all traffic blocked" |
| 168 | fi |
| 169 | local rule_lines |
| 170 | rule_lines=$(block::format_rules "$name") |
| 171 | [[ -n "$rule_lines" ]] && blocks+="$rule_lines" |
| 172 | fi |
| 173 | |
| 174 | ui::section "Client: ${name}" |
| 175 | ui::row "IP" "$ip" |
| 176 | ui::row "Type" "$(peers::display_type "$type")" |
| 177 | ui::row "Status" "$(echo -e "$status")" |
| 178 | ui::row "Endpoint" "$endpoint" |
| 179 | ui::row "Last seen" "$last_seen" |
| 180 | ui::row "AllowedIPs" "$allowed_ips" |
| 181 | ui::row "Public key" "$public_key" |
| 182 | |
| 183 | if [[ -z "$blocks" ]]; then |
| 184 | ui::row "Blocks" "none" |
| 185 | elif [[ "$blocks" == *"all traffic blocked"* ]]; then |
| 186 | ui::row "Blocks" "$(echo -e "\033[1;31mAll\033[0m")" |
| 187 | else |
| 188 | printf " %-20s\n" "Blocks:" |
| 189 | echo -e "$blocks" |
| 190 | fi |
| 191 | printf "\n" |
| 192 | } |
| 193 | |
| 194 | # ============================================ |
| 195 | # Run |
| 196 | # ============================================ |
| 197 | |
| 198 | function cmd::list::run() { |
| 199 | local filter_type="" filter_rule="" filter_group="" |
| 200 | local online_only=false offline_only=false |
| 201 | local restricted_only=false blocked_only=false allowed_only=false |
| 202 | local detailed=false single_name="" |
| 203 | |
| 204 | while [[ $# -gt 0 ]]; do |
| 205 | case "$1" in |
| 206 | --type) filter_type="$2"; shift 2 ;; |
| 207 | --rule) filter_rule="$2"; shift 2 ;; |
| 208 | --group) filter_group="$2"; shift 2 ;; |
| 209 | --online) online_only=true; shift ;; |
| 210 | --offline) offline_only=true; shift ;; |
| 211 | --restricted) restricted_only=true; shift ;; |
| 212 | --blocked) blocked_only=true; shift ;; |
| 213 | --allowed) allowed_only=true; shift ;; |
| 214 | --detailed) detailed=true; shift ;; |
| 215 | --name) single_name="$2"; shift 2 ;; |
| 216 | --help) cmd::list::help; return ;; |
| 217 | *) |
| 218 | log::error "Unknown flag: $1" |
| 219 | cmd::list::help |
| 220 | return 1 |
| 221 | ;; |
| 222 | esac |
| 223 | done |
| 224 | |
| 225 | if [[ -n "$single_name" ]]; then |
| 226 | cmd::list::show_client "$single_name" |
| 227 | return 0 |
| 228 | fi |
| 229 | |
| 230 | local dir |
| 231 | dir="$(ctx::clients)" |
| 232 | local confs=("${dir}"/*.conf) |
| 233 | if [[ ! -f "${confs[0]}" ]]; then |
| 234 | log::wg_warning "No clients configured" |
| 235 | return 0 |
| 236 | fi |
| 237 | |
| 238 | cmd::list::_precompute_all |
| 239 | |
| 240 | if $detailed; then |
| 241 | log::section "WireGuard Clients" |
| 242 | cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe |
| 243 | return 0 |
| 244 | fi |
| 245 | |
| 246 | local filter_desc="" |
| 247 | cmd::list::_build_filter_desc |
| 248 | |
| 249 | declare -A rule_counts=() group_counts=() |
| 250 | _list_header_printed=false |
| 251 | |
| 252 | cmd::list::_iter_confs "$filter_type" cmd::list::_render_row |
| 253 | |
| 254 | if [[ "$_list_header_printed" == "true" ]]; then |
| 255 | cmd::list::_render_footer $has_groups |
| 256 | local group_summary="" |
| 257 | cmd::list::_build_group_summary |
| 258 | cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc" |
| 259 | else |
| 260 | log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}" |
| 261 | fi |
| 262 | } |
| 263 | |
| 264 | # ============================================ |
| 265 | # Iteration |
| 266 | # ============================================ |
| 267 | |
| 268 | function cmd::list::_iter_confs() { |
| 269 | local filter_type="$1" callback="$2" |
| 270 | local dir |
| 271 | dir="$(ctx::clients)" |
| 272 | |
| 273 | for conf in "${dir}"/*.conf; do |
| 274 | [[ -f "$conf" ]] || continue |
| 275 | local client_name |
| 276 | client_name=$(basename "$conf" .conf) |
| 277 | local ip="${p_ips[$client_name]:-}" |
| 278 | [[ -z "$ip" ]] && ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) |
| 279 | |
| 280 | # p_types is authoritative — set during precompute from meta + IP fallback |
| 281 | local type="${p_types[$client_name]:-unknown}" |
| 282 | [[ -n "$filter_type" && "$type" != "$filter_type" ]] && continue |
| 283 | |
| 284 | "$callback" "$client_name" "$ip" "$type" |
| 285 | done |
| 286 | } |
| 287 | |
| 288 | # ============================================ |
| 289 | # Row rendering |
| 290 | # ============================================ |
| 291 | |
| 292 | function cmd::list::_render_row() { |
| 293 | local client_name="$1" ip="$2" type="$3" |
| 294 | |
| 295 | local pubkey="${p_pubkeys[$client_name]:-}" |
| 296 | local handshake_ts="${wg_handshakes[$pubkey]:-0}" |
| 297 | local is_blocked="${p_blocked[$client_name]:-false}" |
| 298 | local is_restricted="${p_restricted[$client_name]:-false}" |
| 299 | local last_ts="${p_last_ts[$client_name]:-}" |
| 300 | |
| 301 | if $online_only; then peers::is_online "$client_name" "$handshake_ts" "$last_ts" || return 0; fi |
| 302 | if $offline_only; then peers::is_offline "$client_name" "$handshake_ts" "$last_ts" || return 0; fi |
| 303 | if $restricted_only && [[ "$is_restricted" != "true" ]]; then return 0; fi |
| 304 | if $blocked_only && [[ "$is_blocked" != "true" ]]; then return 0; fi |
| 305 | if $allowed_only && { [[ "$is_blocked" == "true" ]] || \ |
| 306 | [[ "$is_restricted" == "true" ]]; }; then return 0; fi |
| 307 | |
| 308 | if [[ -n "$filter_group" ]]; then |
| 309 | local all_groups="${peer_group_map[$client_name]:-}" |
| 310 | [[ "$all_groups" != *"$filter_group"* ]] && return 0 |
| 311 | fi |
| 312 | |
| 313 | local status last_seen display_type rule group_display |
| 314 | status=$(peers::format_status_verbose "$client_name" "$pubkey" \ |
| 315 | "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") |
| 316 | last_seen=$(peers::format_last_seen "$client_name" "$pubkey" \ |
| 317 | "$is_blocked" "$last_ts" "" "$handshake_ts") |
| 318 | display_type=$(peers::display_type "$type") |
| 319 | rule="${p_rules[$client_name]:-—}" |
| 320 | |
| 321 | if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi |
| 322 | |
| 323 | if [[ "${_list_header_printed:-false}" == "false" ]]; then |
| 324 | log::section "WireGuard Clients" |
| 325 | cmd::list::_render_header $has_groups |
| 326 | _list_header_printed=true |
| 327 | fi |
| 328 | |
| 329 | rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true |
| 330 | |
| 331 | local padded_status |
| 332 | padded_status=$(ui::pad_status "$status" 25) |
| 333 | |
| 334 | if $has_groups; then |
| 335 | local main_group="${p_main_groups[$client_name]:-}" |
| 336 | if [[ -n "$main_group" ]]; then |
| 337 | group_display="$main_group" |
| 338 | else |
| 339 | group_display="${peer_group_map[$client_name]:-—}" |
| 340 | fi |
| 341 | |
| 342 | if [[ -n "${peer_group_map[$client_name]:-}" ]]; then |
| 343 | group_counts["$group_display"]=$(( ${group_counts[$group_display]:-0} + 1 )) || true |
| 344 | fi |
| 345 | |
| 346 | local rule_col_width=12 group_col_width=12 |
| 347 | [[ "$rule" == "—" ]] && rule_col_width=14 |
| 348 | [[ "$group_display" == "—" ]] && group_col_width=14 |
| 349 | printf " %-28s %-15s %-13s %-${rule_col_width}s %-${group_col_width}s %s %s\n" \ |
| 350 | "$client_name" "$ip" "$display_type" "$rule" \ |
| 351 | "$group_display" "$padded_status" "$last_seen" |
| 352 | else |
| 353 | local rule_col_width=12 |
| 354 | [[ "$rule" == "—" ]] && rule_col_width=14 |
| 355 | printf " %-28s %-15s %-13s %-${rule_col_width}s %s %s\n" \ |
| 356 | "$client_name" "$ip" "$display_type" "$rule" \ |
| 357 | "$padded_status" "$last_seen" |
| 358 | fi |
| 359 | } |
| 360 | |
| 361 | # ============================================ |
| 362 | # Precompute |
| 363 | # ============================================ |
| 364 | |
| 365 | function cmd::list::_precompute_all() { |
| 366 | # Peer data — field 4 is 'type' from peer_data_v2 |
| 367 | declare -gA p_ips=() p_rules=() p_types=() p_last_ts=() p_last_evt=() p_main_groups=() |
| 368 | while IFS="|" read -r name ip rule type last_ts last_evt main_group; do |
| 369 | [[ -z "$name" ]] && continue |
| 370 | p_ips["$name"]="$ip" |
| 371 | p_rules["$name"]="${rule:-—}" |
| 372 | p_types["$name"]="${type:-}" |
| 373 | p_last_ts["$name"]="$last_ts" |
| 374 | p_last_evt["$name"]="$last_evt" |
| 375 | p_main_groups["$name"]="${main_group:-}" |
| 376 | done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)") |
| 377 | |
| 378 | # Fill type from IP for peers missing meta type (pre-migration peers) |
| 379 | for name in "${!p_ips[@]}"; do |
| 380 | [[ -n "${p_types[$name]:-}" ]] && continue |
| 381 | p_types["$name"]=$(peers::get_type_from_ip "${p_ips[$name]}") |
| 382 | done |
| 383 | |
| 384 | # WireGuard handshakes + endpoints |
| 385 | declare -gA wg_handshakes=() wg_endpoints=() |
| 386 | while IFS=$'\t' read -r pubkey ts; do |
| 387 | [[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts" |
| 388 | done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) |
| 389 | while IFS=$'\t' read -r pubkey endpoint; do |
| 390 | [[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint" |
| 391 | done < <(wg show "$(config::interface)" endpoints 2>/dev/null) |
| 392 | |
| 393 | # Block/restricted status |
| 394 | declare -gA p_blocked=() p_restricted=() |
| 395 | cmd::list::_precompute_block_status p_blocked p_restricted |
| 396 | |
| 397 | # Public keys |
| 398 | declare -gA p_pubkeys=() |
| 399 | local dir |
| 400 | dir="$(ctx::clients)" |
| 401 | for kf in "${dir}"/*_public.key; do |
| 402 | [[ -f "$kf" ]] || continue |
| 403 | local kname |
| 404 | kname=$(basename "$kf" _public.key) |
| 405 | p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "") |
| 406 | done |
| 407 | |
| 408 | # Groups + main group |
| 409 | has_groups=false |
| 410 | declare -gA peer_group_map=() |
| 411 | local groups_dir |
| 412 | groups_dir="$(ctx::groups)" |
| 413 | local group_files=("${groups_dir}"/*.group) |
| 414 | if [[ -f "${group_files[0]}" ]]; then |
| 415 | has_groups=true |
| 416 | while IFS=":" read -r peer_name group_name; do |
| 417 | [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name" |
| 418 | done < <(json::peer_group_map "$groups_dir") |
| 419 | fi |
| 420 | |
| 421 | # Transfer/activity data — keyed by pubkey |
| 422 | declare -gA p_rx=() p_tx=() p_activity=() |
| 423 | while IFS="|" read -r pubkey rx tx level; do |
| 424 | [[ -z "$pubkey" ]] && continue |
| 425 | p_rx["$pubkey"]="$rx" |
| 426 | p_tx["$pubkey"]="$tx" |
| 427 | p_activity["$pubkey"]="$level" |
| 428 | done < <(json::peer_transfer "$(config::interface)") |
| 429 | } |
| 430 | |
| 431 | function cmd::list::_precompute_block_status() { |
| 432 | local -n _blocked="$1" |
| 433 | local -n _restricted="$2" |
| 434 | |
| 435 | local wg_peers |
| 436 | wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null) |
| 437 | |
| 438 | while IFS= read -r name; do |
| 439 | if block::has_specific_rules "$name" 2>/dev/null; then |
| 440 | _restricted["$name"]=true |
| 441 | else |
| 442 | _restricted["$name"]=false |
| 443 | fi |
| 444 | |
| 445 | local pubkey |
| 446 | pubkey=$(keys::public "$name" 2>/dev/null || echo "") |
| 447 | if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then |
| 448 | _blocked["$name"]=true |
| 449 | else |
| 450 | _blocked["$name"]=false |
| 451 | fi |
| 452 | done < <(peers::all) |
| 453 | } |
| 454 | |
| 455 | # ============================================ |
| 456 | # Filter helpers |
| 457 | # ============================================ |
| 458 | |
| 459 | function cmd::list::_build_filter_desc() { |
| 460 | filter_desc="" |
| 461 | [[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} " |
| 462 | [[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} " |
| 463 | [[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} " |
| 464 | $online_only && filter_desc+="online " |
| 465 | $offline_only && filter_desc+="offline " |
| 466 | $blocked_only && filter_desc+="blocked " |
| 467 | filter_desc="${filter_desc% }" |
| 468 | } |
| 469 | |
| 470 | function cmd::list::_build_group_summary() { |
| 471 | group_summary="" |
| 472 | if $has_groups; then |
| 473 | declare -A _gs=() |
| 474 | for peer in "${!peer_group_map[@]}"; do |
| 475 | local g="${peer_group_map[$peer]}" |
| 476 | _gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true |
| 477 | done |
| 478 | for g in "${!_gs[@]}"; do |
| 479 | group_summary+="${_gs[$g]} in ${g}, " |
| 480 | done |
| 481 | group_summary="${group_summary%, }" |
| 482 | fi |
| 483 | } |
| 484 | |
| 485 | function cmd::list::_show_client_safe() { |
| 486 | local name="$1" |
| 487 | cmd::list::show_client "$name" || true |
| 488 | } |