#!/usr/bin/env bash # commands/group/helpers.sh # All group implementation logic — called by subcommand run functions # ── Show ───────────────────────────────────────────────────────────────────── function cmd::group::_show_impl() { local name="$1" local group_file; group_file="$(group::path "$name")" log::section "Group: ${name}" printf "\n" local desc; desc=$(json::get "$group_file" "desc") ui::row "Description" "${desc:-—}" local peers_list=() mapfile -t peers_list < <(json::get "$group_file" "peers") || true 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 local valid_count=0 for p in "${peers_list[@]}"; do [[ -z "$p" ]] && continue peers::require_exists "$p" >/dev/null 2>&1 && (( valid_count++ )) || true done local peer_word="peers" [[ "$valid_count" -eq 1 ]] && peer_word="peer" ui::row "Peers" "${valid_count} ${peer_word}" printf "\n" if [[ "$peer_count" -gt 0 ]]; then local w_name=16 w_ip=13 for peer_name in "${peers_list[@]}"; do [[ -z "$peer_name" ]] && continue (( ${#peer_name} > w_name )) && w_name=${#peer_name} done (( w_name += 2 )) ui::group::show_peers peers_list "$w_name" "$w_ip" else printf " \033[2m—\033[0m\n" fi printf "\n" } # ── Rename ─────────────────────────────────────────────────────────────────── function cmd::group::_rename_impl() { local name="$1" new_name="$2" local old_file; old_file="$(group::path "$name")" local new_file; new_file="$(group::path "$new_name")" json::set "$old_file" "name" "\"$new_name\"" mv "$old_file" "$new_file" log::wg_success "Group renamed: ${name} → ${new_name}" } # ── Peer add/remove ─────────────────────────────────────────────────────────── function cmd::group::peer_add_impl() { local name="$1" peer="$2" group::require_exists "$name" || return 1 peers::require_exists "$peer" || return 1 if group::has_peer "$name" "$peer"; then log::wg_warning "Peer '${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_impl() { local name="$1" peer="$2" group::require_exists "$name" || return 1 if ! group::has_peer "$name" "$peer"; then log::wg_warning "Peer '${peer}' is not in group '${name}'" return 0 fi group::remove_peer "$name" "$peer" log::wg_success "Removed '${peer}' from group '${name}'" } # ── Remove peers from WireGuard ─────────────────────────────────────────────── function cmd::group::_rm_peers_impl() { local name="$1" force="$2" 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" != "true" ]]; then read -r -p "Remove all ${peer_count} peers in group '${name}' from WireGuard? [y/N] " confirm case "$confirm" in [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 } # ── Set main ────────────────────────────────────────────────────────────────── function cmd::group::_set_main_impl() { local name="$1" peer="$2" group::require_exists "$name" || return 1 peer=$(peers::resolve_and_require "$peer" "") || return 1 if ! group::has_peer "$name" "$peer"; then log::error "Peer '${peer}' is not in group '${name}'" log::info "Add them first: wgctl group peer --name ${name} --peer ${peer} --action add" return 1 fi peers::set_main_group "$peer" "$name" log::wg_success "Main group for '${peer}' set to '${name}'" } # ── Block ───────────────────────────────────────────────────────────────────── function cmd::group::_block_impl() { local name="$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::_block_peer "$peer_name" "$name"; then (( count++ )) || true else (( skipped++ )) || true fi done [[ "$count" -gt 0 ]] && log::wg_block "All peers from ${name} have been blocked (${count} peers)." [[ "$skipped" -gt 0 ]] && log::wg_warning "${skipped} peers already blocked" } 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") 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 unset IFS block::add_group "$peer_name" "$client_ip" "$group_name" peers::exists_in_server "$peer_name" && block::apply_full "$peer_name" "$client_ip" } # ── Unblock ─────────────────────────────────────────────────────────────────── function cmd::group::_unblock_impl() { local name="$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 [[ "$count" -gt 0 ]] && log::wg_unblock "All peers from ${name} have been unblocked." [[ "$skipped" -gt 0 ]] && \ log::wg_warning "${skipped} peer(s) remain blocked (blocked directly or by other groups)" } 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 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 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" } # ── Rule assign ─────────────────────────────────────────────────────────────── function cmd::group::_rule_assign_impl() { local name="$1" rule="$2" 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" } function cmd::group::_rule_unassign_impl() { local name="$1" rule="$2" log::error "rule unassign not yet implemented" return 1 } # ── Purge stale ─────────────────────────────────────────────────────────────── function cmd::group::_purge_stale_impl() { local name="$1" all="$2" force="$3" dry_run="$4" local -a groups=() if [[ "$all" == "true" ]]; then while IFS= read -r group_file; do groups+=("$(basename "$group_file" .group)") done < <(find "$(ctx::groups)" -name "*.group" 2>/dev/null | sort) else group::require_exists "$name" || return 1 groups=("$name") fi local total_removed=0 for group_name in "${groups[@]}"; do [[ -z "$group_name" ]] && continue local -a stale=() while IFS= read -r peer_name; do [[ -z "$peer_name" ]] && continue [[ ! -f "$(ctx::clients)/${peer_name}.conf" ]] && stale+=("$peer_name") done < <(group::peers "$group_name" 2>/dev/null) [[ ${#stale[@]} -eq 0 ]] && continue if [[ "$force" != "true" && "$dry_run" != "true" ]]; then printf " Group '%s' has %d stale peer(s): %s\n" \ "$group_name" "${#stale[@]}" "${stale[*]}" read -r -p " Remove them? [y/N] " confirm case "$confirm" in [yY]*) ;; *) log::info "Skipped '${group_name}'"; continue ;; esac fi local group_file; group_file="$(group::path "$group_name")" for peer_name in "${stale[@]}"; do if [[ "$dry_run" == "true" ]]; then printf " \033[2m[dry-run]\033[0m Would remove '%s' from '%s'\n" \ "$peer_name" "$group_name" else json::remove "$group_file" "peers" "$peer_name" 2>/dev/null || true log::debug "Removed stale peer '${peer_name}' from group '${group_name}'" fi (( total_removed++ )) || true done done local action="Removed" [[ "$dry_run" == "true" ]] && action="Would remove" [[ "$total_removed" -eq 0 ]] && log::wg_warning "No stale peers found" && return 0 log::wg_success "${action} ${total_removed} stale peer(s)" } # ── Audit ───────────────────────────────────────────────────────────────────── function cmd::group::_audit_impl() { local name="$1" local -a groups=() if [[ -n "$name" ]]; then group::require_exists "$name" || return 1 groups=("$name") else while IFS= read -r group_file; do groups+=("$(basename "$group_file" .group)") done < <(find "$(ctx::groups)" -name "*.group" 2>/dev/null | sort) fi log::section "Group Audit"; echo "" for grp in "${groups[@]}"; do local -a all_peers=() stale=() while IFS= read -r p; do [[ -z "$p" ]] && continue all_peers+=("$p") [[ ! -f "$(ctx::clients)/${p}.conf" ]] && stale+=("$p") done < <(group::peers "$grp" 2>/dev/null) printf " %-20s %d peers" "$grp" "${#all_peers[@]}" [[ ${#stale[@]} -gt 0 ]] && printf " \033[1;31m%d stale\033[0m" "${#stale[@]}" printf "\n" done echo "" } # ── Logs ────────────────────────────────────────────────────────────────────── function cmd::group::_logs_impl() { local name="$1" limit="$2" since="$3" fw="$4" wg="$5" local peers_list=() mapfile -t peers_list < <(group::peers "$name") [[ -z "${peers_list[0]:-}" ]] && \ log::wg_warning "Group '${name}' has no peers" && return 0 load_command logs command::load_subcmd logs show # ensure show_fw_events etc are loaded log::section "Logs: Group '${name}'" for peer_name in "${peers_list[@]}"; do [[ -z "$peer_name" ]] && continue if ! peers::require_exists "$peer_name" >/dev/null 2>&1; then log::wg_warning "Peer '${peer_name}' no longer exists — skipping" continue fi local filter_ip; filter_ip=$(peers::get_ip "$peer_name") local fw_out="" wg_out="" [[ "$wg" != "true" ]] && fw_out=$(cmd::logs::show_fw_events \ "$filter_ip" "$peer_name" "" "$limit" "$(ctx::net)" \ "1" "$since" "" "" "desc" "false" 2>/dev/null) [[ "$fw" != "true" ]] && wg_out=$(cmd::logs::show_wg_events \ "$filter_ip" "$peer_name" "" "$limit" \ "1" "$since" "" "desc" "false" 2>/dev/null) [[ -z "$fw_out" && -z "$wg_out" ]] && continue printf "\n \033[1;37m── %s ──\033[0m\n" "$peer_name" [[ -n "$fw_out" ]] && printf "%s\n" "$fw_out" [[ -n "$wg_out" ]] && printf "%s\n" "$wg_out" done echo "" } # ── Watch ───────────────────────────────────────────────────────────────────── function cmd::group::_watch_impl() { local name="$1" fw="$2" wg="$3" 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 filter_ips="" for peer in "${peers_list[@]}"; do [[ -z "$peer" ]] && continue local ip; ip=$(peers::get_ip "$peer") filter_ips+="${ip}," done filter_ips="${filter_ips%,}" load_command logs load_command watch cmd::logs::follow "$filter_ips" "$name" "" "$fw" "$wg" } # ── Render/JSON ─────────────────────────────────────────────────────────────── function cmd::group::_render_table() { local data="$1" w_name="$2" w_desc="$3" ui::group::list_header_table while IFS="|" read -r name desc total blocked; do [[ -z "$name" ]] && continue ui::group::list_row_table "$name" "$desc" "$total" "$blocked" done <<< "$data" } function cmd::group::_output_json() { local data data=$(json::group_list_data "$(ctx::groups)" "$(ctx::blocks)" "$(ctx::clients)") local -a groups=() while IFS="|" read -r name desc total blocked; do [[ -z "$name" ]] && continue groups+=("{\"name\":\"${name}\",\"description\":\"${desc}\",\"peers\":${total},\"blocked\":${blocked}}") done <<< "$data" local array; array=$(IFS=','; echo "${groups[*]:-}") printf '[%s]' "$array" | json::envelope "groups" "${#groups[@]}" }