Última actividad 1 month ago

Revisión 8a033c0009c27883e24efb4d2740c226aaa35ff8

gistfile1.txt Sin formato
1#!/usr/bin/env bash
2
3# ============================================
4# Lifecycle
5# ============================================
6
7function 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
28function cmd::list::help() {
29 cat <<EOF
30Usage: wgctl list [options]
31
32List all WireGuard clients.
33
34Options:
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
47Examples:
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
55EOF
56}
57
58# ============================================
59# Detail Card
60# ============================================
61
62function 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
145function 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
232function 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
313function 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
341function 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
357function 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
385function 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
465function 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
489function 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
547function 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
601function 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
629function cmd::list::_render_header() {
630 ui::peer::list_header_table "$1"
631}
632
633function cmd::list::_render_footer() {
634 ui::peer::list_footer_table "$1"
635}
636
637function 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
652function cmd::list::_show_client_safe() {
653 local name="$1"
654 cmd::list::show_client "$name" || true
655}