#!/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 --force } # ============================================ # Help # ============================================ function cmd::group::help() { cat < [options] Manage peer groups. Subcommands: list, ls List all groups show Show group details and members 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 from WireGuard server block Block all peers in group unblock Unblock all peers in group rule assign Assign a rule to all peers in group audit Audit all peers in group logs Show logs for all peers in group watch Live monitor for all peers in group Options: --name Group name --desc Group description --peer Peer name --type Peer device type (for peer resolution) --rule Rule name (for rule assign) --new-name New name (for rename) --force Skip confirmation prompts Examples: wgctl group add --name family --desc "Family devices" wgctl group peer add --name family --peer phone-nuno wgctl group block --name family wgctl group rule assign --name family --rule user wgctl group audit --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 "$@" ;; 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 log::section "Groups" printf "\n %-20s %-35s %-8s %s\n" "NAME" "DESCRIPTION" "PEERS" "STATUS" printf " %s\n" "$(printf '─%.0s' {1..75})" while IFS="|" read -r name desc total blocked; do [[ -z "$name" ]] && continue local status_color="" status_str="active" if [[ "$total" -gt 0 ]]; then if [[ "$blocked" -eq "$total" ]]; then status_color="\033[1;31m" status_str="blocked" elif [[ "$blocked" -gt 0 ]]; then status_color="\033[1;33m" status_str="blocked (${blocked}/${total})" else status_color="\033[1;32m" status_str="active" fi fi local short_desc="${desc:0:33}" [[ ${#desc} -gt 33 ]] && short_desc="${short_desc}..." printf " %-20s %-35s %-8s %b\n" \ "$name" "${short_desc:-—}" "$total" \ "${status_color}${status_str}\033[0m" done < <(json::group_list_data "$groups_dir" "$(ctx::blocks)") printf "\n" } # 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 # log::section "Groups" # printf "\n %-20s %-35s %-8s %s\n" "NAME" "DESCRIPTION" "PEERS" "STATUS" # printf " %s\n" "$(printf '─%.0s' {1..75})" # for group_file in "${groups_dir}"/*.group; do # [[ -f "$group_file" ]] || continue # local name desc # name=$(json::get "$group_file" "name") # desc=$(json::get "$group_file" "desc") # # Count peers # local peers_list=() # mapfile -t peers_list < <(json::get "$group_file" "peers") # # Filter empty entries # 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 # # Check block status # local blocked=0 total=0 # for peer_name in "${peers_list[@]}"; do # [[ -z "$peer_name" ]] && continue # (( total++ )) || true # peers::is_blocked "$peer_name" && (( blocked++ )) || true # done # local status_color="" # local status_str="active" # if [[ "$total" -gt 0 ]]; then # if [[ "$blocked" -eq "$total" ]]; then # status_color="\033[1;31m" # status_str="blocked" # elif [[ "$blocked" -gt 0 ]]; then # status_color="\033[1;33m" # status_str="blocked (${blocked}/${total})" # # status_color="\033[1;33m" # # status_str="${blocked}/${total} blocked" # else # status_color="\033[1;32m" # status_str="active" # fi # fi # local short_desc="${desc:0:33}" # [[ ${#desc} -gt 33 ]] && short_desc="${short_desc}..." # printf " %-20s %-35s %-8s %b\n" \ # "$name" \ # "${short_desc:-—}" \ # "$peer_count" \ # "${status_color}${status_str}\033[0m" # done # printf "\n" # } # ============================================ # 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}" local desc desc=$(json::get "$group_file" "desc") printf "\n %-20s %s\n" "Description:" "${desc:-—}" # Load peers local peers_list=() mapfile -t peers_list < <(json::get "$group_file" "peers") # Filter empty entries 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 printf " %-20s %s\n" "Peers:" "$peer_count" printf " %s\n" "$(printf '─%.0s' {1..50})" if [[ "$peer_count" -gt 0 ]]; then printf "\n %-28s %-15s %-12s %s\n" "NAME" "IP" "RULE" "STATUS" printf " %s\n" "$(printf '─%.0s' {1..65})" for peer_name in "${peers_list[@]}"; do [[ -z "$peer_name" ]] && continue local ip rule status_str status_color ip=$(peers::get_ip "$peer_name") rule=$(peers::get_meta "$peer_name" "rule") rule="${rule:-—}" if peers::is_blocked "$peer_name"; then status_color="\033[1;31m" status_str="blocked" else status_color="\033[1;32m" status_str="active" fi printf " %-28s %-15s %-12s %b\n" \ "$peer_name" "${ip:-—}" "$rule" \ "${status_color}${status_str}\033[0m" done else printf " —\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 local group_file group_file="$(group::path "$name")" python3 -c " import json group = {'name': '${name}', 'desc': '${desc}', 'peers': []} with open('${group_file}', 'w') as f: json.dump(group, f, indent=2) "