From adab623f3f5570cf955511e7ae21ea19611acad9 Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Tue, 26 May 2026 20:51:40 +0000 Subject: [PATCH] feat: group purge-stale, peer endpoint history, resolve improvements - group purge-stale: remove stale peers from group(s), --all, --dry-run - daemon: update_peer_history() tracks all endpoints per peer - daemon: endpoint_index.json for O(1) IP -> peer name lookup - json_helper: peer_history_lookup() with index + scan fallback - resolve::endpoint_parts: peer history as step 3 in resolution chain - resolve::service_name: returns service name only, no raw fallback - resolve::endpoint_parts: removed stale cache, always fresh - watch: ui::watch::wg_row/fw_row use shared primitives - ui: ui::_render_endpoint_col, ui::_build_dest shared primitives - shell: peer/hosts/identity/subnet/policy/activity in known commands --- commands/group.command.sh | 96 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/commands/group.command.sh b/commands/group.command.sh index 4f45c2e..0fe8fea 100644 --- a/commands/group.command.sh +++ b/commands/group.command.sh @@ -13,6 +13,8 @@ function cmd::group::on_load() { flag::register --new-name flag::register --main flag::register --force + flag::register --all + flag::register --dry-run } # ============================================ @@ -37,6 +39,7 @@ Subcommands: 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 + purge-stale Remove peers that no longer exist from group(s) block Block all peers in group unblock Unblock all peers in group rule assign Assign a rule to all peers in group @@ -53,6 +56,7 @@ Options: --new-name New group name (for rename) --limit Max log entries per peer (for logs) --force Skip confirmation prompts + --all Apply to all groups (for purge-stale) Examples: wgctl group list @@ -62,6 +66,9 @@ Examples: wgctl group block --name family wgctl group unblock --name family wgctl group rule assign --name family --rule user + wgctl group purge-stale --name family + wgctl group purge-stale --all + wgctl group purge-stale --all --force wgctl group audit --name family wgctl group logs --name family --limit 20 wgctl group watch --name family @@ -88,6 +95,7 @@ function cmd::group::run() { block) cmd::group::block "$@" ;; unblock) cmd::group::unblock "$@" ;; rule) cmd::group::rule "$@" ;; + purge-stale) cmd::group::purge_stale "$@" ;; audit) cmd::group::audit "$@" ;; logs) cmd::group::logs "$@" ;; watch) cmd::group::watch "$@" ;; @@ -813,3 +821,91 @@ function cmd::group::watch() { load_command watch cmd::watch::run --peers "$peer_filter" } + +# ============================================ +# Purge Stale +# ============================================ + +function cmd::group::purge_stale() { + local name="" force=false all=false + local dry_run=false + + while [[ $# -gt 0 ]]; do + case "$1" in + --name) util::require_flag "--name" "${2:-}" || return 1; name="$2"; shift 2 ;; + --force) force=true; shift ;; + --all) all=true; shift ;; + --dry-run) dry_run=true; shift ;; + --help) cmd::group::help; return ;; + *) log::error "Unknown flag: $1"; return 1 ;; + esac + done + + [[ -z "$name" && "$all" == "false" ]] && \ + log::error "Specify --name or --all" && return 1 + + # Build list of groups to process + local -a groups=() + if $all; 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 total_groups=0 + + for group_name in "${groups[@]}"; do + [[ -z "$group_name" ]] && continue + + # Find stale peers — in group but no .conf file + local -a stale=() + while IFS= read -r peer_name; do + [[ -z "$peer_name" ]] && continue + if [[ ! -f "$(ctx::clients)/${peer_name}.conf" ]]; then + stale+=("$peer_name") + fi + done < <(group::peers "$group_name" 2>/dev/null) + + [[ ${#stale[@]} -eq 0 ]] && continue + + (( total_groups++ )) || true + + if ! $force; 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; then + printf " \033[2m[dry-run]\033[0m Would remove '%s' from group '%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 && action="Would remove" + log::wg_success "${action} ${total_removed} stale peer(s)..." + + if $all; then + if [[ "$total_removed" -eq 0 ]]; then + log::wg_warning "No stale peers found in any group" + else + log::wg_success "${action} ${total_removed} stale peer(s)..." + fi + fi +} \ No newline at end of file