Dernière activité 1 month ago

nuno a révisé ce gist 1 month ago. Aller à la révision

1 file changed, 952 insertions

gistfile1.txt(fichier créé)

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