最後活躍 1 month ago

nuno 已修改 1 month ago. 還原成這個修訂版本

1 file changed, 836 insertions

gistfile1.txt(檔案已創建)

@@ -0,0 +1,836 @@
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 + }
17 +
18 + # ============================================
19 + # Help
20 + # ============================================
21 +
22 + function cmd::group::help() {
23 + cat <<EOF
24 + Usage: wgctl group <subcommand> [options]
25 +
26 + Manage peer groups. Operations like block/unblock act on all peers in a group.
27 + A peer can belong to multiple groups (M:N relationship).
28 + Group blocks track which groups blocked a peer — unblocking one group won't
29 + unblock a peer still blocked by another group.
30 +
31 + Subcommands:
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 +
47 + Options:
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 +
57 + Examples:
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
68 + EOF
69 + }
70 +
71 + # ============================================
72 + # Run
73 + # ============================================
74 +
75 + function 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 +
107 + function 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 +
157 + function 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 +
237 + function 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 +
265 + function 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 +
296 + function 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 +
332 + function 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 +
347 + function 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 +
382 + function 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 +
404 + function 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 +
436 + function 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 +
474 + function 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 +
487 + function 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 +
534 + function 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 +
575 + function 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 +
606 + function 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 +
662 + function 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 +
675 + function 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 +
700 + function 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 +
714 + function 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 +
768 + function 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 +
806 + function 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 + }
上一頁 下一頁