From c1d0a9ddd41170dd59d99981088da64b477a8ecd Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Fri, 15 May 2026 09:22:17 +0000 Subject: [PATCH] feat: block/unblock --service, fw::has_rule/has_block_rule/has_allow_rule, block::cleanup, restricted status in inspect, net service annotations --- commands/block.command.sh | 46 +++++++++++++++++++++++------- commands/unblock.command.sh | 57 +++++++++++++++++++++++++++++++++++-- core/json_helper.py | 13 +++++++++ modules/block.module.sh | 10 +++++++ modules/firewall.module.sh | 26 ++++++++++++++++- modules/rule.module.sh | 5 ++-- wgctl | 3 -- 7 files changed, 140 insertions(+), 20 deletions(-) diff --git a/commands/block.command.sh b/commands/block.command.sh index 4652dbb..f7590ef 100644 --- a/commands/block.command.sh +++ b/commands/block.command.sh @@ -57,6 +57,7 @@ function cmd::block::run() { local name="" type="" block_name="" local ips=() subnets=() ports=() services=() local quiet=false force=false + local changed=false while [[ $# -gt 0 ]]; do case "$1" in @@ -139,6 +140,26 @@ function cmd::block::run() { log::error "Service not found or has no ports: ${svc}" return 1 fi + + # Check if already blocked + local already_blocked=true + for resolved in "${resolved_lines[@]}"; do + if [[ "$resolved" == *:*:* ]]; then + local b_ip b_port b_proto + IFS=":" read -r b_ip b_port b_proto <<< "$resolved" + fw::has_block_rule "$client_ip" "$b_ip" "$b_proto" "$b_port" 2>/dev/null || \ + { already_blocked=false; break; } + else + fw::has_block_rule "$client_ip" "$resolved" 2>/dev/null || \ + { already_blocked=false; break; } + fi + done + + if $already_blocked; then + $quiet || log::wg_warning "${svc} is already blocked for ${name}" + continue + fi + for resolved in "${resolved_lines[@]}"; do if [[ "$resolved" == *:*:* ]]; then local b_ip b_port b_proto @@ -151,21 +172,26 @@ function cmd::block::run() { block::add_rule "$name" "$client_ip" "ip" "$svc" "$resolved" fi done + + changed=true + $quiet || log::wg_success "${svc} has been blocked for ${name}" done + [[ ${#ips[@]} -gt 0 || ${#ports[@]} -gt 0 || \ + ${#subnets[@]} -gt 0 ]] && changed=true + # Reapply in correct order: rule ACCEPT first, then peer DROP rules - local peer_rule - peer_rule=$(peers::get_meta "$name" "rule") - if [[ -n "$peer_rule" ]] && rule::exists "$peer_rule"; then - fw::flush_peer "$client_ip" - rule::apply "$peer_rule" "$client_ip" "$name" - block::restore_rules_for "$name" "$client_ip" - else - # No rule assigned — peer blocks are the only fw rules, order is fine - : # no-op + # Only reorder if rules were actually added + if $changed; then + local peer_rule + peer_rule=$(peers::get_meta "$name" "rule") + if [[ -n "$peer_rule" ]] && rule::exists "$peer_rule"; then + fw::flush_peer "$client_ip" + rule::apply "$peer_rule" "$client_ip" "$name" + block::restore_rules_for "$name" "$client_ip" + fi fi - $quiet || log::wg_success "${name} — access restricted" return 0 } diff --git a/commands/unblock.command.sh b/commands/unblock.command.sh index 3149b0f..c8e6097 100644 --- a/commands/unblock.command.sh +++ b/commands/unblock.command.sh @@ -14,6 +14,9 @@ function cmd::unblock::on_load() { flag::register --proto flag::register --subnet flag::register --all + + # System - NET Services + flag::register --service } # ============================================ @@ -54,9 +57,10 @@ function cmd::unblock::run() { local ips=() local subnets=() local ports=() + local services=() local all=false local quiet=false - + while [[ $# -gt 0 ]]; do case "$1" in --name) name="$2"; shift 2 ;; @@ -66,6 +70,7 @@ function cmd::unblock::run() { --quiet) quiet=true; shift ;; --subnet) subnets+=("$2"); shift 2 ;; --port) ports+=("$2"); shift 2 ;; + --service) services+=("$2"); shift 2 ;; --all) all=true; shift ;; --help) cmd::unblock::help; return ;; *) @@ -91,7 +96,7 @@ function cmd::unblock::run() { fi # Default to full unblock if no specific flags given - if [[ ${#ips[@]} -eq 0 && ${#subnets[@]} -eq 0 && ${#ports[@]} -eq 0 ]]; then + if [[ ${#ips[@]} -eq 0 && ${#subnets[@]} -eq 0 && ${#ports[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then all=true fi @@ -105,6 +110,49 @@ function cmd::unblock::run() { return 0 fi + # Unblock services + for svc in "${services[@]}"; do + local resolved_lines=() + mapfile -t resolved_lines < <(net::resolve "$svc" 2>/dev/null) + if [[ ${#resolved_lines[@]} -eq 0 ]]; then + log::error "Service not found: ${svc}" + return 1 + fi + + # Check if actually blocked + local is_blocked=false + for resolved in "${resolved_lines[@]}"; do + if [[ "$resolved" == *:*:* ]]; then + local b_ip b_port b_proto + IFS=":" read -r b_ip b_port b_proto <<< "$resolved" + fw::has_block_rule "$client_ip" "$b_ip" "$b_proto" "$b_port" 2>/dev/null && \ + { is_blocked=true; break; } + else + fw::has_block_rule "$client_ip" "$resolved" 2>/dev/null && \ + { is_blocked=true; break; } + fi + done + + if ! $is_blocked; then + $quiet || log::wg_warning "${svc} is not blocked for ${name}" + continue + fi + + for resolved in "${resolved_lines[@]}"; do + if [[ "$resolved" == *:*:* ]]; then + local b_ip b_port b_proto + IFS=":" read -r b_ip b_port b_proto <<< "$resolved" + fw::unblock_port "$client_ip" "$b_ip" "$b_port" "$b_proto" + block::remove_rule "$name" "port" "$b_ip" "$b_port" "$b_proto" + else + fw::unblock_ip "$client_ip" "$resolved" + block::remove_rule "$name" "ip" "$resolved" + fi + done + + $quiet || log::wg_success "${svc} has been unblocked for ${name}" + done + # Unblock specific IPs for ip in "${ips[@]}"; do fw::unblock_ip "$client_ip" "$ip" @@ -114,6 +162,7 @@ function cmd::unblock::run() { # Unblock specific subnets for subnet in "${subnets[@]}"; do fw::unblock_subnet "$client_ip" "$subnet" + block::remove_rule "$name" "subnet" "$subnet" done # Unblock specific ports @@ -122,9 +171,11 @@ function cmd::unblock::run() { IFS=":" read -r target port proto <<< "$entry" proto="${proto:-tcp}" fw::unblock_port "$client_ip" "$target" "$port" "$proto" + block::remove_rule "$name" "port" "$b_target" "$b_port" "$b_proto" done - $quiet || log::wg_success "Unblock rules applied for: ${name}" + # Clean up block file if now empty + block::cleanup "$name" return 0 } diff --git a/core/json_helper.py b/core/json_helper.py index 3db13f0..2714118 100644 --- a/core/json_helper.py +++ b/core/json_helper.py @@ -1470,6 +1470,19 @@ def net_reverse_lookup(file, ip, port='', proto=''): print(svc_name) return +def block_is_empty(file): + data = _block_read(file) + if not data: + print("true") + return + empty = ( + not data.get("blocked_direct", False) and + not data.get("blocked_by_groups", []) and + not data.get("rules", []) and + not data.get("services", []) + ) + print("true" if empty else "false") + commands = { 'get': lambda args: get(args[0], args[1]), 'set': lambda args: set_key(args[0], args[1], args[2]), diff --git a/modules/block.module.sh b/modules/block.module.sh index 8c3e87e..5e842a9 100644 --- a/modules/block.module.sh +++ b/modules/block.module.sh @@ -222,6 +222,16 @@ function block::format_rules() { return 0 } +function block::cleanup() { + local name="${1:?}" + block::has_file "$name" || return 0 + local result + result=$(json::block_is_empty "$(block::file "$name")") + [[ "$result" == "true" ]] && block::remove_file "$name" + return 0 +} + + # function block::format_rules() { # local name="${1:?}" # block::has_file "$name" || return 0 diff --git a/modules/firewall.module.sh b/modules/firewall.module.sh index 44bb42f..c4e3a41 100644 --- a/modules/firewall.module.sh +++ b/modules/firewall.module.sh @@ -12,6 +12,30 @@ function fw::on_load() { # Rule Management # ============================================ +# ============================================ +# Public Interfaces + +function fw::has_rule() { + # Generic rule existence check + # fw::has_rule [port] [proto] + local action="${1:-DROP}" client_ip="${2:-}" target="${3:-}" \ + port="${4:-}" proto="${5:-}" + if [[ -n "$port" ]]; then + fw::_forward_exists -s "$client_ip" -d "$target" \ + -p "$proto" --dport "$port" -j "$action" + else + fw::_forward_exists -s "$client_ip" -d "$target" -j "$action" + fi +} + +function fw::has_block_rule() { + fw::has_rule "DROP" "$@" +} + +function fw::has_allow_rule() { + fw::has_rule "ACCEPT" "$@" +} + # ============================================ # Block / Unblock @@ -174,7 +198,7 @@ function fw::nat_remove_dns_redirect() { function fw::forward_rules_for_ip() { local ip="${1:-}" - iptables -L FORWARD -n -v