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::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
This commit is contained in:
Nuno Duque Nunes 2026-05-26 20:51:40 +00:00
parent 8b47e55b4a
commit adab623f3f

View file

@ -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 <name> New group name (for rename)
--limit <n> 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 <group> 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
}