wgctl/commands/group.command.sh

925 lines
27 KiB
Bash

#!/usr/bin/env bash
# ============================================
# Lifecycle
# ============================================
function cmd::group::on_load() {
flag::register --name
flag::register --desc
flag::register --peer
flag::register --type
flag::register --rule
flag::register --new-name
flag::register --force
}
# ============================================
# Help
# ============================================
function cmd::group::help() {
cat <<EOF
Usage: wgctl group <subcommand> [options]
Manage peer groups. Groups are organizational — a peer can belong
to multiple groups. Operations like block/unblock act on all peers in a group.
Subcommands:
list, ls List all groups with status
show Show group members and their status
add, new, create Create a new group
remove, rm, del Remove a group definition (not the peers)
rename Rename a group
peer add Add a peer to a group
peer remove, peer rm Remove a peer from a group
rm-peers Remove all peers in group from WireGuard
block Block all peers in group
unblock Unblock all peers in group
rule assign Assign a rule to all peers in group
audit Audit firewall rules for all peers in group
logs Show activity logs for all peers in group
watch Live monitor for all peers in group
Options:
--name <name> Group name
--desc <description> Group description (for add)
--peer <peer> Peer name
--type <type> Peer device type (optional)
--rule <rule> Rule name (for rule assign)
--new-name <name> New group name (for rename)
--limit <n> Max log entries per peer (for logs)
--force Skip confirmation prompts
Examples:
wgctl group list
wgctl group add --name family --desc "Family devices"
wgctl group peer add --name family --peer phone-nuno
wgctl group peer remove --name family --peer phone-nuno
wgctl group show --name family
wgctl group block --name family
wgctl group unblock --name family
wgctl group rule assign --name family --rule user
wgctl group audit --name family
wgctl group logs --name family --limit 20
wgctl group watch --name family
EOF
}
# ============================================
# Run
# ============================================
function cmd::group::run() {
local subcmd="${1:-help}"
shift || true
case "$subcmd" in
list|ls) cmd::group::list "$@" ;;
show) cmd::group::show "$@" ;;
add|new|create) cmd::group::add "$@" ;;
remove|rm|del|delete) cmd::group::remove "$@" ;;
rename) cmd::group::rename "$@" ;;
peer) cmd::group::peer "$@" ;;
rm-peers) cmd::group::rm_peers "$@" ;;
block) cmd::group::block "$@" ;;
unblock) cmd::group::unblock "$@" ;;
rule) cmd::group::rule "$@" ;;
audit) cmd::group::audit "$@" ;;
logs) cmd::group::logs "$@" ;;
watch) cmd::group::watch "$@" ;;
help) cmd::group::help ;;
*)
log::error "Unknown subcommand: '${subcmd}'"
cmd::group::help
return 1
;;
esac
}
# ============================================
# List
# ============================================
function cmd::group::list() {
local groups_dir
groups_dir="$(ctx::groups)"
local groups=("${groups_dir}"/*.group)
if [[ ! -f "${groups[0]}" ]]; then
log::wg "No groups configured"
return 0
fi
log::section "Groups"
printf "\n %-20s %-35s %-8s %s\n" "NAME" "DESCRIPTION" "PEERS" "STATUS"
printf " %s\n" "$(printf '─%.0s' {1..75})"
while IFS="|" read -r name desc total blocked; do
[[ -z "$name" ]] && continue
local status_color="" status_str="active"
if [[ "$total" -gt 0 ]]; then
if [[ "$blocked" -eq "$total" ]]; then
status_color="\033[1;31m"
status_str="blocked"
elif [[ "$blocked" -gt 0 ]]; then
status_color="\033[1;33m"
status_str="blocked (${blocked}/${total})"
else
status_color="\033[1;32m"
status_str="active"
fi
fi
local short_desc="${desc:0:33}"
[[ ${#desc} -gt 33 ]] && short_desc="${short_desc}..."
local desc_col_width=35
[[ "$desc" == "—" || -z "$desc" ]] && desc_col_width=37
printf " %-20s %-${desc_col_width}s %-8s %b\n" \
"$name" "${short_desc:-}" "$total" \
"${status_color}${status_str}\033[0m"
done < <(json::group_list_data "$groups_dir" "$(ctx::blocks)")
printf "\n"
}
# function cmd::group::list() {
# local groups_dir
# groups_dir="$(ctx::groups)"
# local groups=("${groups_dir}"/*.group)
# if [[ ! -f "${groups[0]}" ]]; then
# log::wg "No groups configured"
# return 0
# fi
# log::section "Groups"
# printf "\n %-20s %-35s %-8s %s\n" "NAME" "DESCRIPTION" "PEERS" "STATUS"
# printf " %s\n" "$(printf '─%.0s' {1..75})"
# for group_file in "${groups_dir}"/*.group; do
# [[ -f "$group_file" ]] || continue
# local name desc
# name=$(json::get "$group_file" "name")
# desc=$(json::get "$group_file" "desc")
# # Count peers
# local peers_list=()
# mapfile -t peers_list < <(json::get "$group_file" "peers")
# # Filter empty entries
# local filtered=()
# for p in "${peers_list[@]:-}"; do
# [[ -n "$p" ]] && filtered+=("$p")
# done
# peers_list=("${filtered[@]:-}")
# local peer_count=${#peers_list[@]}
# [[ -z "${peers_list[0]}" ]] && peer_count=0
# # Check block status
# local blocked=0 total=0
# for peer_name in "${peers_list[@]}"; do
# [[ -z "$peer_name" ]] && continue
# (( total++ )) || true
# peers::is_blocked "$peer_name" && (( blocked++ )) || true
# done
# local status_color=""
# local status_str="active"
# if [[ "$total" -gt 0 ]]; then
# if [[ "$blocked" -eq "$total" ]]; then
# status_color="\033[1;31m"
# status_str="blocked"
# elif [[ "$blocked" -gt 0 ]]; then
# status_color="\033[1;33m"
# status_str="blocked (${blocked}/${total})"
# # status_color="\033[1;33m"
# # status_str="${blocked}/${total} blocked"
# else
# status_color="\033[1;32m"
# status_str="active"
# fi
# fi
# local short_desc="${desc:0:33}"
# [[ ${#desc} -gt 33 ]] && short_desc="${short_desc}..."
# printf " %-20s %-35s %-8s %b\n" \
# "$name" \
# "${short_desc:-—}" \
# "$peer_count" \
# "${status_color}${status_str}\033[0m"
# done
# printf "\n"
# }
# ============================================
# Show
# ============================================
function cmd::group::show() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
local group_file
group_file="$(group::path "$name")"
log::section "Group: ${name}"
local desc
desc=$(json::get "$group_file" "desc")
printf "\n %-20s %s\n" "Description:" "${desc:-}"
# Load peers
local peers_list=()
mapfile -t peers_list < <(json::get "$group_file" "peers")
# Filter empty entries
local filtered=()
for p in "${peers_list[@]:-}"; do
[[ -n "$p" ]] && filtered+=("$p")
done
peers_list=("${filtered[@]:-}")
local peer_count=${#peers_list[@]}
[[ -z "${peers_list[0]}" ]] && peer_count=0
printf " %-20s %s\n" "Peers:" "$peer_count"
printf " %s\n" "$(printf '─%.0s' {1..50})"
if [[ "$peer_count" -gt 0 ]]; then
printf "\n %-28s %-15s %-12s %s\n" "NAME" "IP" "RULE" "STATUS"
printf " %s\n" "$(printf '─%.0s' {1..65})"
for peer_name in "${peers_list[@]}"; do
[[ -z "$peer_name" ]] && continue
# Skip if peer no longer exists
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
continue
fi
local ip rule status_str status_color
ip=$(peers::get_ip "$peer_name")
rule=$(peers::get_meta "$peer_name" "rule")
rule="${rule:-}"
if peers::is_blocked "$peer_name" 2>/dev/null; then
status_color="\033[1;31m"
status_str="blocked"
else
status_color="\033[1;32m"
status_str="active"
fi
printf " %-28s %-15s %-12s %b\n" \
"$peer_name" "0" "$rule" \
"${status_str}\033[0m"
done
else
printf " —\n"
fi
printf "\n"
return 0
}
# ============================================
# Add
# ============================================
function cmd::group::add() {
local name="" desc=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--desc) util::require_flag "--desc" "${2:-}" || return 1; desc="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
if group::exists "$name"; then
log::error "Group already exists: ${name}"
return 1
fi
json::create_group "$(group::path "$name")" "$name" "$desc"
log::wg_success "Group created: ${name}"
}
# ============================================
# Remove
# ============================================
function cmd::group::remove() {
local name="" force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--force) force=true; shift ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
if ! $force; then
read -r -p "Remove group '${name}'? This only removes the group definition, not the peers. [y/N] " confirm
case "$confirm" in
[yY][eE][sS]|[yY]) ;;
*) log::info "Aborted"; return 0 ;;
esac
fi
rm -f "$(group::path "$name")"
log::wg_success "Group removed: ${name}"
}
# ============================================
# Rename
# ============================================
function cmd::group::rename() {
local name="" new_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--new-name) util::require_flag "--new-name" "${2:-}" || return 1; new_name="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
[[ -z "$new_name" ]] && log::error "Missing required flag: --new-name" && return 1
group::require_exists "$name" || return 1
if group::exists "$new_name"; then
log::error "Group already exists: ${new_name}"
return 1
fi
local old_file new_file
old_file="$(group::path "$name")"
new_file="$(group::path "$new_name")"
# Update name field in file
json::set "$old_file" "name" "\"$new_name\""
mv "$old_file" "$new_file"
log::wg_success "Group renamed: ${name}${new_name}"
}
# ============================================
# Peer subcommand
# ============================================
function cmd::group::peer() {
local subcmd="${1:-help}"
shift || true
case "$subcmd" in
add) cmd::group::peer_add "$@" ;;
remove|rm|del) cmd::group::peer_remove "$@" ;;
*)
log::error "Unknown peer subcommand: '${subcmd}'"
cmd::group::help
return 1
;;
esac
}
function cmd::group::peer_add() {
local name="" peer="" type=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;;
--type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
[[ -z "$peer" ]] && log::error "Missing required flag: --peer" && return 1
group::require_exists "$name" || return 1
peer=$(peers::resolve_and_require "$peer" "$type") || return 1
# Check if already in group
if group::peers "$name" | grep -qF "$peer"; then
log::wg_warning "'${peer}' is already in group '${name}'"
return 0
fi
group::add_peer "$name" "$peer"
log::wg_success "Added '${peer}' to group '${name}'"
}
function cmd::group::peer_remove() {
local name="" peer="" type=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--peer) util::require_flag "--peer" "${2:-}" || return 1; peer="$2"; shift 2 ;;
--type) util::require_flag "--type" "${2:-}" || return 1; type="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
[[ -z "$peer" ]] && log::error "Missing required flag: --peer" && return 1
group::require_exists "$name" || return 1
peer=$(peers::resolve_and_require "$peer" "$type") || return 1
group::remove_peer "$name" "$peer"
log::wg_success "Removed '${peer}' from group '${name}'"
}
# ============================================
# Remove peers from WireGuard
# ============================================
function cmd::group::rm_peers() {
local name="" force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--force) force=true; shift ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
local peer_count=${#peers_list[@]}
[[ -z "${peers_list[0]:-}" ]] && peer_count=0
if [[ "$peer_count" -eq 0 ]]; then
log::wg_warning "Group '${name}' has no peers"
return 0
fi
if ! $force; then
read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm
case "$confirm" in
[yY][eE][sS]|[yY]) ;;
*) log::info "Aborted"; return 0 ;;
esac
fi
load_command remove
group::each_peer "$name" cmd::group::_rm_peer_cb
log::wg_success "Removed peers from group '${name}' (definition kept)"
}
function cmd::group::_rm_peer_cb() {
local peer_name="${1:-}"
if ! group::_peer_exists_check "$peer_name"; then
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
return 0
fi
cmd::remove::run --name "$peer_name" --force
}
# function cmd::group::rm_peers() {
# local name="" force=false
# while [[ $# -gt 0 ]]; do
# case "$1" in
# --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
# --force) force=true; shift ;;
# --help) cmd::group::help; return ;;
# *) log::error "Unknown flag: $1"; return 1 ;;
# esac
# done
# [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
# group::require_exists "$name" || return 1
# local peers_list=()
# mapfile -t peers_list < <(group::peers "$name")
# local peer_count=${#peers_list[@]}
# [[ -z "${peers_list[0]}" ]] && peer_count=0
# if [[ "$peer_count" -eq 0 ]]; then
# log::wg_warning "Group '${name}' has no peers"
# return 0
# fi
# if ! $force; then
# read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm
# case "$confirm" in
# [yY][eE][sS]|[yY]) ;;
# *) log::info "Aborted"; return 0 ;;
# esac
# fi
# local count=0
# for peer_name in "${peers_list[@]}"; do
# [[ -z "$peer_name" ]] && continue
# # Skip if peer no longer exists
# if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
# log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
# continue
# fi
# cmd::remove::run --name "$peer_name" --force
# (( count++ )) || true
# done
# log::wg_success "Removed ${count} peers from WireGuard (group '${name}' definition kept)"
# }
# ============================================
# Block / Unblock
# ============================================
function cmd::group::block() {
local name="" force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--force) force=true; shift ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
if [[ ${#peers_list[@]} -eq 0 ]] || [[ -z "${peers_list[0]:-}" ]]; then
log::wg_warning "Group '${name}' has no peers"
return 0
fi
local count=0 skipped=0 blocked_names=()
local filtered=()
for p in "${peers_list[@]:-}"; do
[[ -n "$p" ]] && filtered+=("$p")
done
[[ ${#filtered[@]} -eq 0 ]] && log::wg_warning "Group '${name}' has no peers" && return 0
for peer_name in "${filtered[@]}"; do
if cmd::group::_block_peer "$peer_name" "$name"; then
(( count++ )) || true
else
(( skipped++ )) || true
fi
done
if [[ "$count" -gt 0 ]]; then
log::wg_block "All peers from ${name} have been blocked (${count} peers)."
fi
if [[ "$skipped" -gt 0 ]]; then
log::wg_warning "${skipped} peers already blocked"
fi
}
function cmd::group::unblock() {
local name="" force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--force) force=true; shift ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
local filtered=()
for p in "${peers_list[@]:-}"; do [[ -n "$p" ]] && filtered+=("$p"); done
[[ ${#filtered[@]} -eq 0 ]] && log::wg_warning "Group '${name}' has no peers" && return 0
local count=0 skipped=0
for peer_name in "${filtered[@]}"; do
if cmd::group::_unblock_peer "$peer_name" "$name"; then
(( count++ )) || true
else
(( skipped++ )) || true
fi
done
if [[ "$count" -gt 0 ]]; then
log::wg_unblock "All peers from ${name} have been unblocked."
fi
if [[ "$skipped" -gt 0 ]]; then
log::wg_warning "${skipped} peer(s) remain blocked (blocked directly or by other groups)"
fi
}
function cmd::group::_block_peer() {
local peer_name="${1:-}" group_name="${2:-}"
if ! group::_peer_exists_check "$peer_name"; then
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
return 0
fi
local client_ip
client_ip=$(peers::get_ip "$peer_name")
# Check if already blocked by this group
local current_blocked_groups
current_blocked_groups=$(block::get_groups "$peer_name")
local IFS=','
for g in $current_blocked_groups; do
if [[ "$g" == "$group_name" ]]; then
log::wg_warning "${peer_name} — already blocked by group '${group_name}'"
return 1
fi
done
# Add group to block tracking
block::add_group "$peer_name" "$client_ip" "$group_name"
# Apply fw rules only if peer is still in WG server (not yet blocked)
if peers::exists_in_server "$peer_name"; then
block::apply_full "$peer_name" "$client_ip"
fi
}
function cmd::group::_unblock_peer() {
local peer_name="${1:-}" group_name="${2:-}"
if ! group::_peer_exists_check "$peer_name"; then
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
return 1
fi
# Check if blocked by this group at all
if ! block::has_file "$peer_name"; then
log::wg_warning "${peer_name} — not blocked"
return 1
fi
local current_groups
current_groups=$(block::get_groups "$peer_name")
if [[ "$current_groups" != *"$group_name"* ]]; then
log::wg_warning "${peer_name} — not blocked by group '${group_name}'"
return 1
fi
local client_ip
client_ip=$(peers::get_ip "$peer_name")
block::remove_group "$peer_name" "$client_ip" "$group_name"
if block::is_blocked "$peer_name"; then
local groups
groups=$(block::get_groups "$peer_name")
local direct
direct=$(block::is_blocked_direct "$peer_name")
if [[ "$direct" == "true" ]]; then
log::wg_warning "${peer_name} — still blocked directly, skipping"
else
log::wg_warning "${peer_name} — still blocked by group(s): ${groups}, skipping"
fi
return 1
fi
log::debug "_unblock_peer: restoring $peer_name"
block::restore_peer "$peer_name" "$client_ip"
block::remove_file "$peer_name"
local rule
rule=$(peers::get_meta "$peer_name" "rule")
[[ -n "$rule" ]] && rule::exists "$rule" && \
rule::apply "$rule" "$client_ip" "$peer_name"
log::debug "_unblock_peer: done"
}
# ============================================
# Rule assign
# ============================================
function cmd::group::rule() {
local subcmd="${1:-help}"
shift || true
case "$subcmd" in
assign) cmd::group::rule_assign "$@" ;;
*)
log::error "Unknown rule subcommand: '${subcmd}'"
cmd::group::help
return 1
;;
esac
}
function cmd::group::rule_assign() {
local name="" rule=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--rule) util::require_flag "--rule" "${2:-}" || return 1; rule="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
[[ -z "$rule" ]] && log::error "Missing required flag: --rule" && return 1
group::require_exists "$name" || return 1
rule::require_exists "$rule" || return 1
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
[[ -z "${peers_list[0]:-}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
group::each_peer "$name" cmd::group::_rule_assign_cb "$rule"
log::wg_success "Assigned rule '${rule}' to group '${name}'"
}
function cmd::group::_rule_assign_cb() {
local peer_name="${1:-}" rule="${2:-}"
if ! group::_peer_exists_check "$peer_name"; then
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
return 0
fi
load_command rule
cmd::rule::assign --name "$rule" --peer "$peer_name"
}
# ============================================
# Audit
# ============================================
function cmd::group::audit() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
# Run audit filtered to group peers
load_command audit
# Pass first peer as filter — audit handles the rest per-peer
# Actually run full audit and filter output
local peer_args=()
for peer_name in "${peers_list[@]}"; do
[[ -z "$peer_name" ]] && continue
peer_args+=("$peer_name")
done
test::reset
log::section "Audit: Group '${name}'"
# Precompute fw counts
declare -A peer_fw_counts
while IFS=":" read -r pname count; do
[[ -n "$pname" ]] && peer_fw_counts["$pname"]="$count"
done < <(json::audit_fw_counts "$(ctx::clients)")
test::section "Peer Rules"
for peer_name in "${peer_args[@]}"; do
# Skip if peer no longer exists
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
continue
fi
cmd::audit::check_peer "$peer_name" false "${peer_fw_counts[$peer_name]:-0}"
done
test::summary
}
# ============================================
# Logs
# ============================================
function cmd::group::logs() {
local name="" limit=50
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--limit) util::require_flag "--limit" "${2:-}" || return 1; limit="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
log::section "Logs: Group '${name}'"
for peer_name in "${peers_list[@]}"; do
[[ -z "$peer_name" ]] && continue
# Skip if peer no longer exists
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
log::wg_warning "Peer '${peer_name}' no longer exists — skipping"
continue
fi
printf "\n \033[1;37m── %s ──\033[0m\n" "$peer_name"
load_command logs
cmd::logs::show --name "$peer_name" --limit "$limit"
done
}
# ============================================
# Watch
# ============================================
function cmd::group::watch() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;;
--help) cmd::group::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1
group::require_exists "$name" || return 1
local peers_list=()
mapfile -t peers_list < <(group::peers "$name")
[[ -z "${peers_list[0]}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0
local peer_filter
peer_filter=$(IFS=','; echo "${peers_list[*]}")
# Build comma-separated peer list for watch filter
# Watch already supports --type filter but not multiple peers
# For now, use follow mode filtered to group peers one at a time
# or just run watch with no filter (shows all, user sees group context)
log::section "Live Monitor: Group '${name}'"
printf " Monitoring: %s\n\n" "${peers_list[*]}"
load_command watch
cmd::watch::run --peers "$peer_filter"
}