Última atividade 1 month ago

gistfile1.txt Bruto
1#!/usr/bin/env bash
2
3# ============================================
4# Lifecycle
5# ============================================
6
7function cmd::group::on_load() {
8 flag::register --name
9 flag::register --desc
10 flag::register --peer
11 flag::register --type
12 flag::register --rule
13 flag::register --new-name
14 flag::register --main
15 flag::register --force
16}
17
18# ============================================
19# Help
20# ============================================
21
22function cmd::group::help() {
23 cat <<EOF
24Usage: wgctl group <subcommand> [options]
25
26Manage peer groups. Operations like block/unblock act on all peers in a group.
27A peer can belong to multiple groups (M:N relationship).
28Group blocks track which groups blocked a peer — unblocking one group won't
29unblock a peer still blocked by another group.
30
31Subcommands:
32 list, ls List all groups
33 show Show group members and their status
34 add, new, create Create a new group
35 remove, rm, del Remove a group definition
36 rename Rename a group
37 peer add Add a peer to a group
38 peer remove, peer rm Remove a peer from a group
39 rm-peers Remove all peers in group from WireGuard
40 block Block all peers in group
41 unblock Unblock all peers in group
42 rule assign Assign a rule to all peers in group
43 audit Audit firewall rules for all peers in group
44 logs Show activity logs for all peers in group
45 watch Live monitor for all peers in group
46
47Options:
48 --name <name> Group name
49 --desc <description> Group description (for add)
50 --peer <peer> Peer name
51 --type <type> Peer device type (optional)
52 --rule <rule> Rule name (for rule assign)
53 --new-name <name> New group name (for rename)
54 --limit <n> Max log entries per peer (for logs)
55 --force Skip confirmation prompts
56
57Examples:
58 wgctl group list
59 wgctl group add --name family --desc "Family devices"
60 wgctl group peer add --name family --peer phone-nuno
61 wgctl group show --name family
62 wgctl group block --name family
63 wgctl group unblock --name family
64 wgctl group rule assign --name family --rule user
65 wgctl group audit --name family
66 wgctl group logs --name family --limit 20
67 wgctl group watch --name family
68EOF
69}
70
71# ============================================
72# Run
73# ============================================
74
75function cmd::group::run() {
76 local subcmd="${1:-help}"
77 shift || true
78
79 case "$subcmd" in
80 list|ls) cmd::group::list "$@" ;;
81 show) cmd::group::show "$@" ;;
82 add|new|create) cmd::group::add "$@" ;;
83 remove|rm|del|delete) cmd::group::remove "$@" ;;
84 rename) cmd::group::rename "$@" ;;
85 peer) cmd::group::peer "$@" ;;
86 rm-peers) cmd::group::rm_peers "$@" ;;
87 set-main) cmd::group::set_main "$@" ;;
88 block) cmd::group::block "$@" ;;
89 unblock) cmd::group::unblock "$@" ;;
90 rule) cmd::group::rule "$@" ;;
91 audit) cmd::group::audit "$@" ;;
92 logs) cmd::group::logs "$@" ;;
93 watch) cmd::group::watch "$@" ;;
94 help) cmd::group::help ;;
95 *)
96 log::error "Unknown subcommand: '${subcmd}'"
97 cmd::group::help
98 return 1
99 ;;
100 esac
101}
102
103# ============================================
104# List
105# ============================================
106
107function cmd::group::list() {
108 local groups_dir
109 groups_dir="$(ctx::groups)"
110
111 local groups=("${groups_dir}"/*.group)
112 if [[ ! -f "${groups[0]}" ]]; then
113 log::wg "No groups configured"
114 return 0
115 fi
116
117 log::section "Groups"
118 printf "\n %-20s %-35s %-8s %s\n" "NAME" "DESCRIPTION" "PEERS" "STATUS"
119 printf " %s\n" "$(printf '─%.0s' {1..75})"
120
121 while IFS="|" read -r name desc total blocked; do
122 [[ -z "$name" ]] && continue
123
124 local status_color="" status_str="active"
125 if [[ "$total" -gt 0 ]]; then
126 if [[ "$blocked" -eq "$total" ]]; then
127 status_color="\033[1;31m"
128 status_str="blocked"
129 elif [[ "$blocked" -gt 0 ]]; then
130 status_color="\033[1;33m"
131 status_str="blocked (${blocked}/${total})"
132 else
133 status_color="\033[1;32m"
134 status_str="active"
135 fi
136 fi
137
138 local short_desc="${desc:0:33}"
139 [[ ${#desc} -gt 33 ]] && short_desc="${short_desc}..."
140
141 local desc_col_width=35
142 [[ "$desc" == "—" || -z "$desc" ]] && desc_col_width=37
143
144 printf " %-20s %-${desc_col_width}s %-8s %b\n" \
145 "$name" "${short_desc:-—}" "$total" \
146 "${status_color}${status_str}\033[0m"
147
148 done < <(json::group_list_data "$groups_dir" "$(ctx::blocks)")
149
150 printf "\n"
151}
152
153# ============================================
154# Show
155# ============================================
156
157function cmd::group::show() {
158 local name=""
159
160 while [[ $# -gt 0 ]]; do
161 case "$1" in
162 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
163 --help) cmd::group::help; return ;;
164 *) log::error "Unknown flag: $1"; return 1 ;;
165 esac
166 done
167
168 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
169 group::require_exists "$name" || return 1
170
171 local group_file
172 group_file="$(group::path "$name")"
173
174 log::section "Group: ${name}"
175
176 local desc
177 desc=$(json::get "$group_file" "desc")
178 printf "\n %-20s %s\n" "Description:" "${desc:-—}"
179
180 # Load peers
181 local peers_list=()
182 mapfile -t peers_list < <(json::get "$group_file" "peers")
183 # Filter empty entries
184 local filtered=()
185 for p in "${peers_list[@]:-}"; do
186 [[ -n "$p" ]] && filtered+=("$p")
187 done
188 peers_list=("${filtered[@]:-}")
189 local peer_count=${#peers_list[@]}
190
191 [[ -z "${peers_list[0]}" ]] && peer_count=0
192
193 printf " %-20s %s\n" "Peers:" "$peer_count"
194 printf " %s\n" "$(printf '─%.0s' {1..50})"
195
196 if [[ "$peer_count" -gt 0 ]]; then
197 printf "\n %-28s %-15s %-12s %s\n" "NAME" "IP" "RULE" "STATUS"
198 printf " %s\n" "$(printf '─%.0s' {1..65})"
199 for peer_name in "${peers_list[@]}"; do
200 [[ -z "$peer_name" ]] && continue
201
202 # Skip if peer no longer exists
203 if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
204 log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
205 continue
206 fi
207
208 local ip rule status_str status_color
209 ip=$(peers::get_ip "$peer_name")
210 rule=$(peers::get_meta "$peer_name" "rule")
211 rule="${rule:-—}"
212
213 if peers::is_blocked "$peer_name" 2>/dev/null; then
214 status_color="\033[1;31m"
215 status_str="blocked"
216 else
217 status_color="\033[1;32m"
218 status_str="active"
219 fi
220
221 printf " %-28s %-15s %-12s %b\n" \
222 "$peer_name" "0" "$rule" \
223 "${status_str}\033[0m"
224 done
225 else
226 printf " —\n"
227 fi
228
229 printf "\n"
230 return 0
231}
232
233# ============================================
234# Add
235# ============================================
236
237function cmd::group::add() {
238 local name="" desc=""
239
240 while [[ $# -gt 0 ]]; do
241 case "$1" in
242 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
243 --desc) util::require_flag "--desc" "${2:-}" || return 1; desc="$2"; shift 2 ;;
244 --help) cmd::group::help; return ;;
245 *) log::error "Unknown flag: $1"; return 1 ;;
246 esac
247 done
248
249 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
250
251 if group::exists "$name"; then
252 log::error "Group already exists: ${name}"
253 return 1
254 fi
255
256 json::create_group "$(group::path "$name")" "$name" "$desc"
257
258 log::wg_success "Group created: ${name}"
259}
260
261# ============================================
262# Remove
263# ============================================
264
265function cmd::group::remove() {
266 local name="" force=false
267
268 while [[ $# -gt 0 ]]; do
269 case "$1" in
270 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
271 --force) force=true; shift ;;
272 --help) cmd::group::help; return ;;
273 *) log::error "Unknown flag: $1"; return 1 ;;
274 esac
275 done
276
277 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
278 group::require_exists "$name" || return 1
279
280 if ! $force; then
281 read -r -p "Remove group '${name}'? This only removes the group definition, not the peers. [y/N] " confirm
282 case "$confirm" in
283 [yY][eE][sS]|[yY]) ;;
284 *) log::info "Aborted"; return 0 ;;
285 esac
286 fi
287
288 rm -f "$(group::path "$name")"
289 log::wg_success "Group removed: ${name}"
290}
291
292# ============================================
293# Rename
294# ============================================
295
296function cmd::group::rename() {
297 local name="" new_name=""
298
299 while [[ $# -gt 0 ]]; do
300 case "$1" in
301 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
302 --new-name) util::require_flag "--new-name" "${2:-}" || return 1; new_name="$2"; shift 2 ;;
303 --help) cmd::group::help; return ;;
304 *) log::error "Unknown flag: $1"; return 1 ;;
305 esac
306 done
307
308 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
309 [[ -z "$new_name" ]] && log::error "Missing required flag: --new-name" && return 1
310 group::require_exists "$name" || return 1
311
312 if group::exists "$new_name"; then
313 log::error "Group already exists: ${new_name}"
314 return 1
315 fi
316
317 local old_file new_file
318 old_file="$(group::path "$name")"
319 new_file="$(group::path "$new_name")"
320
321 # Update name field in file
322 json::set "$old_file" "name" "\"$new_name\""
323 mv "$old_file" "$new_file"
324
325 log::wg_success "Group renamed: ${name} → ${new_name}"
326}
327
328# ============================================
329# Peer subcommand
330# ============================================
331
332function cmd::group::peer() {
333 local subcmd="${1:-help}"
334 shift || true
335
336 case "$subcmd" in
337 add) cmd::group::peer_add "$@" ;;
338 remove|rm|del) cmd::group::peer_remove "$@" ;;
339 *)
340 log::error "Unknown peer subcommand: '${subcmd}'"
341 cmd::group::help
342 return 1
343 ;;
344 esac
345}
346
347function cmd::group::peer_add() {
348 local name="" peer="" type="" set_main=false
349
350 while [[ $# -gt 0 ]]; do
351 case "$1" in
352 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
353 --peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;;
354 --type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;;
355 --main) util::require_flag "--main" "${2:-}" || return 1; set_main=true; shift ;;
356 --help) cmd::group::help; return ;;
357 *) log::error "Unknown flag: $1"; return 1 ;;
358 esac
359 done
360
361 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
362 [[ -z "$peer" ]] && log::error "Missing required flag: --peer" && return 1
363 group::require_exists "$name" || return 1
364
365 peer=$(peers::resolve_and_require "$peer" "$type") || return 1
366
367 # Check if already in group
368 if group::peers "$name" | grep -qF "$peer"; then
369 log::wg_warning "'${peer}' is already in group '${name}'"
370 return 0
371 fi
372
373 group::add_peer "$name" "$peer"
374 log::wg_success "Added '${peer}' to group '${name}'"
375
376 if $set_main; then
377 peers::set_main_group "$peer_name" "$group_name"
378 log::wg_success "Set '${group_name}' as main group for ${peer_name}"
379 fi
380}
381
382function cmd::group::peer_remove() {
383 local name="" peer="" type=""
384
385 while [[ $# -gt 0 ]]; do
386 case "$1" in
387 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
388 --peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;;
389 --type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;;
390 --help) cmd::group::help; return ;;
391 *) log::error "Unknown flag: $1"; return 1 ;;
392 esac
393 done
394
395 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
396 [[ -z "$peer" ]] && log::error "Missing required flag: --peer" && return 1
397 group::require_exists "$name" || return 1
398
399 peer=$(peers::resolve_and_require "$peer" "$type") || return 1
400 group::remove_peer "$name" "$peer"
401 log::wg_success "Removed '${peer}' from group '${name}'"
402}
403
404function cmd::group::set_main() {
405 local group_name="" peer_name="" type=""
406 while [[ $# -gt 0 ]]; do
407 case "$1" in
408 --name) group_name="$2"; shift 2 ;;
409 --peer) peer_name="$2"; shift 2 ;;
410 --type) type="$2"; shift 2 ;;
411 *) log::error "Unknown flag: $1"; return 1 ;;
412 esac
413 done
414
415 [[ -z "$group_name" ]] && log::error "Missing --name" && return 1
416 [[ -z "$peer_name" ]] && log::error "Missing --peer" && return 1
417
418 # Resolve peer name
419 peer_name=$(peers::resolve_and_require "$peer_name" "$type") || return 1
420
421 # Verify peer is in the group
422 if ! group::has_peer "$group_name" "$peer_name"; then
423 log::error "Peer '${peer_name}' is not in group '${group_name}'"
424 log::info "Add them first: wgctl group peer add --name ${group_name} --peer ${peer_name}"
425 return 1
426 fi
427
428 peers::set_main_group "$peer_name" "$group_name"
429 log::wg_success "Main group for '${peer_name}' set to '${group_name}'"
430}
431
432# ============================================
433# Remove peers from WireGuard
434# ============================================
435
436function cmd::group::rm_peers() {
437 local name="" force=false
438
439 while [[ $# -gt 0 ]]; do
440 case "$1" in
441 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
442 --force) force=true; shift ;;
443 --help) cmd::group::help; return ;;
444 *) log::error "Unknown flag: $1"; return 1 ;;
445 esac
446 done
447
448 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
449 group::require_exists "$name" || return 1
450
451 local peers_list=()
452 mapfile -t peers_list < <(group::peers "$name")
453 local peer_count=${#peers_list[@]}
454 [[ -z "${peers_list[0]:-}" ]] && peer_count=0
455
456 if [[ "$peer_count" -eq 0 ]]; then
457 log::wg_warning "Group '${name}' has no peers"
458 return 0
459 fi
460
461 if ! $force; then
462 read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm
463 case "$confirm" in
464 [yY][eE][sS]|[yY]) ;;
465 *) log::info "Aborted"; return 0 ;;
466 esac
467 fi
468
469 load_command remove
470 group::each_peer "$name" cmd::group::_rm_peer_cb
471 log::wg_success "Removed peers from group '${name}' (definition kept)"
472}
473
474function cmd::group::_rm_peer_cb() {
475 local peer_name="${1:-}"
476 if ! group::_peer_exists_check "$peer_name"; then
477 log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
478 return 0
479 fi
480 cmd::remove::run --name "$peer_name" --force
481}
482
483# ============================================
484# Block / Unblock
485# ============================================
486
487function cmd::group::block() {
488 local name="" force=false
489
490 while [[ $# -gt 0 ]]; do
491 case "$1" in
492 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
493 --force) force=true; shift ;;
494 --help) cmd::group::help; return ;;
495 *) log::error "Unknown flag: $1"; return 1 ;;
496 esac
497 done
498
499 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
500 group::require_exists "$name" || return 1
501
502 local peers_list=()
503 mapfile -t peers_list < <(group::peers "$name")
504
505 if [[ ${#peers_list[@]} -eq 0 ]] || [[ -z "${peers_list[0]:-}" ]]; then
506 log::wg_warning "Group '${name}' has no peers"
507 return 0
508 fi
509
510 local count=0 skipped=0 blocked_names=()
511 local filtered=()
512 for p in "${peers_list[@]:-}"; do
513 [[ -n "$p" ]] && filtered+=("$p")
514 done
515 [[ ${#filtered[@]} -eq 0 ]] && log::wg_warning "Group '${name}' has no peers" && return 0
516
517 for peer_name in "${filtered[@]}"; do
518 if cmd::group::_block_peer "$peer_name" "$name"; then
519 (( count++ )) || true
520 else
521 (( skipped++ )) || true
522 fi
523 done
524
525 if [[ "$count" -gt 0 ]]; then
526 log::wg_block "All peers from ${name} have been blocked (${count} peers)."
527 fi
528
529 if [[ "$skipped" -gt 0 ]]; then
530 log::wg_warning "${skipped} peers already blocked"
531 fi
532}
533
534function cmd::group::unblock() {
535 local name="" force=false
536
537 while [[ $# -gt 0 ]]; do
538 case "$1" in
539 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
540 --force) force=true; shift ;;
541 --help) cmd::group::help; return ;;
542 *) log::error "Unknown flag: $1"; return 1 ;;
543 esac
544 done
545
546 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
547 group::require_exists "$name" || return 1
548
549 local peers_list=()
550 mapfile -t peers_list < <(group::peers "$name")
551
552 local filtered=()
553 for p in "${peers_list[@]:-}"; do [[ -n "$p" ]] && filtered+=("$p"); done
554 [[ ${#filtered[@]} -eq 0 ]] && log::wg_warning "Group '${name}' has no peers" && return 0
555
556 local count=0 skipped=0
557
558 for peer_name in "${filtered[@]}"; do
559 if cmd::group::_unblock_peer "$peer_name" "$name"; then
560 (( count++ )) || true
561 else
562 (( skipped++ )) || true
563 fi
564 done
565
566 if [[ "$count" -gt 0 ]]; then
567 log::wg_unblock "All peers from ${name} have been unblocked."
568 fi
569
570 if [[ "$skipped" -gt 0 ]]; then
571 log::wg_warning "${skipped} peer(s) remain blocked (blocked directly or by other groups)"
572 fi
573}
574
575function cmd::group::_block_peer() {
576 local peer_name="${1:-}" group_name="${2:-}"
577 if ! group::_peer_exists_check "$peer_name"; then
578 log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
579 return 0
580 fi
581
582 local client_ip
583 client_ip=$(peers::get_ip "$peer_name")
584
585 # Check if already blocked by this group
586 local current_blocked_groups
587 current_blocked_groups=$(block::get_groups "$peer_name")
588
589 local IFS=','
590 for g in $current_blocked_groups; do
591 if [[ "$g" == "$group_name" ]]; then
592 log::wg_warning "${peer_name} — already blocked by group '${group_name}'"
593 return 1
594 fi
595 done
596
597 # Add group to block tracking
598 block::add_group "$peer_name" "$client_ip" "$group_name"
599
600 # Apply fw rules only if peer is still in WG server (not yet blocked)
601 if peers::exists_in_server "$peer_name"; then
602 block::apply_full "$peer_name" "$client_ip"
603 fi
604}
605
606function cmd::group::_unblock_peer() {
607 local peer_name="${1:-}" group_name="${2:-}"
608
609 if ! group::_peer_exists_check "$peer_name"; then
610 log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
611 return 1
612 fi
613
614 # Check if blocked by this group at all
615 if ! block::has_file "$peer_name"; then
616 log::wg_warning "${peer_name} — not blocked"
617 return 1
618 fi
619
620 local current_groups
621 current_groups=$(block::get_groups "$peer_name")
622 if [[ "$current_groups" != *"$group_name"* ]]; then
623 log::wg_warning "${peer_name} — not blocked by group '${group_name}'"
624 return 1
625 fi
626
627 local client_ip
628 client_ip=$(peers::get_ip "$peer_name")
629
630 block::remove_group "$peer_name" "$client_ip" "$group_name"
631
632 if block::is_blocked "$peer_name"; then
633 local groups
634 groups=$(block::get_groups "$peer_name")
635
636 local direct
637 direct=$(block::is_blocked_direct "$peer_name")
638
639 if [[ "$direct" == "true" ]]; then
640 log::wg_warning "${peer_name} — still blocked directly, skipping"
641 else
642 log::wg_warning "${peer_name} — still blocked by group(s): ${groups}, skipping"
643 fi
644
645 return 1
646 fi
647
648 block::restore_peer "$peer_name" "$client_ip"
649 block::remove_file "$peer_name"
650
651 local rule
652 rule=$(peers::get_meta "$peer_name" "rule")
653
654 [[ -n "$rule" ]] && rule::exists "$rule" && \
655 rule::apply "$rule" "$client_ip" "$peer_name"
656}
657
658# ============================================
659# Rule assign
660# ============================================
661
662function cmd::group::rule() {
663 local subcmd="${1:-help}"
664 shift || true
665 case "$subcmd" in
666 assign) cmd::group::rule_assign "$@" ;;
667 *)
668 log::error "Unknown rule subcommand: '${subcmd}'"
669 cmd::group::help
670 return 1
671 ;;
672 esac
673}
674
675function cmd::group::rule_assign() {
676 local name="" rule=""
677
678 while [[ $# -gt 0 ]]; do
679 case "$1" in
680 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
681 --rule) util::require_flag "--rule" "${2:-}" || return 1; rule="$2"; shift 2 ;;
682 --help) cmd::group::help; return ;;
683 *) log::error "Unknown flag: $1"; return 1 ;;
684 esac
685 done
686
687 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
688 [[ -z "$rule" ]] && log::error "Missing required flag: --rule" && return 1
689 group::require_exists "$name" || return 1
690 rule::require_exists "$rule" || return 1
691
692 local peers_list=()
693 mapfile -t peers_list < <(group::peers "$name")
694 [[ -z "${peers_list[0]:-}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
695
696 group::each_peer "$name" cmd::group::_rule_assign_cb "$rule"
697 log::wg_success "Assigned rule '${rule}' to group '${name}'"
698}
699
700function cmd::group::_rule_assign_cb() {
701 local peer_name="${1:-}" rule="${2:-}"
702 if ! group::_peer_exists_check "$peer_name"; then
703 log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
704 return 0
705 fi
706 load_command rule
707 cmd::rule::assign --name "$rule" --peer "$peer_name"
708}
709
710# ============================================
711# Audit
712# ============================================
713
714function cmd::group::audit() {
715 local name=""
716
717 while [[ $# -gt 0 ]]; do
718 case "$1" in
719 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
720 --help) cmd::group::help; return ;;
721 *) log::error "Unknown flag: $1"; return 1 ;;
722 esac
723 done
724
725 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
726 group::require_exists "$name" || return 1
727
728 local peers_list=()
729 mapfile -t peers_list < <(group::peers "$name")
730 [[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
731
732 # Run audit filtered to group peers
733 load_command audit
734 # Pass first peer as filter — audit handles the rest per-peer
735 # Actually run full audit and filter output
736 local peer_args=()
737 for peer_name in "${peers_list[@]}"; do
738 [[ -z "$peer_name" ]] && continue
739 peer_args+=("$peer_name")
740 done
741
742 test::reset
743 log::section "Audit: Group '${name}'"
744
745 # Precompute fw counts
746 declare -A peer_fw_counts
747 while IFS=":" read -r pname count; do
748 [[ -n "$pname" ]] && peer_fw_counts["$pname"]="$count"
749 done < <(json::audit_fw_counts "$(ctx::clients)")
750
751 test::section "Peer Rules"
752 for peer_name in "${peer_args[@]}"; do
753 # Skip if peer no longer exists
754 if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
755 log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
756 continue
757 fi
758 cmd::audit::check_peer "$peer_name" false "${peer_fw_counts[$peer_name]:-0}"
759 done
760
761 test::summary
762}
763
764# ============================================
765# Logs
766# ============================================
767
768function cmd::group::logs() {
769 local name="" limit=50
770
771 while [[ $# -gt 0 ]]; do
772 case "$1" in
773 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
774 --limit) util::require_flag "--limit" "${2:-}" || return 1; limit="$2"; shift 2 ;;
775 --help) cmd::group::help; return ;;
776 *) log::error "Unknown flag: $1"; return 1 ;;
777 esac
778 done
779
780 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
781 group::require_exists "$name" || return 1
782
783 local peers_list=()
784 mapfile -t peers_list < <(group::peers "$name")
785 [[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
786
787 log::section "Logs: Group '${name}'"
788
789 for peer_name in "${peers_list[@]}"; do
790 [[ -z "$peer_name" ]] && continue
791 # Skip if peer no longer exists
792 if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
793 log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
794 continue
795 fi
796 printf "\n \033[1;37m── %s ──\033[0m\n" "$peer_name"
797 load_command logs
798 cmd::logs::show --name "$peer_name" --limit "$limit"
799 done
800}
801
802# ============================================
803# Watch
804# ============================================
805
806function cmd::group::watch() {
807 local name=""
808
809 while [[ $# -gt 0 ]]; do
810 case "$1" in
811 --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
812 --help) cmd::group::help; return ;;
813 *) log::error "Unknown flag: $1"; return 1 ;;
814 esac
815 done
816
817 [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
818 group::require_exists "$name" || return 1
819
820 local peers_list=()
821 mapfile -t peers_list < <(group::peers "$name")
822 [[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
823
824 local peer_filter
825 peer_filter=$(IFS=','; echo "${peers_list[*]}")
826
827 # Build comma-separated peer list for watch filter
828 # Watch already supports --type filter but not multiple peers
829 # For now, use follow mode filtered to group peers one at a time
830 # or just run watch with no filter (shows all, user sees group context)
831 log::section "Live Monitor: Group '${name}'"
832 printf " Monitoring: %s\n\n" "${peers_list[*]}"
833
834 load_command watch
835 cmd::watch::run --peers "$peer_filter"
836}
837