829 lines
25 KiB
Bash
829 lines
25 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 --main
|
|
flag::register --force
|
|
}
|
|
|
|
# ============================================
|
|
# Help
|
|
# ============================================
|
|
|
|
function cmd::group::help() {
|
|
cat <<EOF
|
|
Usage: wgctl group <subcommand> [options]
|
|
|
|
Manage peer groups. Operations like block/unblock act on all peers in a group.
|
|
A peer can belong to multiple groups (M:N relationship).
|
|
Group blocks track which groups blocked a peer — unblocking one group won't
|
|
unblock a peer still blocked by another group.
|
|
|
|
Subcommands:
|
|
list, ls List all groups
|
|
show Show group members and their status
|
|
add, new, create Create a new group
|
|
remove, rm, del Remove a group definition
|
|
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 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 "$@" ;;
|
|
set-main) cmd::group::set_main "$@" ;;
|
|
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
|
|
|
|
local data
|
|
data=$(json::group_list_data "$groups_dir" "$(ctx::blocks)" "$(ctx::clients)")
|
|
[[ -z "$data" ]] && log::wg "No groups configured" && return 0
|
|
|
|
# Measure column widths
|
|
local w_name=12 w_desc=16
|
|
while IFS="|" read -r name desc total blocked; do
|
|
[[ -z "$name" ]] && continue
|
|
(( ${#name} > w_name )) && w_name=${#name}
|
|
local desc_len=${#desc}
|
|
[[ -z "$desc" ]] && desc_len=1
|
|
(( desc_len > w_desc )) && w_desc=$desc_len
|
|
done <<< "$data"
|
|
(( w_name += 2 ))
|
|
(( w_desc += 2 ))
|
|
|
|
log::section "Groups"
|
|
echo ""
|
|
|
|
while IFS="|" read -r name desc total blocked; do
|
|
[[ -z "$name" ]] && continue
|
|
ui::group::list_row "$name" "$desc" "$total" "$blocked" "$w_name" "$w_desc"
|
|
done <<< "$data"
|
|
|
|
echo ""
|
|
}
|
|
|
|
# ============================================
|
|
# 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}"
|
|
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 peer_word="peers"
|
|
[[ "$peer_count" -eq 1 ]] && peer_word="peer"
|
|
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
|
|
# Measure name and IP widths
|
|
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 ))
|
|
|
|
for peer_name in "${peers_list[@]}"; do
|
|
[[ -z "$peer_name" ]] && continue
|
|
|
|
if ! peers::require_exists "$peer_name" > /dev/null 2>&1; then
|
|
printf " \033[2m%-${w_name}s (no longer exists)\033[0m\n" "$peer_name"
|
|
continue
|
|
fi
|
|
|
|
local ip rule is_blocked
|
|
ip=$(peers::get_ip "$peer_name")
|
|
rule=$(peers::get_meta "$peer_name" "rule")
|
|
peers::is_blocked "$peer_name" 2>/dev/null && is_blocked="true" || is_blocked="false"
|
|
|
|
ui::group::show_member_row "$peer_name" "$ip" "${rule:--}" \
|
|
"$is_blocked" "$w_name" "$w_ip"
|
|
done
|
|
else
|
|
printf " \033[2m—\033[0m\n"
|
|
fi
|
|
|
|
printf "\n"
|
|
}
|
|
|
|
# ============================================
|
|
# 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="" set_main=false
|
|
|
|
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 ;;
|
|
--main) util::require_flag "--main" "${2:-}" || return 1; set_main=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
|
|
[[ -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}'"
|
|
|
|
if $set_main; then
|
|
peers::set_main_group "$peer_name" "$group_name"
|
|
log::wg_success "Set '${group_name}' as main group for ${peer_name}"
|
|
fi
|
|
}
|
|
|
|
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}'"
|
|
}
|
|
|
|
function cmd::group::set_main() {
|
|
local group_name="" peer_name="" type=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--name) group_name="$2"; shift 2 ;;
|
|
--peer) peer_name="$2"; shift 2 ;;
|
|
--type) type="$2"; shift 2 ;;
|
|
*) log::error "Unknown flag: $1"; return 1 ;;
|
|
esac
|
|
done
|
|
|
|
[[ -z "$group_name" ]] && log::error "Missing --name" && return 1
|
|
[[ -z "$peer_name" ]] && log::error "Missing --peer" && return 1
|
|
|
|
# Resolve peer name
|
|
peer_name=$(peers::resolve_and_require "$peer_name" "$type") || return 1
|
|
|
|
# Verify peer is in the group
|
|
if ! group::has_peer "$group_name" "$peer_name"; then
|
|
log::error "Peer '${peer_name}' is not in group '${group_name}'"
|
|
log::info "Add them first: wgctl group peer add --name ${group_name} --peer ${peer_name}"
|
|
return 1
|
|
fi
|
|
|
|
peers::set_main_group "$peer_name" "$group_name"
|
|
log::wg_success "Main group for '${peer_name}' set to '${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
|
|
}
|
|
|
|
# ============================================
|
|
# 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
|
|
|
|
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() {
|
|
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"
|
|
}
|