From 51e3443357b80e3fbc8bb16e2d74cf35c37421ce Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Tue, 12 May 2026 04:27:47 +0000 Subject: [PATCH] refactor: rule::show new layout, assign fix, unblock helpers, test improvements --- commands/block.command.sh | 19 +-- commands/list.command.sh | 4 +- commands/rule.command.sh | 304 ++++++++++++++++++++++++++---------- commands/test.command.sh | 52 +++++- commands/unblock.command.sh | 56 +++---- core/json.sh | 3 +- core/json_helper.py | 15 ++ daemon/endpoint_cache.json | 2 +- modules/rule.module.sh | 2 + wgctl | 2 +- 10 files changed, 312 insertions(+), 147 deletions(-) diff --git a/commands/block.command.sh b/commands/block.command.sh index a89bb5f..9a58412 100644 --- a/commands/block.command.sh +++ b/commands/block.command.sh @@ -41,23 +41,6 @@ Examples: EOF } -# ============================================ -# Helpers -# ============================================ - -function cmd::block::get_client_ip() { - local name="$1" - local conf - conf="$(ctx::clients)/${name}.conf" - - if [[ ! -f "$conf" ]]; then - log::error "Client not found: ${name}" - return 1 - fi - - grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1 -} - # ============================================ # Block Run # ============================================ @@ -112,7 +95,7 @@ function cmd::block::run() { endpoint=$(cmd::block::_get_endpoint "$name" "$public_key") local client_ip - client_ip=$(cmd::block::get_client_ip "$name") || return 1 + client_ip=$(peers::get_ip "$name") || return 1 # $quiet || log::section "Blocking client: ${name} (${client_ip})" diff --git a/commands/list.command.sh b/commands/list.command.sh index dab49c2..d991804 100644 --- a/commands/list.command.sh +++ b/commands/list.command.sh @@ -119,9 +119,9 @@ function cmd::list::_format_last_seen() { local handshake_ts="${6:-0}" if [[ "$is_blocked" == "true" ]]; then - if [[ -n "$last_ts" ]]; then + if [[ -n "$last_ts" && "$last_ts" != "0" && "$last_ts" != "null" ]]; then local formatted - formatted=$(fmt::datetime "$last_ts") + formatted=$(fmt::datetime_iso "$last_ts") echo "${formatted} (dropped)" else echo "—" diff --git a/commands/rule.command.sh b/commands/rule.command.sh index fb2824f..dd016d3 100644 --- a/commands/rule.command.sh +++ b/commands/rule.command.sh @@ -18,6 +18,7 @@ function cmd::rule::on_load() { flag::register --peer flag::register --peers flag::register --dns-redirect + flag::register --color } # ============================================ @@ -87,6 +88,7 @@ function cmd::rule::run() { assign) cmd::rule::assign "$@" ;; unassign) cmd::rule::unassign "$@" ;; migrate) cmd::rule::migrate "$@" ;; + reapply) cmd::rule::reapply "$@" ;; help) cmd::rule::help ;; *) log::error "Unknown subcommand: '${subcmd}'" @@ -131,87 +133,71 @@ function cmd::rule::list() { # ============================================ function cmd::rule::show() { - local name="" show_peers=false + local name="" show_peers=false color=false while [[ $# -gt 0 ]]; do case "$1" in --name) name="$2"; shift 2 ;; --peers) show_peers=true; shift ;; + --color) color=true; shift ;; --help) cmd::rule::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done - if [[ -z "$name" ]]; then - log::error "Missing required flag: --name" - return 1 - fi - + [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 rule::require_exists "$name" || return 1 local rule_file rule_file="$(ctx::rule::path "${name}.rule")" + # Precompute peers before any operations + local peer_list=() + mapfile -t peer_list < <(peers::with_rule "$name") + local peer_count=${#peer_list[@]} + log::section "Rule: ${name}" local desc dns_redirect desc=$(json::get "$rule_file" "desc") dns_redirect=$(json::get "$rule_file" "dns_redirect") - printf "\n %-20s %s\n" "Description:" "${desc:-—}" - printf " %-20s %s\n" "DNS Redirect:" "${dns_redirect:-false}" + printf "\n" + ui::row "Description" "${desc:-—}" + ui::row "DNS Redirect" "${dns_redirect:-false}" - # Allow ports - local allow_ports + # Load all entries + local allow_ports allow_ips block_ips block_ports allow_ports=$(json::get "$rule_file" "allow_ports") - if [[ -n "$allow_ports" ]]; then - printf "\n Allow Ports:\n" - while IFS= read -r entry; do - printf " + %s\n" "$entry" - done <<< "$allow_ports" - fi - - # Allow IPs - local allow_ips allow_ips=$(json::get "$rule_file" "allow_ips") - if [[ -n "$allow_ips" ]]; then - printf "\n Allow IPs:\n" - while IFS= read -r ip; do - printf " + %s\n" "$ip" - done <<< "$allow_ips" - fi - - # Block IPs - local block_ips block_ips=$(json::get "$rule_file" "block_ips") - if [[ -n "$block_ips" ]]; then - printf "\n Block IPs:\n" - while IFS= read -r ip; do - printf " - %s\n" "$ip" - done <<< "$block_ips" - fi - - # Block ports - local block_ports block_ports=$(json::get "$rule_file" "block_ports") - if [[ -n "$block_ports" ]]; then - printf "\n Block Ports:\n" - while IFS= read -r entry; do - printf " - %s\n" "$entry" - done <<< "$block_ports" + + # Allow section + if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then + cmd::rule::_show_section "Allow" "green" "$color" + cmd::rule::_show_entries "Ports" "+" "$allow_ports" "$color" "green" + cmd::rule::_show_entries "IPs" "+" "$allow_ips" "$color" "green" fi - # Precompute peers before any other operations - local peer_list=() - mapfile -t peer_list < <(peers::with_rule "$name") - local peer_count=${#peer_list[@]} + # Block section + if [[ -n "$block_ips" || -n "$block_ports" ]]; then + cmd::rule::_show_section "Block" "red" "$color" + cmd::rule::_show_entries "IPs" "-" "$block_ips" "$color" "red" + cmd::rule::_show_entries "Ports" "-" "$block_ports" "$color" "red" + fi - # Peer count — always shown - printf "\n %-20s %s\n" "Assigned Peers:" "$peer_count" - printf " %s\n" "$(printf '─%.0s' {1..40})" + if [[ -z "$allow_ports" && -z "$allow_ips" && -z "$block_ips" && -z "$block_ports" ]]; then + printf "\n" + ui::row "Access" "full (no restrictions)" + fi + + # Peers section + cmd::rule::_show_section "Peers" "white" false + ui::row "Assigned" "$peer_count" - # Peer details — only with --peers flag if $show_peers && [[ $peer_count -gt 0 ]]; then + printf "\n" for peer_name in "${peer_list[@]}"; do local ip ip=$(peers::get_ip "$peer_name") @@ -222,6 +208,97 @@ function cmd::rule::show() { printf "\n" } +# function cmd::rule::show() { +# local name="" show_peers=false + +# while [[ $# -gt 0 ]]; do +# case "$1" in +# --name) name="$2"; shift 2 ;; +# --peers) show_peers=true; shift ;; +# --help) cmd::rule::help; return ;; +# *) log::error "Unknown flag: $1"; return 1 ;; +# esac +# done + +# if [[ -z "$name" ]]; then +# log::error "Missing required flag: --name" +# return 1 +# fi + +# rule::require_exists "$name" || return 1 + +# local rule_file +# rule_file="$(ctx::rule::path "${name}.rule")" + +# log::section "Rule: ${name}" + +# local desc dns_redirect +# desc=$(json::get "$rule_file" "desc") +# dns_redirect=$(json::get "$rule_file" "dns_redirect") + +# printf "\n" +# ui::row "Description" "${desc:-—}" +# ui::row "DNS Redirect" "${dns_redirect:-false}" + +# # Allow ports +# local allow_ports +# allow_ports=$(json::get "$rule_file" "allow_ports") +# if [[ -n "$allow_ports" ]]; then +# printf "\n Allow Ports:\n" +# ui::print_list "+" "$allow_ports" +# fi + +# # Allow IPs +# local allow_ips +# allow_ips=$(json::get "$rule_file" "allow_ips") +# if [[ -n "$allow_ips" ]]; then +# printf "\n Allow IPs:\n" +# while IFS= read -r ip; do +# printf " + %s\n" "$ip" +# done <<< "$allow_ips" +# fi + +# # Block IPs +# local block_ips +# block_ips=$(json::get "$rule_file" "block_ips") +# if [[ -n "$block_ips" ]]; then +# printf "\n Block IPs:\n" +# while IFS= read -r ip; do +# printf " - %s\n" "$ip" +# done <<< "$block_ips" +# fi + +# # Block ports +# local block_ports +# block_ports=$(json::get "$rule_file" "block_ports") +# if [[ -n "$block_ports" ]]; then +# printf "\n Block Ports:\n" +# while IFS= read -r entry; do +# printf " - %s\n" "$entry" +# done <<< "$block_ports" +# fi + +# # Precompute peers before any other operations +# local peer_list=() +# mapfile -t peer_list < <(peers::with_rule "$name") +# local peer_count=${#peer_list[@]} + +# # Peer count — always shown +# printf "\n %-20s %s\n" "Assigned Peers:" "$peer_count" +# printf " %s\n" "$(printf '─%.0s' {1..40})" + +# # Peer details — only with --peers flag +# if $show_peers && [[ $peer_count -gt 0 ]]; then +# for peer_name in "${peer_list[@]}"; do +# local ip +# ip=$(peers::get_ip "$peer_name") +# printf " %-28s %s\n" "$peer_name" "$ip" +# done +# fi + +# printf "\n" +# } + # ============================================ # Add # ============================================ @@ -260,20 +337,15 @@ function cmd::rule::add() { local rule_file rule_file="$(ctx::rule::path "${name}.rule")" - # Build JSON using json_helper - python3 -c " -import json -rule = { - 'name': '${name}', - 'desc': '${desc}', - 'dns_redirect': $(${dns_redirect} && echo 'true' || echo 'false'), - 'allow_ips': [$(printf '"%s",' "${allow_ips[@]}" | sed 's/,$//')] , - 'block_ips': [$(printf '"%s",' "${block_ips[@]}" | sed 's/,$//')], - 'block_ports': [$(printf '"%s",' "${block_ports[@]}" | sed 's/,$//')] -} -with open('${rule_file}', 'w') as f: - json.dump(rule, f, indent=2) -" + local allow_str block_str port_str + + allow_str=$(IFS=','; echo "${allow_ips[*]}") + block_str=$(IFS=','; echo "${block_ips[*]}") + port_str=$(IFS=','; echo "${block_ports[*]}") + + json::create_rule "$rule_file" "$name" "$desc" \ + "$($dns_redirect && echo true || echo false)" \ + "$allow_str" "$block_str" "$port_str" || return 1 log::wg_success "Rule created: ${name}" } @@ -369,19 +441,21 @@ function cmd::rule::remove() { rule::require_exists "$name" || return 1 # Check for assigned peers - local peer_count - peer_count=$(peers::with_rule "$name" | grep -c . || echo 0) + + local peer_list=() + mapfile -t peer_list < <(peers::with_rule "$name") + local peer_count=${#peer_list[@]} + if [[ "$peer_count" -gt 0 ]]; then log::error "Rule '${name}' is assigned to ${peer_count} peer(s) — unassign first or use --force" - if ! $force; then - return 1 - fi + $force || return 1 + # Force: unassign from all peers - while IFS= read -r peer; do + for peer in "${peer_list[@]}"; do local ip ip=$(peers::get_ip "$peer") rule::unapply "$name" "$ip" - done < <(peers::with_rule "$name") + done fi rm -f "$(ctx::rule::path "${name}.rule")" @@ -394,7 +468,6 @@ function cmd::rule::remove() { function cmd::rule::assign() { local name="" peer="" type="" - while [[ $# -gt 0 ]]; do case "$1" in --name) name="$2"; shift 2 ;; @@ -421,15 +494,19 @@ function cmd::rule::assign() { # Unapply existing rule first if any local existing_rule existing_rule=$(peers::get_meta "$peer" "rule") - if [[ -n "$existing_rule" ]]; then - local ip - ip=$(peers::get_ip "$peer") + + local ip + ip=$(peers::get_ip "$peer") + log::debug "rule::assign: peer=$peer ip=$ip" + [[ -z "$ip" ]] && log::error "Could not resolve IP for: $peer" && return 1 + + log::debug "assign: peer=$peer ip=$ip clients=$(ctx::clients)" + + if [[ -n "$existing_rule" && "$existing_rule" != "$name" ]]; then rule::unapply "$existing_rule" "$ip" log::wg "Removed existing rule '${existing_rule}' from: ${peer}" fi - local ip - ip=$(peers::get_ip "$peer") rule::apply "$name" "$ip" log::wg_success "Assigned rule '${name}' to: ${peer}" @@ -502,21 +579,76 @@ function cmd::rule::migrate() { local count=0 local lines mapfile -t lines < "$tmp" - echo "DEBUG: lines count=${#lines[@]}" -for line in "${lines[@]}"; do - echo "DEBUG: processing line=$line" + + for line in "${lines[@]}"; do IFS=" " read -r peer_name default_rule ip <<< "$line" rule::apply "$default_rule" "$ip" "$peer_name" &2 + # Block/unblock cmd::test::run_cmd "block peer" "blocked" \ block --name phone-testunit @@ -226,10 +230,13 @@ function cmd::test::section_destructive() { unblock --name phone-testunit # Rule assign/unassign - cmd::test::run_cmd "rule assign" "Assigned" \ - rule assign --name admin --peer phone-testunit - cmd::test::run_cmd "rule unassign" "Unassigned" \ - rule unassign --peer phone-testunit + cmd::test::run_cmd "rule assign" "Assigned" \ + rule assign --name user --peer phone-testunit + cmd::test::run_cmd "rule unassign" "Unassigned" \ + rule unassign --peer phone-testunit + # Re-assign user rule (default) for cleanup + /usr/local/bin/wgctl rule assign --name user --peer phone-testunit \ + > /dev/null 2>&1 || true # Group operations cmd::test::run_cmd "group add" "created" \ @@ -318,10 +325,43 @@ function cmd::test::fn_rename() { rename --name phone-testunit2 # Cleanup - /usr/local/bin/wgctl remove --name phone-testunit2 --force \ + "$WGCTL_BINARY" remove --name phone-testunit2 --force \ > /dev/null 2>&1 || true } +function cmd::test::fn_unblock() { + test::section "cmd::unblock::run" + + # Setup — add and block a peer + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true + "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1 + "$WGCTL_BINARY" block --name phone-testunit > /dev/null 2>&1 + + # Tests + cmd::test::run_cmd "unblock peer" "unblocked" unblock --name phone-testunit + cmd::test::run_cmd "unblock not blocked" "not blocked" unblock --name phone-testunit + cmd::test::run_cmd_fails "unblock nonexistent" unblock --name nonexistent-peer + cmd::test::run_cmd_fails "unblock missing --name" unblock + + # Cleanup + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true +} + +function cmd::test::fn_rule_assign() { + test::section "cmd::rule::assign" + + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true + "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1 + + # Verify peer exists + echo "DEBUG peer exists: $("$WGCTL_BINARY" list | grep phone-testunit)" + + cmd::test::run_cmd "rule assign" "Assigned" \ + rule assign --name admin --peer phone-testunit + + "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true +} + # ============================================ # Run # ============================================ diff --git a/commands/unblock.command.sh b/commands/unblock.command.sh index e2662fe..3f67c6c 100644 --- a/commands/unblock.command.sh +++ b/commands/unblock.command.sh @@ -40,23 +40,6 @@ Examples: EOF } -# ============================================ -# Helpers -# ============================================ - -function cmd::unblock::get_client_ip() { - local name="$1" - local conf - conf="$(ctx::clients)/${name}.conf" - - if [[ ! -f "$conf" ]]; then - log::error "Client not found: ${name}" - return 1 - fi - - grep "^Address" "$conf" | awk '{print $3}' | cut -d'/' -f1 -} - # ============================================ # Unblock Run # ============================================ @@ -109,24 +92,12 @@ function cmd::unblock::run() { fi local client_ip - client_ip=$(cmd::unblock::get_client_ip "$name") || return 1 + client_ip=$(peers::get_ip "$name") || return 1 # $quiet || log::section "Unblocking client: ${name} (${client_ip})" - if $all; then - fw::unblock_all "$client_ip" - fw::remove_block_file "$name" - monitor::unwatch_client "$name" - - # Re-add peer to server if missing - if ! peers::exists_in_server "$name"; then - local public_key - public_key=$(keys::public "$name") || return 1 - peers::add_to_server "$name" "$public_key" "$client_ip" - peers::reload - fi - - $quiet || log::wg_success "${name} has been unblocked." + if $all; then + cmd::unblock::_unblock_all "$name" "$client_ip" "$quiet" return 0 fi @@ -150,3 +121,24 @@ function cmd::unblock::run() { $quiet || log::wg_success "Unblock rules applied for: ${name}" } + +function cmd::unblock::_unblock_all() { + local name="${1:-}" + 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" + + 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 + + $quiet || log::wg_success "${name} has been unblocked." +} \ No newline at end of file diff --git a/core/json.sh b/core/json.sh index 5778e1c..ef08cd0 100644 --- a/core/json.sh +++ b/core/json.sh @@ -26,4 +26,5 @@ function json::peer_data() { WGCTL_DATETIME_FMT="$FMT_DATETIME" python3 function json::iso_to_ts() { python3 "$JSON_HELPER" iso_to_ts "$@"