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 --new-name
flag::register --main flag::register --main
flag::register --force flag::register --force
flag::register --all
flag::register --dry-run
} }
# ============================================ # ============================================
@ -37,6 +39,7 @@ Subcommands:
peer add Add a peer to a group peer add Add a peer to a group
peer remove, peer rm Remove a peer from a group peer remove, peer rm Remove a peer from a group
rm-peers Remove all peers in group from WireGuard 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 block Block all peers in group
unblock Unblock all peers in group unblock Unblock all peers in group
rule assign Assign a rule to 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) --new-name <name> New group name (for rename)
--limit <n> Max log entries per peer (for logs) --limit <n> Max log entries per peer (for logs)
--force Skip confirmation prompts --force Skip confirmation prompts
--all Apply to all groups (for purge-stale)
Examples: Examples:
wgctl group list wgctl group list
@ -62,6 +66,9 @@ Examples:
wgctl group block --name family wgctl group block --name family
wgctl group unblock --name family wgctl group unblock --name family
wgctl group rule assign --name family --rule user 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 audit --name family
wgctl group logs --name family --limit 20 wgctl group logs --name family --limit 20
wgctl group watch --name family wgctl group watch --name family
@ -88,6 +95,7 @@ function cmd::group::run() {
block) cmd::group::block "$@" ;; block) cmd::group::block "$@" ;;
unblock) cmd::group::unblock "$@" ;; unblock) cmd::group::unblock "$@" ;;
rule) cmd::group::rule "$@" ;; rule) cmd::group::rule "$@" ;;
purge-stale) cmd::group::purge_stale "$@" ;;
audit) cmd::group::audit "$@" ;; audit) cmd::group::audit "$@" ;;
logs) cmd::group::logs "$@" ;; logs) cmd::group::logs "$@" ;;
watch) cmd::group::watch "$@" ;; watch) cmd::group::watch "$@" ;;
@ -813,3 +821,91 @@ function cmd::group::watch() {
load_command watch load_command watch
cmd::watch::run --peers "$peer_filter" 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
}