From a7fd62ce32bdf707844c70f80310efd5b8481f4d Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Wed, 13 May 2026 00:06:34 +0000 Subject: [PATCH] refactor: group::each_peer helper, peer existence checks, group remove cleanup, watch multi-peer filter --- commands/add.command.sh | 35 +++-- commands/block.command.sh | 14 +- commands/group.command.sh | 149 +++++++++++++++----- commands/inspect.command.sh | 2 +- commands/list.command.sh | 270 ++++++++++++++++++++---------------- commands/logs.command.sh | 8 +- commands/preset.command.sh | 199 -------------------------- commands/remove.command.sh | 1 + commands/shell.command.sh | 2 +- commands/test.command.sh | 13 +- commands/unblock.command.sh | 20 +-- commands/watch.command.sh | 103 +++++++------- core/context.sh | 7 - core/json.sh | 6 +- core/json_helper.py | 109 +++++++++++---- daemon/endpoint_cache.json | 2 +- modules/config.module.sh | 1 - modules/firewall.module.sh | 46 +----- modules/group.module.sh | 33 +++++ modules/peers.module.sh | 44 ++---- modules/rule.module.sh | 9 +- presets/guest.preset | 5 - presets/no-docker.preset | 5 - presets/no-internet.preset | 5 - presets/no-proxmox.preset | 5 - wgctl | 5 - 26 files changed, 505 insertions(+), 593 deletions(-) delete mode 100644 commands/preset.command.sh delete mode 100644 presets/guest.preset delete mode 100644 presets/no-docker.preset delete mode 100644 presets/no-internet.preset delete mode 100644 presets/no-proxmox.preset diff --git a/commands/add.command.sh b/commands/add.command.sh index 18f1d84..e279d04 100644 --- a/commands/add.command.sh +++ b/commands/add.command.sh @@ -9,8 +9,8 @@ function cmd::add::on_load() { flag::register --type flag::register --subtype flag::register --rule + flag::register --group flag::register --ip - flag::register --preset flag::register --guest flag::register --tunnel flag::register --show-config @@ -30,10 +30,12 @@ Add a new WireGuard client. Options: --name Client name (e.g. nuno) --type Device type: desktop, laptop, phone, tablet, guest + --subtype Guest subtype: desktop, laptop, phone, tablet (mostly used for guest) --ip Override auto-assigned IP (optional) - --preset Apply a firewall preset (repeatable) --guest Shorthand for --type guest - --tunnel Tunnel mode: split (default) or full + --tunnel Tunnel mode: split|full (default: split) + --rule Assign rule on creation (default: user, guest types: guest) + --group Add to group on creation (group must exist) --show-config Shows the WireGuard peer config --show-qr Shows the WireGuard config in a QR Code @@ -44,6 +46,10 @@ Device Types and Subnets: tablet 10.1.4.x guest 10.1.100.x +Rules: + Automatically assigned based on type (guest → guest rule, others → user rule). + Override with --rule. Manage rules with: wgctl rule help + Tunnel Modes: split Route only VPN subnet + LAN through WireGuard (default) full Route all traffic through WireGuard @@ -53,7 +59,9 @@ Examples: wgctl add --name nuno --type laptop --ip 10.1.2.5 wgctl add --name nuno --type phone --tunnel full wgctl add --name guest1 --type phone --guest - wgctl add --name restricted --type desktop --preset no-docker --preset no-proxmox + wgctl add --name restricted --type desktop + wgctl add --name dev --type laptop --rule dev-01 + wgctl add --name visitor --type guest --show-qr EOF } @@ -118,10 +126,10 @@ function cmd::add::run() { local type="" local subtype="" local rule="" + local group="" local ip="" local tunnel="" local guest=false - local presets=() local show_config=false local show_qr=false @@ -132,8 +140,8 @@ function cmd::add::run() { --type) type="$2"; shift 2 ;; --subtype) subtype="$2"; shift 2 ;; --rule) rule="$2"; shift 2 ;; + --group) group="$2"; shift 2 ;; --ip) ip="$2"; shift 2 ;; - --preset) presets+=("$2"); shift 2 ;; --guest) guest=true; shift ;; --tunnel) tunnel="$2"; shift 2 ;; --show-config) show_config=true; shift ;; @@ -176,16 +184,21 @@ function cmd::add::run() { peers::create_client_config "$full_name" "$effective_type" "$ip" "$allowed_ips" || return 1 [[ -n "$subtype" ]] && peers::set_meta "$full_name" "subtype" "$subtype" + if [[ -n "$group" ]]; then + if ! group::exists "$group"; then + log::wg_warning "Group '${group}' not found — skipping group assignment" + else + group::add_peer "$group" "$full_name" + log::wg "Added to group: ${group}" + fi + fi + local public_key public_key=$(keys::public "$full_name") || return 1 peers::add_to_server "$full_name" "$public_key" "$ip" || return 1 - for preset in "${presets[@]}"; do - fw::apply_preset "$preset" "$ip" || return 1 - done - [[ -n "$rule" ]] && rule::apply "$rule" "$ip" || return 1 - peers::reload || return 1 + peers::reload || return 1 log::wg_success "Client added successfully: ${full_name} (${ip}) [${tunnel} tunnel]" cmd::add::_show_result "$full_name" "${subtype:-$type}" diff --git a/commands/block.command.sh b/commands/block.command.sh index 9a58412..e8111e8 100644 --- a/commands/block.command.sh +++ b/commands/block.command.sh @@ -27,17 +27,21 @@ Block a client entirely or restrict access to specific IPs/ports/subnets. Block rules are persisted and restored on WireGuard restart. Options: - --name Client name (e.g. phone-nuno) - --ip Block access to specific IP (repeatable) - --subnet Block access to subnet (repeatable) - --port Block specific port, e.g. 10.0.0.210:9000:tcp (repeatable) + --name Client name (e.g. phone-nuno) + --type Device type (optional, combines with --name) + --ip Block access to specific IP (repeatable) + --subnet Block access to subnet (repeatable) + --port Block specific port, e.g. 10.0.0.210:9000:tcp (repeatable) + --force Skip confirmation prompt + --quiet Suppress output (used by group block) Examples: wgctl block --name phone-nuno + wgctl block --name nuno --type phone wgctl block --name phone-nuno --ip 10.0.0.210 wgctl block --name phone-nuno --subnet 10.0.0.0/24 wgctl block --name phone-nuno --port 10.0.0.210:9000:tcp - wgctl ban --name phone-nuno --ip 10.0.0.100 --ip 10.0.0.200 + wgctl ban --name phone-nuno EOF } diff --git a/commands/group.command.sh b/commands/group.command.sh index bdbd7a8..0e9145b 100644 --- a/commands/group.command.sh +++ b/commands/group.command.sh @@ -260,12 +260,19 @@ function cmd::group::show() { printf " %s\n" "$(printf '─%.0s' {1..65})" 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 + 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 + if peers::is_blocked "$peer_name" 2>/dev/null; then status_color="\033[1;31m" status_str="blocked" else @@ -274,14 +281,15 @@ function cmd::group::show() { fi printf " %-28s %-15s %-12s %b\n" \ - "$peer_name" "${ip:-—}" "$rule" \ - "${status_color}${status_str}\033[0m" + "$peer_name" "0" "$rule" \ + "${status_str}\033[0m" done else printf " —\n" fi printf "\n" + return 0 } # ============================================ @@ -307,15 +315,7 @@ function cmd::group::add() { 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) -" /dev/null 2>&1; then +# log::wg_warning "Peer '${peer_name}' no longer exists — skipping" +# continue +# fi + +# cmd::remove::run --name "$peer_name" --force +# (( count++ )) || true +# done + +# log::wg_success "Removed ${count} peers from WireGuard (group '${name}' definition kept)" +# } + # ============================================ # Block / Unblock # ============================================ @@ -536,6 +590,12 @@ function cmd::group::block() { local count=0 blocked_names=() 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 + if peers::is_blocked "$peer_name"; then log::wg_warning "${peer_name} — already blocked" continue @@ -582,6 +642,12 @@ function cmd::group::unblock() { local count=0 unblocked_names=() 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 + if ! peers::is_blocked "$peer_name"; then log::wg_warning "${peer_name} — not blocked" continue @@ -635,20 +701,24 @@ function cmd::group::rule_assign() { [[ -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 + 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 + [[ -z "${peers_list[0]:-}" ]] && log::wg_warning "Group '${name}' has no peers" && return 0 - local count=0 - for peer_name in "${peers_list[@]}"; do - [[ -z "$peer_name" ]] && continue - cmd::rule::assign --name "$rule" --peer "$peer_name" - (( count++ )) || true - done + group::each_peer "$name" cmd::group::_rule_assign_cb "$rule" + log::wg_success "Assigned rule '${rule}' to group '${name}'" +} - log::wg_success "Assigned rule '${rule}' to ${count} peers in 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" } # ============================================ @@ -694,6 +764,11 @@ function cmd::group::audit() { 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 @@ -727,6 +802,11 @@ function cmd::group::logs() { 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" @@ -755,15 +835,18 @@ function cmd::group::watch() { 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 peers: %s\n\n" "${peers_list[*]}" + printf " Monitoring: %s\n\n" "${peers_list[*]}" load_command logs # Use follow mode — it shows all peers but user knows context # Future: add --peers flag to follow for multi-peer filter - cmd::logs::follow "" "" "" false false -} \ No newline at end of file + cmd::logs::follow "" "" "" false false "$peer_filter" +} diff --git a/commands/inspect.command.sh b/commands/inspect.command.sh index 6915d20..44c5fe5 100644 --- a/commands/inspect.command.sh +++ b/commands/inspect.command.sh @@ -110,7 +110,7 @@ function cmd::inspect::_rule_info() { ui::print_list "-" "$block_ips" ui::print_list "-" "$block_ports" else - ui::row "Blocks" "— (full access)" + ui::row "Blocks" "—" fi } diff --git a/commands/list.command.sh b/commands/list.command.sh index d991804..1980828 100644 --- a/commands/list.command.sh +++ b/commands/list.command.sh @@ -6,6 +6,8 @@ function cmd::list::on_load() { flag::register --type + flag::register --rule + flag::register --group flag::register --online flag::register --offline flag::register --restricted @@ -26,18 +28,22 @@ Usage: wgctl list [options] List all WireGuard clients. Options: - --type Filter by device type - --online Show only connected clients - --offline Show only disconnected clients - --allowed Show only fully allowed clients - --restricted Show only restricted clients - --blocked Show only blocked clients - --detailed Show full detail cards for all clients - --name Show detail card for a single client + --type Filter by device type (desktop, laptop, phone, tablet, guest) + --rule Filter by assigned rule + --group Filter by group membership + --online Show only connected clients + --offline Show only disconnected clients + --blocked Show only blocked clients + --restricted Show only restricted clients + --allowed Show only unrestricted clients + --detailed Show full detail cards for all clients + --name Show detail card for a single client Examples: wgctl list - wgctl list --type phone + wgctl list --type guest + wgctl list --rule user + wgctl list --group family wgctl list --online wgctl list --blocked wgctl list --detailed @@ -45,47 +51,6 @@ Examples: EOF } -# ============================================ -# Precompute helpers -# ============================================ - -function cmd::list::_precompute_wg() { - # Returns two associative arrays via nameref - local -n _handshakes="$1" - local -n _endpoints="$2" - - while IFS=$'\t' read -r pubkey ts; do - [[ -n "$pubkey" ]] && _handshakes["$pubkey"]="$ts" - done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) - - while IFS=$'\t' read -r pubkey endpoint; do - [[ -n "$pubkey" ]] && _endpoints["$pubkey"]="$endpoint" - done < <(wg show "$(config::interface)" endpoints 2>/dev/null) -} - -function cmd::list::_precompute_block_status() { - local -n _blocked="$1" - local -n _restricted="$2" - - # Blocked = not in wg server config - local wg_peers - wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null) - - while IFS= read -r name; do - # Check block file - [[ -f "$(ctx::block::path "${name}.block")" ]] && _restricted["$name"]=true || _restricted["$name"]=false - - # Check if in server config - local pubkey - pubkey=$(keys::public "$name" 2>/dev/null || echo "") - if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then - _blocked["$name"]=true - else - _blocked["$name"]=false - fi - done < <(peers::all) -} - # ============================================ # Status / Display helpers # ============================================ @@ -186,7 +151,7 @@ function peers::get_type() { function cmd::list::display_type() { local name="${1:-0}" local type="${2:-0}" - local subtype="${3:-0}" + local subtype="${3:-}" if config::is_guest_type "$type" && [[ -n "$subtype" ]]; then echo "guest/${subtype}" elif config::is_guest_type "$type"; then @@ -234,9 +199,6 @@ function cmd::list::_render_footer() { function cmd::list::_render_summary() { local group_summary="${1:-}" local -n _rule_counts="$2" - local total="${3:-}" # filtered total - - # total=$(find "$(ctx::clients)" -name "*.conf" | wc -l | tr -d ' ') # Count total from rule_counts (only filtered peers) local total=0 @@ -273,7 +235,7 @@ function cmd::list::show_client() { local ip allowed_ips public_key ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) - allowed_ips=$(grep "^AllowedIPs" "$conf" | awk '{print $3}') + allowed_ips=$(grep "^AllowedIPs" "$conf" | cut -d'=' -f2- | xargs) public_key=$(keys::public "$name" 2>/dev/null || echo "unknown") local type @@ -356,7 +318,7 @@ function cmd::list::is_attempting() { cmd::list::_is_attempting "$@"; } # ============================================ function cmd::list::run() { - local filter_type="" + local filter_type="" filter_rule="" filter_group="" local online_only=false offline_only=false local restricted_only=false blocked_only=false allowed_only=false local detailed=false single_name="" @@ -364,6 +326,8 @@ function cmd::list::run() { while [[ $# -gt 0 ]]; do case "$1" in --type) filter_type="$2"; shift 2 ;; + --rule) filter_rule="$2"; shift 2 ;; + --group) filter_group="$2"; shift 2 ;; --online) online_only=true; shift ;; --offline) offline_only=true; shift ;; --restricted) restricted_only=true; shift ;; @@ -383,92 +347,45 @@ function cmd::list::run() { # Single detail card if [[ -n "$single_name" ]]; then cmd::list::show_client "$single_name" - return + return 0 fi local dir dir="$(ctx::clients)" local confs=("${dir}"/*.conf) if [[ ! -f "${confs[0]}" ]]; then - log::wg_list "No clients configured" + log::wg_warning "No clients configured" return 0 fi # ── Precompute everything ────────────────── - - # Peer data (ip, rule, subtype, last_ts, last_evt) — single Python call - declare -A p_ips p_rules p_subtypes p_last_ts p_last_evt - while IFS="|" read -r name ip rule subtype last_ts last_evt; do - [[ -z "$name" ]] && continue - p_ips["$name"]="$ip" - p_rules["$name"]="${rule:-—}" - p_subtypes["$name"]="$subtype" - p_last_ts["$name"]="$last_ts" - p_last_evt["$name"]="$last_evt" - done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)") - - # WireGuard handshakes + endpoints — two wg show calls - declare -A wg_handshakes wg_endpoints - cmd::list::_precompute_wg wg_handshakes wg_endpoints - - # Block/restricted status - declare -A p_blocked p_restricted - cmd::list::_precompute_block_status p_blocked p_restricted - - # Public keys — read from key files - declare -A p_pubkeys - for kf in "${dir}"/*_public.key; do - [[ -f "$kf" ]] || continue - local kname - kname=$(basename "$kf" _public.key) - p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "") - done - - # Group map - local has_groups=false - local groups_dir - groups_dir="$(ctx::groups)" - local group_files=("${groups_dir}"/*.group) - [[ -f "${group_files[0]}" ]] && has_groups=true - - declare -A peer_group_map - if $has_groups; then - while IFS=":" read -r peer_name group_name; do - [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name" - done < <(json::peer_group_map "$groups_dir") - fi + cmd::list::_precompute_all # ── Detailed mode ────────────────────────── - if $detailed; then log::section "WireGuard Clients" - cmd::list::_iter_confs "$filter_type" cmd::list::show_client - return + cmd::list::_iter_confs "$filter_type" cmd::list::_show_client_safe + return 0 fi -# ── Table view ───────────────────────────── + # ── Build filter description ─────────────── + local filter_desc="" + cmd::list::_build_filter_desc - log::section "WireGuard Clients" - cmd::list::_render_header $has_groups - - declare -A rule_counts - declare -A group_counts + # ── Table view ───────────────────────────── + declare -A rule_counts=() group_counts=() + _list_header_printed=false cmd::list::_iter_confs "$filter_type" cmd::list::_render_row - cmd::list::_render_footer $has_groups - # Build summaries - declare -A displayed_rules displayed_groups - - local group_summary="" - if $has_groups; then - for g in "${!group_counts[@]}"; do - group_summary+="${group_counts[$g]} in ${g}, " - done - group_summary="${group_summary%, }" + if [[ "$_list_header_printed" == "true" ]]; then + cmd::list::_render_footer $has_groups + local group_summary="" + cmd::list::_build_group_summary + cmd::list::_render_summary "$group_summary" rule_counts "$filter_desc" + else + log::wg_warning "No results found${filter_desc:+ for: ${filter_desc}}" fi - - cmd::list::_render_summary "$group_summary" rule_counts } function cmd::list::_iter_confs() { @@ -510,6 +427,11 @@ function cmd::list::_render_row() { if $allowed_only && { [[ "$is_blocked" == "true" ]] || \ [[ "$is_restricted" == "true" ]]; }; then return 0; fi + if [[ -n "$filter_group" ]]; then + local peer_group="${peer_group_map[$client_name]:-}" + [[ "$peer_group" != "$filter_group" ]] && return 0 + fi + # Format display values local status last_seen display_type rule group_display status=$(cmd::list::_format_status "$client_name" "$pubkey" \ @@ -520,6 +442,15 @@ function cmd::list::_render_row() { "${p_subtypes[$client_name]:-}") rule="${p_rules[$client_name]:-—}" + if [[ -n "$filter_rule" && "$rule" != "$filter_rule" ]]; then return 0; fi + + # Print header on first match + if [[ "${_list_header_printed:-false}" == "false" ]]; then + log::section "WireGuard Clients" + cmd::list::_render_header $has_groups + _list_header_printed=true + fi + # Update rule counts for summary (outer scope array) rule_counts["$rule"]=$(( ${rule_counts[$rule]:-0} + 1 )) || true @@ -548,4 +479,101 @@ function cmd::list::_render_row() { "$client_name" "$ip" "$display_type" "$rule" \ "$padded_status" "$last_seen" fi +} + +# ============================================ +# Private helpers +# ============================================ + +function cmd::list::_precompute_all() { + # Peer data + declare -gA p_ips=() p_rules=() p_subtypes=() p_last_ts=() p_last_evt=() + while IFS="|" read -r name ip rule subtype last_ts last_evt; do + [[ -z "$name" ]] && continue + p_ips["$name"]="$ip" + p_rules["$name"]="${rule:-—}" + p_subtypes["$name"]="$subtype" + p_last_ts["$name"]="$last_ts" + p_last_evt["$name"]="$last_evt" + done < <(json::peer_data "$(ctx::clients)" "$(ctx::meta)" "$(ctx::events_log)") + + # WireGuard handshakes + endpoints + declare -gA wg_handshakes=() wg_endpoints=() + while IFS=$'\t' read -r pubkey ts; do + [[ -n "$pubkey" ]] && wg_handshakes["$pubkey"]="$ts" + done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null) + while IFS=$'\t' read -r pubkey endpoint; do + [[ -n "$pubkey" ]] && wg_endpoints["$pubkey"]="$endpoint" + done < <(wg show "$(config::interface)" endpoints 2>/dev/null) + + # Block/restricted status + declare -gA p_blocked=() p_restricted=() + local wg_peers + wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null) + while IFS= read -r name; do + [[ -f "$(ctx::block::path "${name}.block")" ]] \ + && p_restricted["$name"]=true || p_restricted["$name"]=false + local pubkey + pubkey=$(keys::public "$name" 2>/dev/null || echo "") + if [[ -n "$pubkey" ]] && ! echo "$wg_peers" | grep -qF "$pubkey"; then + p_blocked["$name"]=true + else + p_blocked["$name"]=false + fi + done < <(peers::all) + + # Public keys + declare -gA p_pubkeys=() + local dir + dir="$(ctx::clients)" + for kf in "${dir}"/*_public.key; do + [[ -f "$kf" ]] || continue + local kname + kname=$(basename "$kf" _public.key) + p_pubkeys["$kname"]=$(cat "$kf" 2>/dev/null || echo "") + done + + # Groups + has_groups=false + declare -gA peer_group_map=() + local groups_dir + groups_dir="$(ctx::groups)" + local group_files=("${groups_dir}"/*.group) + if [[ -f "${group_files[0]}" ]]; then + has_groups=true + while IFS=":" read -r peer_name group_name; do + [[ -n "$peer_name" ]] && peer_group_map["$peer_name"]="$group_name" + done < <(json::peer_group_map "$groups_dir") + fi +} + +function cmd::list::_build_filter_desc() { + filter_desc="" + [[ -n "$filter_type" ]] && filter_desc+="type=${filter_type} " + [[ -n "$filter_rule" ]] && filter_desc+="rule=${filter_rule} " + [[ -n "$filter_group" ]] && filter_desc+="group=${filter_group} " + $online_only && filter_desc+="online " + $offline_only && filter_desc+="offline " + $blocked_only && filter_desc+="blocked " + filter_desc="${filter_desc% }" +} + +function cmd::list::_build_group_summary() { + group_summary="" + if $has_groups; then + declare -A _gs=() + for peer in "${!peer_group_map[@]}"; do + local g="${peer_group_map[$peer]}" + _gs["$g"]=$(( ${_gs["$g"]:-0} + 1 )) || true + done + for g in "${!_gs[@]}"; do + group_summary+="${_gs[$g]} in ${g}, " + done + group_summary="${group_summary%, }" + fi +} + +function cmd::list::_show_client_safe() { + local name="$1" + cmd::list::show_client "$name" || true } \ No newline at end of file diff --git a/commands/logs.command.sh b/commands/logs.command.sh index d702eb8..5627740 100644 --- a/commands/logs.command.sh +++ b/commands/logs.command.sh @@ -101,8 +101,10 @@ function cmd::logs::show() { } function cmd::logs::follow() { - local filter_ip="$1" filter_name="$2" filter_type="$3" - local fw_only="$4" wg_only="$5" + local filter_ip="${1:-}" filter_name="${2:-}" filter_type="${3:-}" + local fw_only="${4:-false}" wg_only="${5:-false}" + local filter_peers="${6:-}" + local clients_dir clients_dir="$(ctx::clients)" @@ -137,7 +139,7 @@ function cmd::logs::follow() { printf " %-20s %-8s %-20s %-25s %b\n" \ "$ts" "wireguard" "$client" "$dst_or_endpoint" "$colored_event" fi - done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" "$clients_dir") + done < <(json::follow_logs "$fw_log" "$wg_log" "$filter_ip" "$filter_type" "$clients_dir" "$filter_peers") } function cmd::logs::remove() { diff --git a/commands/preset.command.sh b/commands/preset.command.sh deleted file mode 100644 index 3f579ca..0000000 --- a/commands/preset.command.sh +++ /dev/null @@ -1,199 +0,0 @@ -#!/usr/bin/env bash - -# ============================================ -# Help -# ============================================ - -function cmd::preset::help() { - cat < [options] - -Manage firewall presets. - -Subcommands: - list, ls List available presets - add, new, create Add a new preset - remove, rm, del Remove a preset - -Options for add: - --name Preset name (e.g. no-jellyfin) - --desc Human readable description - --block-ip Block specific IP (repeatable) - --block-subnet Block subnet (repeatable) - --block-port Block specific port (repeatable) - -Examples: - wgctl preset list - wgctl preset add --name no-jellyfin --desc "Block Jellyfin" --block-ip 10.0.0.210 --block-port 10.0.0.210:8096:tcp - wgctl preset remove --name no-jellyfin -EOF -} - -# ============================================ -# Run -# ============================================ - -function cmd::preset::run() { - local subcmd="${1:-help}" - shift || true - - case "$subcmd" in - list|ls) cmd::preset::list "$@" ;; - add|new|create) cmd::preset::add "$@" ;; - remove|rm|del|delete) cmd::preset::remove "$@" ;; - help) cmd::preset::help ;; - *) - log::error "Unknown subcommand: '${subcmd}'" - cmd::preset::help - return 1 - ;; - esac -} - -# ============================================ -# List -# ============================================ - -function cmd::preset::list() { - local dir - dir="$(ctx::presets)" - - local presets=("${dir}"/*.preset) - if [[ ! -f "${presets[0]}" ]]; then - log::wg_preset "No presets configured" - return 0 - fi - - log::section "Available Presets" - - printf "\n %-25s %-40s %s\n" "NAME" "DESCRIPTION" "RULES" - printf " %s\n" "$(printf '─%.0s' {1..75})" - - for preset_file in "${dir}"/*.preset; do - [[ -f "$preset_file" ]] || continue - - # Reset vars before sourcing - local PRESET_NAME="" PRESET_DESC="" - local BLOCK_IPS="" BLOCK_SUBNETS="" BLOCK_PORTS="" - - source "$preset_file" - - local rules="" - [[ -n "$BLOCK_IPS" ]] && rules+="IPs:$(echo "$BLOCK_IPS" | wc -w) " - [[ -n "$BLOCK_SUBNETS" ]] && rules+="Subnets:$(echo "$BLOCK_SUBNETS" | wc -w) " - [[ -n "$BLOCK_PORTS" ]] && rules+="Ports:$(echo "$BLOCK_PORTS" | wc -w)" - - printf " %-25s %-40s %s\n" \ - "$PRESET_NAME" \ - "${PRESET_DESC:-—}" \ - "${rules:-—}" - done - - printf "\n" -} - -# ============================================ -# Add -# ============================================ - -function cmd::preset::add() { - local name="" - local desc="" - local block_ips=() - local block_subnets=() - local block_ports=() - - while [[ $# -gt 0 ]]; do - case "$1" in - --name) name="$2"; shift 2 ;; - --desc) desc="$2"; shift 2 ;; - --block-ip) block_ips+=("$2"); shift 2 ;; - --block-subnet) block_subnets+=("$2"); shift 2 ;; - --block-port) block_ports+=("$2"); shift 2 ;; - --help) cmd::preset::help; return ;; - *) - log::error "Unknown flag: $1" - cmd::preset::help - return 1 - ;; - esac - done - - if [[ -z "$name" ]]; then - log::error "Missing required flag: --name" - return 1 - fi - - if [[ ${#block_ips[@]} -eq 0 && ${#block_subnets[@]} -eq 0 && ${#block_ports[@]} -eq 0 ]]; then - log::error "At least one of --block-ip, --block-subnet, or --block-port is required" - return 1 - fi - - local preset_file - preset_file="$(ctx::preset::path "${name}.preset")" - - if [[ -f "$preset_file" ]]; then - log::error "Preset already exists: ${name}" - return 1 - fi - - cat > "$preset_file" </dev/null || true diff --git a/commands/shell.command.sh b/commands/shell.command.sh index f0e20c4..c2c9433 100644 --- a/commands/shell.command.sh +++ b/commands/shell.command.sh @@ -19,7 +19,7 @@ function cmd::shell::_is_wgctl_command() { local known=( list add remove rm inspect block unblock rule group audit logs watch fw config qr - rename keys ip preset service shell help + rename keys ip service shell help ) local c for c in "${known[@]}"; do diff --git a/commands/test.command.sh b/commands/test.command.sh index 09bccf9..808d7ee 100644 --- a/commands/test.command.sh +++ b/commands/test.command.sh @@ -138,13 +138,12 @@ function cmd::test::run_function() { function cmd::test::section_list() { test::section "List" cmd::test::run_cmd "list" "WireGuard Clients" list - cmd::test::run_cmd "list --online" "STATUS" list --online - cmd::test::run_cmd "list --offline" "STATUS" list --offline - cmd::test::run_cmd "list --blocked" "STATUS" list --blocked - cmd::test::run_cmd "list --type phone" "phone" list --type phone - cmd::test::run_cmd "list --type guest" "guest" list --type guest - # TODO: Fix detailed, hangs -# cmd::test::run_cmd "list --detailed" "Client:" list --detailed + cmd::test::run_cmd "list --online" "" list --online + cmd::test::run_cmd "list --offline" "" list --offline + cmd::test::run_cmd "list --blocked" "" list --blocked + cmd::test::run_cmd "list --type phone" "" list --type phone + cmd::test::run_cmd "list --type guest" "" list --type guest + cmd::test::run_cmd "list --detailed" "Client:" list --detailed cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno } diff --git a/commands/unblock.command.sh b/commands/unblock.command.sh index 3f67c6c..1ec47b9 100644 --- a/commands/unblock.command.sh +++ b/commands/unblock.command.sh @@ -27,16 +27,20 @@ Usage: wgctl unblock --name [options] Remove block rules for a client. Options: - --name Client name (e.g. phone-nuno) - --ip Unblock specific IP (repeatable) - --subnet Unblock specific subnet (repeatable) - --port Unblock specific port (repeatable) - --all Remove all block rules for this client + --name Client name (e.g. phone-nuno) + --type Device type (optional, combines with --name) + --ip Unblock specific IP (repeatable) + --subnet Unblock specific subnet (repeatable) + --port Unblock specific port (repeatable) + --all Remove all block rules for this client + --force Skip confirmation prompt + --quiet Suppress output (used by group unblock) Examples: - wgctl unblock --name phone-nuno --all + wgctl unblock --name phone-nuno + wgctl unblock --name nuno --type phone wgctl unblock --name phone-nuno --ip 10.0.0.210 - wgctl unban --name phone-nuno --all + wgctl unban --name phone-nuno EOF } @@ -127,7 +131,6 @@ function cmd::unblock::_unblock_all() { local client_ip="${2:-}" local quiet="${3:-false}" - log::debug "_unblock_all: name=$name ip=$client_ip" fw::unblock_all "$client_ip" fw::remove_block_file "$name" monitor::unwatch_client "$name" @@ -135,7 +138,6 @@ function cmd::unblock::_unblock_all() { if ! peers::exists_in_server "$name"; then local public_key public_key=$(keys::public "$name") || return 1 - log::debug "_unblock_all: adding to server pub=$public_key" peers::add_to_server "$name" "$public_key" "$client_ip" peers::reload fi diff --git a/commands/watch.command.sh b/commands/watch.command.sh index ce8c675..3f74f21 100644 --- a/commands/watch.command.sh +++ b/commands/watch.command.sh @@ -158,72 +158,65 @@ function cmd::watch::tail_events() { declare -A _WATCH_LAST_ATTEMPT=() tail -f "$(ctx::root)/daemon/events.log" 2>/dev/null | while IFS= read -r line; do - [[ -z "$line" ]] && continue + [[ -z "$line" ]] && continue - local event_data - event_data=$(python3 -c " -import json, sys -try: - e = json.loads('${line//\'/\'\\\'\'}') - print(e.get('timestamp',''), e.get('client',''), e.get('endpoint',''), e.get('event','')) -except: - pass -" 2>/dev/null) + local event_data + event_data=$(json::parse_event "$line") - [[ -z "$event_data" ]] && continue + [[ -z "$event_data" ]] && continue - if $restricted_only; then - local conf - conf="$(ctx::clients)/${client}.conf" - [[ -f "$conf" ]] || continue - cmd::list::is_restricted "$client" || continue - fi + local ts client endpoint event + IFS="|" read -r ts client endpoint event <<< "$event_data" - local ts client endpoint event - read -r ts client endpoint event <<< "$event_data" + if $restricted_only; then + local conf + conf="$(ctx::clients)/${client}.conf" + [[ -f "$conf" ]] || continue + cmd::list::is_restricted "$client" || continue + fi - # Apply filters - [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue + # Apply filters + [[ -n "$filter_name" && "$client" != "$filter_name" ]] && continue - if [[ -n "$filter_type" ]]; then - local conf - conf="$(ctx::clients)/${client}.conf" - [[ -f "$conf" ]] || continue - local ip - ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) - local subnet - subnet=$(config::subnet_for "$filter_type") - string::starts_with "$ip" "$subnet" || continue - fi + if [[ -n "$filter_type" ]]; then + local conf + conf="$(ctx::clients)/${client}.conf" + [[ -f "$conf" ]] || continue + local ip + ip=$(grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1) + local subnet + subnet=$(config::subnet_for "$filter_type") + string::starts_with "$ip" "$subnet" || continue + fi - # Filter by status - if $allowed_only && [[ "$event" != "handshake" ]]; then - continue - fi + # Filter by status + if $allowed_only && [[ "$event" != "handshake" ]]; then + continue + fi - if $restricted_only; then - local conf - conf="$(ctx::clients)/${client}.conf" - [[ -f "$conf" ]] || continue - cmd::list::is_restricted "$client" || continue - fi + if $restricted_only; then + local conf + conf="$(ctx::clients)/${client}.conf" + [[ -f "$conf" ]] || continue + cmd::list::is_restricted "$client" || continue + fi - local formatted_ts - formatted_ts=$(fmt::datetime_iso "$ts") + local formatted_ts + formatted_ts=$(fmt::datetime_iso "$ts") - # Before printing the event - local now - now=$(date +%s) - local safe_client="${client//[-.]/_}" - local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}" - local diff=$(( now - last )) - if (( diff < 30 )); then - continue - fi - _WATCH_LAST_ATTEMPT[$safe_client]="$now" + # Before printing the event + local now + now=$(date +%s) + local safe_client="${client//[-.]/_}" + local last="${_WATCH_LAST_ATTEMPT[$safe_client]:-0}" + local diff=$(( now - last )) + if (( diff < 30 )); then + continue + fi + _WATCH_LAST_ATTEMPT[$safe_client]="$now" - cmd::watch::format_event \ - "$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked" + cmd::watch::format_event \ + "$formatted_ts" "$client" "${endpoint:-—}" "$event" "blocked" done } diff --git a/core/context.sh b/core/context.sh index c245762..08efca0 100644 --- a/core/context.sh +++ b/core/context.sh @@ -9,7 +9,6 @@ _CTX_WG="/etc/wireguard" _CTX_CORE="${_CTX_ROOT}/core" _CTX_MODULES="${_CTX_ROOT}/modules" _CTX_COMMANDS="${_CTX_ROOT}/commands" -_CTX_PRESETS="${_CTX_ROOT}/presets" _CTX_CLIENTS="${_CTX_WG}/clients" _CTX_DATA="${_CTX_WG}/.wgctl" @@ -29,7 +28,6 @@ function ctx::root() { echo "$_CTX_ROOT"; } function ctx::core() { echo "$_CTX_CORE"; } function ctx::modules() { echo "$_CTX_MODULES"; } function ctx::commands() { echo "$_CTX_COMMANDS"; } -function ctx::presets() { echo "$_CTX_PRESETS"; } function ctx::blocks() { echo "$_CTX_BLOCKS"; } function ctx::groups() { echo "$_CTX_GROUPS"; } function ctx::rules() { echo "$_CTX_RULES"; } @@ -54,11 +52,6 @@ function ctx::client::path() { echo "$_CTX_CLIENTS/$*" } -function ctx::preset::path() { - local IFS="/" - echo "$_CTX_PRESETS/$*" -} - function ctx::meta::path() { local IFS="/" echo "$_CTX_META/$*" diff --git a/core/json.sh b/core/json.sh index ef08cd0..b343f93 100644 --- a/core/json.sh +++ b/core/json.sh @@ -27,4 +27,8 @@ function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@" name map ip_to_name = {} @@ -424,6 +425,9 @@ def follow_logs(fw_file, wg_file, filter_ip, filter_type, clients_dir): ip = ip_to_name.get(filter_ip, '') if client != ip and client != filter_ip: continue + + if peer_filter and client not in peer_filter: + continue if filter_type and not client.startswith(filter_type + '-'): continue ts = e.get('timestamp', '')[:16].replace('T', ' ') @@ -638,34 +642,83 @@ def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips, block_port with open(file, 'w') as f: json.dump(rule, f, indent=2) -commands = { - 'get': lambda args: get(args[0], args[1]), - 'set': lambda args: set_key(args[0], args[1], args[2]), - 'delete': lambda args: delete_key(args[0], args[1]), - 'append': lambda args: append(args[0], args[1], args[2]), - 'remove': lambda args: remove_value(args[0], args[1], args[2]), - 'cat': lambda args: cat(args[0]), - 'has_key': lambda args: has_key(args[0], args[1]), - 'filter_values': lambda args: filter_values(args[0], args[1], args[2]), - 'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]), - 'events_for': lambda args: events_for(args[0], args[1], args[2]), - 'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]), - 'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]), - 'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]), - 'format_wg_event': lambda args: format_wg_event(sys.stdin.read()), - 'remove_events': lambda args: remove_events(args[0], args[1]), - 'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4]), - 'count': lambda args: count(args[0], args[1]), - 'audit_fw_counts': lambda args: audit_fw_counts(args[0]), - 'peer_group_map': lambda args: peer_group_map(args[0]), - 'peer_groups': lambda args: peer_groups(args[0], args[1]), - 'peer_data': lambda args: peer_data(args[0], args[1], args[2]), - 'iso_to_ts': lambda args: iso_to_ts(args[0]), - 'rule_list_data': lambda args: rule_list_data(args[0], args[1]), - 'group_list_data': lambda args: group_list_data(args[0], args[1]), - 'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]), - 'create_rule': lambda args: create_rule(args[0], args[1], args[2], args[3], args[4], args[5], args[6]), +def cleanup_config(config_file): + """Normalize blank lines in WireGuard config""" + import re + try: + with open(config_file) as f: + config = f.read() + config = re.sub(r'\n{3,}', '\n\n', config) + config = config.rstrip('\n') + '\n' + with open(config_file, 'w') as f: + f.write(config) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) +def remove_peer_block(config_file, name): + """Remove a peer block from WireGuard config by name""" + import re + try: + with open(config_file) as f: + config = f.read() + pattern = r'\n\[Peer\]\n# ' + re.escape(name) + r'\n[^\n]+\n[^\n]+\n' + result = re.sub(pattern, '\n', config) + with open(config_file, 'w') as f: + f.write(result) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + +def create_group(file, name, desc): + """Create a new group JSON file""" + try: + group = {'name': name, 'desc': desc, 'peers': []} + with open(file, 'w') as f: + json.dump(group, f, indent=2) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + +def parse_event(line): + """Parse a single JSON event line""" + try: + e = json.loads(line) + print(f"{e.get('timestamp','')}|{e.get('client','')}|{e.get('endpoint','')}|{e.get('event','')}") + except: + pass + +commands = { + 'get': lambda args: get(args[0], args[1]), + 'set': lambda args: set_key(args[0], args[1], args[2]), + 'delete': lambda args: delete_key(args[0], args[1]), + 'append': lambda args: append(args[0], args[1], args[2]), + 'remove': lambda args: remove_value(args[0], args[1], args[2]), + 'cat': lambda args: cat(args[0]), + 'has_key': lambda args: has_key(args[0], args[1]), + 'filter_values': lambda args: filter_values(args[0], args[1], args[2]), + 'last_event': lambda args: last_event(args[0], args[1], args[2], args[3]), + 'events_for': lambda args: events_for(args[0], args[1], args[2]), + 'fw_events': lambda args: fw_events(args[0], args[1], args[2], args[3], args[4]), + 'wg_events': lambda args: wg_events(args[0], args[1], args[2], args[3]), + 'format_fw_event': lambda args: format_fw_event(sys.stdin.read(), args[0]), + 'format_wg_event': lambda args: format_wg_event(sys.stdin.read()), + 'remove_events': lambda args: remove_events(args[0], args[1]), + 'follow_logs': lambda args: follow_logs(args[0], args[1], args[2], args[3], args[4], args[5]), + 'count': lambda args: count(args[0], args[1]), + 'audit_fw_counts': lambda args: audit_fw_counts(args[0]), + 'peer_group_map': lambda args: peer_group_map(args[0]), + 'peer_groups': lambda args: peer_groups(args[0], args[1]), + 'peer_data': lambda args: peer_data(args[0], args[1], args[2]), + 'iso_to_ts': lambda args: iso_to_ts(args[0]), + 'rule_list_data': lambda args: rule_list_data(args[0], args[1]), + 'group_list_data': lambda args: group_list_data(args[0], args[1]), + 'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]), + 'create_rule': lambda args: create_rule(args[0], args[1], args[2], args[3], args[4], args[5], args[6]), + 'cleanup_config': lambda args: cleanup_config(args[0]), + 'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]), + 'create_group': lambda args: create_group(args[0], args[1], args[2]), + 'parse_event': lambda args: parse_event(args[0]), } if __name__ == '__main__': diff --git a/daemon/endpoint_cache.json b/daemon/endpoint_cache.json index 9dac522..13a97e2 100644 --- a/daemon/endpoint_cache.json +++ b/daemon/endpoint_cache.json @@ -1,6 +1,6 @@ { "phone-fred": "94.63.0.129", - "phone-helena": "148.69.37.26", + "phone-helena": "148.69.44.173", "phone-nuno": "94.63.0.129", "tablet-nuno": "148.69.202.5", "guest-zephyr": "5.13.82.5", diff --git a/modules/config.module.sh b/modules/config.module.sh index 7dcddd9..5732674 100644 --- a/modules/config.module.sh +++ b/modules/config.module.sh @@ -53,7 +53,6 @@ function config::load() { WG_LAN) _WG_LAN="$value" ;; # Add debug temporarily to config::load: DATE_FORMAT) - log::debug "config: setting date format to $value" _FMT_DATE_FORMAT="$value" fmt::set_date_format "$value" ;; diff --git a/modules/firewall.module.sh b/modules/firewall.module.sh index eb49362..baf4dc4 100644 --- a/modules/firewall.module.sh +++ b/modules/firewall.module.sh @@ -88,7 +88,7 @@ function fw::unallow_port() { } function fw::flush_peer() { - local client_ip="$1" + local client_ip="${1:?client_ip required}" log::debug "flush_peer: starting for $client_ip" # Collect line numbers into array @@ -205,47 +205,3 @@ function fw::restore_blocks() { log::debug "Restored block rules for: ${name}" done } - -# ============================================ -# Preset Application -# ============================================ - -function fw::apply_preset() { - local name="$1" - local client_ip="$2" - local preset_file - preset_file="$(ctx::preset::path "${name}.preset")" - - if [[ ! -f "$preset_file" ]]; then - log::error "Preset not found: ${name}" - return 1 - fi - - source "$preset_file" - - if [[ -n "${BLOCK_IPS:-}" ]]; then - for ip in $BLOCK_IPS; do - fw::block_ip "$client_ip" "$ip" - fw::save_block "$client_ip" "$client_ip" "$ip" - done - fi - - if [[ -n "${BLOCK_SUBNETS:-}" ]]; then - for subnet in $BLOCK_SUBNETS; do - fw::block_subnet "$client_ip" "$subnet" - fw::save_block "$client_ip" "$client_ip" "$subnet" - done - fi - - if [[ -n "${BLOCK_PORTS:-}" ]]; then - for entry in $BLOCK_PORTS; do - local target port proto - IFS=":" read -r target port proto <<< "$entry" - proto="${proto:-tcp}" - fw::block_port "$client_ip" "$target" "$port" "$proto" - fw::save_block "$name" "$client_ip" "$target" "$port" "$proto" - done - fi - - log::debug "Applied preset '${name}' to: ${client_ip}" -} \ No newline at end of file diff --git a/modules/group.module.sh b/modules/group.module.sh index a6759a9..5290aee 100644 --- a/modules/group.module.sh +++ b/modules/group.module.sh @@ -50,4 +50,37 @@ function group::peer_groups() { echo "$group_name" fi done < <(group::all) +} + +function group::remove_peer_from_all() { + local peer_name="${1:?peer_name required}" + while IFS= read -r group_name; do + group::remove_peer "$group_name" "$peer_name" + done < <(json::peer_groups "$(ctx::groups)" "$peer_name") +} + +function group::each_peer() { + local name="${1:?name required}" + local callback="${2:?callback required}" + shift 2 + # $@ = extra args passed to callback + + 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 ]] && return 0 + + for peer_name in "${filtered[@]}"; do + "$callback" "$peer_name" "$@" + done +} + +function group::_peer_exists_check() { + local peer_name="${1:-}" + peers::require_exists "$peer_name" > /dev/null 2>&1 } \ No newline at end of file diff --git a/modules/peers.module.sh b/modules/peers.module.sh index 52a49ed..0c63c8d 100644 --- a/modules/peers.module.sh +++ b/modules/peers.module.sh @@ -60,27 +60,14 @@ function peers::remove_client_config() { # ============================================ function peers::cleanup_config() { - local config - config=$(config::config_file) - - python3 -c " -import re -config = open('${config}').read() - -# Normalize multiple blank lines to single blank line -config = re.sub(r'\n{3,}', '\n\n', config) - -# Ensure file ends with single newline -config = config.rstrip('\n') + '\n' - -open('${config}', 'w').write(config) -" + json::cleanup_config "$(config::config_file)" } + function peers::add_to_server() { - local name="$1" - local public_key="$2" - local ip="$3" + local name="${1:?name required}" + local public_key="${2:?public_key required}" + local ip="${3:?ip required}" local config config=$(config::config_file) @@ -97,21 +84,12 @@ EOF } function peers::remove_block() { - local name="$1" - local config - config=$(config::config_file) - - python3 -c " -import re -config = open('${config}').read() -pattern = r'\n\[Peer\]\n# ${name}\n[^\n]+\n[^\n]+\n' -result = re.sub(pattern, '\n', config) -open('${config}', 'w').write(result) -" + local name="${1:?name required}" + json::remove_peer_block "$(config::config_file)" "$name" } function peers::remove_from_server() { - local name="$1" + local name="${1:?name required}" peers::remove_block "$name" peers::cleanup_config log::debug "Removed peer from server config: ${name}" @@ -183,8 +161,8 @@ function peers::exists_in_server() { } function peers::is_blocked() { - local name="$1" - ! peers::exists_in_server "$name" + local name="${1:-}" + peers::exists_in_server "$name" && return 1 || return 0 } # ============================================ @@ -248,7 +226,7 @@ function peers::with_rule() { function peers::get_ip() { local name="$1" grep "^Address" "$(ctx::clients)/${name}.conf" 2>/dev/null \ - | awk '{print $3}' | cut -d'/' -f1 + | awk '{print $3}' | cut -d'/' -f1 || true } function peers::find_by_ip() { diff --git a/modules/rule.module.sh b/modules/rule.module.sh index bf79556..8325f44 100644 --- a/modules/rule.module.sh +++ b/modules/rule.module.sh @@ -72,13 +72,11 @@ function rule::is_applied() { # ============================================ function rule::apply() { - local rule_name="$1" - local client_ip="$2" + local rule_name="${1:?rule_name required}" + local client_ip="${2:?client_ip required}" local peer_name="${3:-}" # optional, avoids find_by_ip call - log::debug "rule::apply ENTRY: rule=$rule_name ip=$client_ip peer=$peer_name" rule::require_exists "$rule_name" || return 1 - log::debug "rule::apply: exists check passed" # Use provided peer_name or look it up if [[ -z "$peer_name" ]]; then @@ -100,7 +98,6 @@ function rule::apply() { # Check if already applied local peer_name peer_name=$(peers::find_by_ip "$client_ip") - log::debug "rule::apply: find_by_ip($client_ip) = '$peer_name'" if [[ -n "$peer_name" ]]; then # Check if already applied via iptables if rule::is_applied "$rule_name" "$client_ip"; then @@ -140,10 +137,8 @@ function rule::apply() { done < <(rule::get "$rule_name" "allow_ports") # Persist rule assignment in meta - log::debug "rule::apply: peer_name=$peer_name ip=$client_ip" if [[ -n "$peer_name" ]]; then peers::set_meta "$peer_name" "rule" "$rule_name" - log::debug "rule::apply: set meta rule=$rule_name for $peer_name" fi local dns_redirect diff --git a/presets/guest.preset b/presets/guest.preset deleted file mode 100644 index 6dc5e0a..0000000 --- a/presets/guest.preset +++ /dev/null @@ -1,5 +0,0 @@ -PRESET_NAME="guest" -PRESET_DESC="Internet only, no LAN access" -BLOCK_IPS="" -BLOCK_SUBNETS="10.0.0.0/24" -BLOCK_PORTS="" diff --git a/presets/no-docker.preset b/presets/no-docker.preset deleted file mode 100644 index ab5c1f1..0000000 --- a/presets/no-docker.preset +++ /dev/null @@ -1,5 +0,0 @@ -PRESET_NAME="no-docker" -PRESET_DESC="Block access to Docker host" -BLOCK_IPS="10.0.0.210" -BLOCK_SUBNETS="" -BLOCK_PORTS="" diff --git a/presets/no-internet.preset b/presets/no-internet.preset deleted file mode 100644 index b81a0ec..0000000 --- a/presets/no-internet.preset +++ /dev/null @@ -1,5 +0,0 @@ -PRESET_NAME="no-internet" -PRESET_DESC="LAN access only, no internet" -BLOCK_IPS="" -BLOCK_SUBNETS="0.0.0.0/0" -BLOCK_PORTS="" diff --git a/presets/no-proxmox.preset b/presets/no-proxmox.preset deleted file mode 100644 index 4d33302..0000000 --- a/presets/no-proxmox.preset +++ /dev/null @@ -1,5 +0,0 @@ -PRESET_NAME="no-proxmox" -PRESET_DESC="Block access to Proxmox" -BLOCK_IPS="10.0.0.100" -BLOCK_SUBNETS="" -BLOCK_PORTS="" diff --git a/wgctl b/wgctl index 5665faf..19559c7 100755 --- a/wgctl +++ b/wgctl @@ -120,11 +120,6 @@ Service Commands: enable Enable WireGuard on boot disable Disable WireGuard on boot -Preset Commands: - preset list List available presets - preset add Add a new preset - preset remove Remove a preset - Run 'wgctl --help' for command-specific help. EOF }