From 9a3ac2ae470f425fc88362f9a94cff6d37c61fee Mon Sep 17 00:00:00 2001 From: Nuno Duque Nunes Date: Fri, 15 May 2026 08:04:06 +0000 Subject: [PATCH] feat: net command, service annotations, block::restore_rules_for, fw refactor, restricted status, block system cleanup --- commands/block.command.sh | 132 +++++++----- commands/inspect.command.sh | 205 ++++++++++--------- commands/list.command.sh | 26 +-- commands/net.command.sh | 299 +++++++++++++++++++++++++++ commands/rule.command.sh | 390 +++++++++++++++++++++++------------- commands/test.command.sh | 29 +++ core/context.sh | 2 + core/json.sh | 9 + core/json_helper.py | 179 +++++++++++++++++ modules/block.module.sh | 92 ++++++++- modules/firewall.module.sh | 376 ++++++++++++++++------------------ modules/net.module.sh | 102 ++++++++++ modules/rule.module.sh | 137 +++++++++++++ wgctl | 4 + 14 files changed, 1476 insertions(+), 506 deletions(-) create mode 100644 commands/net.command.sh create mode 100644 modules/net.module.sh diff --git a/commands/block.command.sh b/commands/block.command.sh index c1ca70e..4652dbb 100644 --- a/commands/block.command.sh +++ b/commands/block.command.sh @@ -14,6 +14,9 @@ function cmd::block::on_load() { flag::register --proto flag::register --subnet flag::register --block-name + + # System - NET Services + flag::register --service } # ============================================ @@ -51,94 +54,119 @@ EOF # ============================================ function cmd::block::run() { - local name="" - local type="" - local block_name="" - local ips=() - local subnets=() - local ports=() - local quiet=false + local name="" type="" block_name="" + local ips=() subnets=() ports=() services=() + local quiet=false force=false while [[ $# -gt 0 ]]; do case "$1" in - --name) name="$2"; shift 2 ;; - --type) type="$2"; shift 2 ;; - --ip) ips+=("$2"); shift 2 ;; - --block-name) block_name="$2"; shift 2 ;; - --force) force=true; shift ;; - --quiet) quiet=true; shift ;; - --subnet) subnets+=("$2"); shift 2 ;; - --port) ports+=("$2"); shift 2 ;; - --help) cmd::block::help; return ;; + --name) name="$2"; shift 2 ;; + --type) type="$2"; shift 2 ;; + --ip) ips+=("$2"); shift 2 ;; + --block-name) block_name="$2"; shift 2 ;; + --service) services+=("$2"); shift 2 ;; + --force) force=true; shift ;; + --quiet) quiet=true; shift ;; + --subnet) subnets+=("$2"); shift 2 ;; + --port) ports+=("$2"); shift 2 ;; + --help) cmd::block::help; return ;; *) log::error "Unknown flag: $1" cmd::block::help - return 1 - ;; + return 1 ;; esac done - if [[ -z "$name" ]]; then - log::error "Missing required flag: --name" - cmd::block::help - return 1 - fi + [[ -z "$name" ]] && log::error "Missing required flag: --name" && \ + cmd::block::help && return 1 name=$(peers::resolve_and_require "$name" "$type") || return 1 - # Check if actually blocked - if peers::is_blocked "$name" || block::has_file "$name"; then - log::wg_warning "Client is already blocked: ${name}" - return 0 - fi - - # Update cache first - monitor::update_endpoint_cache - - local public_key - public_key=$(keys::public "$name") || return 1 - - local endpoint - endpoint=$(cmd::block::_get_endpoint "$name" "$public_key") - local client_ip client_ip=$(peers::get_ip "$name") || return 1 - # No specific target — block everything - # Only full block if no specific targets provided - if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && ${#subnets[@]} -eq 0 ]]; then + # Full block if no specific targets + if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \ + ${#subnets[@]} -eq 0 && ${#services[@]} -eq 0 ]]; then + if peers::is_blocked "$name" || block::has_file "$name"; then + log::wg_warning "Client is already blocked: ${name}" + return 0 + fi + monitor::update_endpoint_cache cmd::block::_block_all "$name" "$client_ip" "$quiet" return 0 fi - # Specific rules — don't full block - block::set_direct "$name" "$client_ip" "false" # ensure not marked as full block + # Specific rules — check if already fully blocked + if block::has_file "$name"; then + local direct + direct=$(block::is_blocked_direct "$name") + if [[ "$direct" == "true" ]]; then + log::wg_warning "${name} is fully blocked — unblock first to add specific rules" + return 1 + fi + fi # Block specific IPs for ip in "${ips[@]}"; do ip::require_valid "$ip" fw::block_ip "$client_ip" "$ip" - block::add_rule "$name" "$client_ip" "ip" "" "$ip" + block::add_rule "$name" "$client_ip" "ip" "${block_name:-}" "$ip" done # Block specific subnets for subnet in "${subnets[@]}"; do ip::require_valid "$subnet" fw::block_subnet "$client_ip" "$subnet" - block::add_rule "$name" "$client_ip" "subnet" "" "$target_ip" + block::add_rule "$name" "$client_ip" "subnet" "${block_name:-}" "$subnet" done # Block specific ports for entry in "${ports[@]}"; do - local target port proto - IFS=":" read -r target port proto <<< "$entry" - ip::require_valid "$target" - - fw::block_port "$client_ip" "$target" "$port" "${proto:-tcp}" - block::add_rule "$name" "$client_ip" "port" "" "$target" "$port" "${proto:-tcp}" + local b_target b_port b_proto + IFS=":" read -r b_target b_port b_proto <<< "$entry" + ip::require_valid "$b_target" + fw::block_port "$client_ip" "$b_target" "$b_port" "${b_proto:-tcp}" + block::add_rule "$name" "$client_ip" "port" "${block_name:-}" \ + "$b_target" "$b_port" "${b_proto:-tcp}" done - log::debug "Block rules applied for: ${name}" + # Block 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 or has no ports: ${svc}" + return 1 + 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::block_port "$client_ip" "$b_ip" "$b_port" "$b_proto" + block::add_rule "$name" "$client_ip" "port" "$svc" \ + "$b_ip" "$b_port" "$b_proto" + else + fw::block_ip "$client_ip" "$resolved" + block::add_rule "$name" "$client_ip" "ip" "$svc" "$resolved" + fi + done + done + + # 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 + fi + + $quiet || log::wg_success "${name} — access restricted" + return 0 } function cmd::block::_get_endpoint() { diff --git a/commands/inspect.command.sh b/commands/inspect.command.sh index 973990b..c541cbb 100644 --- a/commands/inspect.command.sh +++ b/commands/inspect.command.sh @@ -56,9 +56,12 @@ function cmd::inspect::_peer_info() { peers::is_blocked "$name" && is_blocked="true" || is_blocked="false" last_ts=$(monitor::last_attempt "$name") + local is_restricted="false" + block::has_specific_rules "$name" 2>/dev/null && is_restricted="true" + local status last_seen endpoint status=$(peers::format_status "$name" "$public_key" \ - "$is_blocked" "false" "$handshake_ts" "$last_ts") + "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts") last_seen=$(peers::format_last_seen "$name" "$public_key" \ "$is_blocked" "$last_ts" "" "$handshake_ts") endpoint=$(monitor::get_cached_endpoint "$name") @@ -109,6 +112,107 @@ function cmd::inspect::_peer_info() { return 0 } +# function cmd::inspect::_rule_info() { +# local name="${1:-}" +# local rule +# rule=$(peers::get_meta "$name" "rule") +# [[ -z "$rule" ]] && return 0 +# rule::exists "$rule" || return 0 + +# cmd::inspect::_section "Rule: ${rule}" + +# local rule_file +# rule_file="$(rule::path "$rule")" + +# # Check for inheritance +# local extends_raw=() +# mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) + +# if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then +# # Show inheritance tree +# for base_name in "${extends_raw[@]}"; do +# [[ -z "$base_name" ]] && continue +# printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name" + +# local base_allows base_blocks +# base_allows=$(rule::get "$base_name" "allow_ports")$'\n'$(rule::get "$base_name" "allow_ips") +# base_blocks=$(rule::get "$base_name" "block_ips")$'\n'$(rule::get "$base_name" "block_ports") + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "+" "$e" +# done <<< "$base_allows" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$base_blocks" + +# local base_dns +# base_dns=$(rule::get_own "$base_name" "dns_redirect") +# [[ "${base_dns,,}" == "true" ]] && \ +# printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" +# done + +# # Own rules +# local own_allows own_blocks +# own_allows=$(json::get "$rule_file" "allow_ports" 2>/dev/null)$'\n'$(json::get "$rule_file" "allow_ips" 2>/dev/null) +# own_blocks=$(json::get "$rule_file" "block_ips" 2>/dev/null)$'\n'$(json::get "$rule_file" "block_ports" 2>/dev/null) +# local has_own=false + +# while IFS= read -r e; do [[ -n "$e" ]] && has_own=true && break; done <<< "$own_allows$own_blocks" + +# if $has_own; then +# printf "\n \033[0;37mOwn:\033[0m\n" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "+" "$e" +# done <<< "$own_allows" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$own_blocks" +# fi + +# else +# # No inheritance — flat view +# local allow_ports allow_ips block_ips block_ports +# allow_ports=$(rule::get "$rule" "allow_ports") +# allow_ips=$(rule::get "$rule" "allow_ips") +# block_ips=$(rule::get "$rule" "block_ips") +# block_ports=$(rule::get "$rule" "block_ports") + +# if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then +# printf "\n" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "+" "$e" +# done <<< "$allow_ports"$'\n'"$allow_ips" +# fi + +# if [[ -n "$block_ips" || -n "$block_ports" ]]; then +# printf "\n" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$block_ips"$'\n'"$block_ports" +# fi + +# if [[ -z "$allow_ports" && -z "$allow_ips" && \ +# -z "$block_ips" && -z "$block_ports" ]]; then +# printf "\n full access (no restrictions)\n" +# fi + +# local dns_redirect +# dns_redirect=$(rule::get_own "$rule" "dns_redirect") +# [[ "${dns_redirect,,}" == "true" ]] && \ +# printf "\n \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" +# fi + +# return 0 +# } + function cmd::inspect::_rule_info() { local name="${1:-}" local rule @@ -118,93 +222,12 @@ function cmd::inspect::_rule_info() { cmd::inspect::_section "Rule: ${rule}" - local rule_file - rule_file="$(rule::path "$rule")" - - # Check for inheritance - local extends_raw=() - mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) - - if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]:-}" ]]; then - # Show inheritance tree - for base_name in "${extends_raw[@]}"; do - [[ -z "$base_name" ]] && continue - printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name" - - local base_allows base_blocks - base_allows=$(rule::get "$base_name" "allow_ports")$'\n'$(rule::get "$base_name" "allow_ips") - base_blocks=$(rule::get "$base_name" "block_ips")$'\n'$(rule::get "$base_name" "block_ports") - - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$base_allows" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$base_blocks" - - local base_dns - base_dns=$(rule::get_own "$base_name" "dns_redirect") - [[ "${base_dns,,}" == "true" ]] && \ - printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" - done - - # Own rules - local own_allows own_blocks - own_allows=$(json::get "$rule_file" "allow_ports" 2>/dev/null)$'\n'$(json::get "$rule_file" "allow_ips" 2>/dev/null) - own_blocks=$(json::get "$rule_file" "block_ips" 2>/dev/null)$'\n'$(json::get "$rule_file" "block_ports" 2>/dev/null) - local has_own=false - - while IFS= read -r e; do [[ -n "$e" ]] && has_own=true && break; done <<< "$own_allows$own_blocks" - - if $has_own; then - printf "\n \033[0;37mOwn:\033[0m\n" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$own_allows" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$own_blocks" - fi - + if rule::render_extends_tree "$rule"; then + printf "\n" else # No inheritance — flat view - local allow_ports allow_ips block_ips block_ports - allow_ports=$(rule::get "$rule" "allow_ports") - allow_ips=$(rule::get "$rule" "allow_ips") - block_ips=$(rule::get "$rule" "block_ips") - block_ports=$(rule::get "$rule" "block_ports") - - if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then - printf "\n" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$allow_ports"$'\n'"$allow_ips" - fi - - if [[ -n "$block_ips" || -n "$block_ports" ]]; then - printf "\n" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$block_ips"$'\n'"$block_ports" - fi - - if [[ -z "$allow_ports" && -z "$allow_ips" && \ - -z "$block_ips" && -z "$block_ports" ]]; then - printf "\n full access (no restrictions)\n" - fi - - local dns_redirect - dns_redirect=$(rule::get_own "$rule" "dns_redirect") - [[ "${dns_redirect,,}" == "true" ]] && \ - printf "\n \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" + rule::render_flat "$rule" fi - return 0 } @@ -317,11 +340,13 @@ function cmd::inspect::_firewall_info() { "$(color::red "-${drops}")" \ "$(printf '─%.0s' {1..28})" - if [[ ${#rules_output[@]} -gt 0 ]]; then - for line in "${rules_output[@]}"; do - fw::format_rule "$line" - done - fi + fw::list_peer_rules "$ip" false + + # if [[ ${#rules_output[@]} -gt 0 ]]; then + # for line in "${rules_output[@]}"; do + # fw::format_rule "$line" + # done + # fi return 0 } diff --git a/commands/list.command.sh b/commands/list.command.sh index 2696b4f..542b054 100644 --- a/commands/list.command.sh +++ b/commands/list.command.sh @@ -381,22 +381,7 @@ function cmd::list::_precompute_all() { # 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 - if block::is_blocked "$name" 2>/dev/null; then - p_restricted["$name"]=true - else - p_restricted["$name"]=false - fi - 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) + cmd::list::_precompute_block_status p_blocked p_restricted # Public keys declare -gA p_pubkeys=() @@ -440,11 +425,10 @@ function cmd::list::_precompute_block_status() { wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null) while IFS= read -r name; do - # Restricted = has block rules but still in server (partial block) - if block::is_blocked "$name" 2>/dev/null; then - _restricted["$name"]=true - else - _restricted["$name"]=false + if block::has_specific_rules "$name" 2>/dev/null; then + _restricted["$name"]=true + else + _restricted["$name"]=false fi # Blocked = removed from WG server diff --git a/commands/net.command.sh b/commands/net.command.sh new file mode 100644 index 0000000..811d623 --- /dev/null +++ b/commands/net.command.sh @@ -0,0 +1,299 @@ +#!/usr/bin/env bash + +function cmd::net::on_load() { + flag::register --name + flag::register --ip + flag::register --port + flag::register --desc + flag::register --tag + flag::register --detailed + flag::register --force +} + +function cmd::net::help() { + cat < [options] + +Manage named network services for use with block/allow rules. +Services map names to IPs and ports, making rules more readable. + +Subcommands: + list List all services + show --name Show service details + add --name --ip Add a service + add --name --port + Add a port to a service + rm --name Remove service or port + rm --name Remove all ports from service + +Options for add (service): + --name Service name (e.g. proxmox) + --ip Service IP address + --desc Optional description + --tag Optional tag (repeatable) + +Options for add (port): + --name Service:port-name (e.g. proxmox:web-ui) + --port Port and protocol (e.g. 8006:tcp) + --desc Optional description + +Options for list: + --detailed Show ports for each service + --tag Filter by tag + +Examples: + wgctl net list + wgctl net list --detailed + wgctl net list --tag admin + wgctl net show --name proxmox + wgctl net add --name proxmox --ip 10.0.0.100 --desc "Proxmox VE" + wgctl net add --name proxmox:web-ui --port 8006:tcp --desc "Web UI" + wgctl net add --name proxmox:ssh --port 22:tcp + wgctl net rm --name proxmox:web-ui + wgctl net rm --name proxmox:ports + wgctl net rm --name proxmox +EOF +} + +function cmd::net::run() { + local subcmd="${1:-list}" + shift || true + case "$subcmd" in + list) cmd::net::list "$@" ;; + show) cmd::net::show "$@" ;; + add) cmd::net::add "$@" ;; + rm|remove|del) cmd::net::rm "$@" ;; + help) cmd::net::help ;; + *) + log::error "Unknown subcommand: '${subcmd}'" + cmd::net::help + return 1 ;; + esac +} + +# ============================================ +# List +# ============================================ + +function cmd::net::list() { + local detailed=false filter_tag="" + + while [[ $# -gt 0 ]]; do + case "$1" in + --detailed) detailed=true; shift ;; + --tag) filter_tag="$2"; shift 2 ;; + --help) cmd::net::help; return ;; + *) log::error "Unknown flag: $1"; return 1 ;; + esac + done + + local net_file + net_file="$(ctx::net)" + + if [[ ! -f "$net_file" ]]; then + log::wg_warning "No services configured. Use 'wgctl net add' to add one." + return 0 + fi + + log::section "Network Services" + printf "\n %-20s %-16s %-6s %s\n" "NAME" "IP" "PORTS" "DESCRIPTION" + local divider + divider=$(printf '─%.0s' {1..72}) + printf " %s\n" "$divider" + + local found=false + while IFS="|" read -r name ip desc tags ports; do + [[ -z "$name" ]] && continue + + # Tag filter + if [[ -n "$filter_tag" ]]; then + [[ "$tags" != *"$filter_tag"* ]] && continue + fi + + found=true + local tag_display="" + [[ -n "$tags" ]] && tag_display=" \033[0;37m[${tags}]\033[0m" + + printf " %-20s %-16s %-6s %s%b\n" \ + "$name" "$ip" "${ports}p" "${desc:-—}" "$tag_display" + + if $detailed; then + local has_ports=false + # Show ports inline + while IFS="|" read -r ptype pname pport pproto pdesc; do + [[ "$ptype" != "port" ]] && continue + has_ports=true + local ann + ann=$(net::annotation "$ip" "$pport" "$pproto") + printf " \033[0;37m%-18s %s:%s%s\033[0m\n" \ + "${pname}" "$pport" "$pproto" \ + "${pdesc:+ # $pdesc}" + done < <(json::net_show "$net_file" "$name") + $has_ports && printf "\n" # newline after each service with ports + fi + + done < <(json::net_list "$net_file") + + if ! $found; then + [[ -n "$filter_tag" ]] && \ + log::wg_warning "No services with tag: ${filter_tag}" || \ + log::wg_warning "No services configured" + fi + + printf "\n" + return 0 +} + +# ============================================ +# Show +# ============================================ + +function cmd::net::show() { + local name="" + while [[ $# -gt 0 ]]; do + case "$1" in + --name) util::require_flag "--name" "${2:-}" || return 1 + name="$2"; shift 2 ;; + --help) cmd::net::help; return ;; + *) log::error "Unknown flag: $1"; return 1 ;; + esac + done + + [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 + net::require_exists "$name" || return 1 + + log::section "Service: ${name}" + printf "\n" + + while IFS="|" read -r key val1 val2 val3 val4; do + case "$key" in + name) ui::row "Name" "$val1" ;; + ip) ui::row "IP" "$val1" ;; + desc) ui::row "Description" "${val1:-—}" ;; + tags) ui::row "Tags" "${val1:-—}" ;; + port) + # val1=port_name val2=port val3=proto val4=desc + local ann + ann=$(net::annotation "$(json::net_resolve "$(ctx::net)" "$name")" \ + "$val2" "$val3" 2>/dev/null || true) + printf " %-20s \033[0;36m%s\033[0m %s:%s%s\n" \ + "${val1}:" "" "$val2" "$val3" \ + "${val4:+ # $val4}" + ;; + esac + done < <(json::net_show "$(ctx::net)" "$name") + + printf "\n" + return 0 +} + +# ============================================ +# Add +# ============================================ + +function cmd::net::add() { + local name="" ip="" port="" desc="" tags=() + + while [[ $# -gt 0 ]]; do + case "$1" in + --name) util::require_flag "--name" "${2:-}" || return 1 + name="$2"; shift 2 ;; + --ip) ip="$2"; shift 2 ;; + --port) port="$2"; shift 2 ;; + --desc) desc="$2"; shift 2 ;; + --tag) tags+=("$2"); shift 2 ;; + --help) cmd::net::help; return ;; + *) log::error "Unknown flag: $1"; return 1 ;; + esac + done + + [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 + + if [[ "$name" == *:* ]]; then + # Port mode: proxmox:web-ui + local svc_name="${name%%:*}" + local port_name="${name##*:}" + + [[ -z "$port" ]] && log::error "Missing required flag: --port" && return 1 + net::require_exists "$svc_name" || return 1 + + local port_num proto + if [[ "$port" == *:* ]]; then + port_num="${port%%:*}" + proto="${port##*:}" + else + port_num="$port" + proto="tcp" + fi + + json::net_add_port "$(ctx::net)" "$svc_name" "$port_name" \ + "$port_num" "$proto" "$desc" + + log::wg_success "Added port: ${svc_name}:${port_name} → ${port_num}/${proto}" + else + # Service mode: proxmox + [[ -z "$ip" ]] && log::error "Missing required flag: --ip" && return 1 + + local tags_str + tags_str=$(IFS=','; echo "${tags[*]}") + + json::net_add_service "$(ctx::net)" "$name" "$ip" "$desc" "$tags_str" + + log::wg_success "Service added: ${name} → ${ip}" + fi + return 0 +} + +# ============================================ +# Remove +# ============================================ + +function cmd::net::rm() { + 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::net::help; return ;; + *) log::error "Unknown flag: $1"; return 1 ;; + esac + done + + [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 + + # Validate existence + if [[ "$name" == *:* ]]; then + local svc_name="${name%%:*}" + local port_name="${name##*:}" + if [[ "$port_name" != "ports" ]]; then + # Check specific port exists + local exists + exists=$(json::net_exists "$(ctx::net)" "$name") + if [[ "$exists" != "true" ]]; then + log::error "Port not found: ${name}" + return 1 + fi + else + net::require_exists "$svc_name" || return 1 + fi + else + net::require_exists "$name" || return 1 + fi + + if ! $force; then + local what="service '${name}'" + [[ "$name" == *:ports ]] && what="all ports from '${name%%:*}'" + [[ "$name" == *:* && "$name" != *:ports ]] && what="port '${name}'" + read -r -p "Remove ${what}? [y/N] " confirm + case "$confirm" in + [yY]*) ;; + *) log::info "Aborted"; return 0 ;; + esac + fi + + json::net_remove "$(ctx::net)" "$name" + log::wg_success "Removed: ${name}" + return 0 +} \ No newline at end of file diff --git a/commands/rule.command.sh b/commands/rule.command.sh index a1ef11a..e2ae595 100644 --- a/commands/rule.command.sh +++ b/commands/rule.command.sh @@ -12,6 +12,8 @@ function cmd::rule::on_load() { flag::register --remove-extends flag::register --block-ip flag::register --allow-ip + flag::register --block-service + flag::register --allow-service flag::register --block-port flag::register --allow-port flag::register --remove-block-ip @@ -311,17 +313,225 @@ function cmd::rule::list() { # Show # ============================================ +# function cmd::rule::show() { +# local name="" show_peers=true color=false show_resolved=false + +# while [[ $# -gt 0 ]]; do +# case "$1" in +# --name) util::require_flag "--name" "${2:-}" || return 1 +# name="$2"; shift 2 ;; +# --no-peers) show_peers=false; shift ;; +# --color) color=true; shift ;; +# --resolved) show_resolved=true; shift ;; +# --help) cmd::rule::help; return ;; +# *) log::error "Unknown flag: $1"; return 1 ;; +# esac +# done + +# [[ -z "$name" ]] && log::error "Missing required flag: --name" && return 1 +# rule::require_exists "$name" || return 1 + +# local rule_file +# rule_file="$(rule::path "$name")" + +# local dns_redirect +# dns_redirect=$(rule::get_own "$name" "dns_redirect") +# dns_redirect="${dns_redirect:-false}" + +# local resolved_dns +# resolved_dns=$(rule::get "$name" "dns_redirect") +# resolved_dns="${resolved_dns:-false}" + +# local dns_display +# if [[ "${resolved_dns,,}" == "true" && "${dns_redirect,,}" != "true" ]]; then +# dns_display="true (inherited)" +# elif [[ "${dns_redirect,,}" == "true" ]]; then +# dns_display="true" +# else +# dns_display="false" +# fi + + +# log::section "Rule: ${name}" +# printf "\n" + +# # ── Header info ────────────────────────────── +# local desc group dns_redirect +# desc=$(json::get "$rule_file" "desc") +# group=$(json::get "$rule_file" "group") + +# ui::row "Description" "${desc:-—}" +# ui::row "Group" "${group:-—}" +# ui::row "DNS" "$dns_display" + +# # ── Extends section ────────────────────────── +# local extends_raw=() +# mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) + +# if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]}" ]]; then +# cmd::rule::_show_section "Extends" + +# for base_name in "${extends_raw[@]}"; do +# [[ -z "$base_name" ]] && continue +# printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name" + +# # Show resolved entries for this base +# local base_allow_ports base_allow_ips base_block_ips base_block_ports base_dns +# base_allow_ports=$(rule::get "$base_name" "allow_ports" 2>/dev/null || true) +# base_allow_ips=$(rule::get "$base_name" "allow_ips" 2>/dev/null || true) +# base_block_ips=$(rule::get "$base_name" "block_ips" 2>/dev/null || true) +# base_block_ports=$(rule::get "$base_name" "block_ports" 2>/dev/null || true) +# base_dns=$(json::get "$(rule::path "$base_name")" "dns_redirect" 2>/dev/null || true) + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "+" "$e" +# done <<< "$base_allow_ports" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$base_allow_ips" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$base_block_ips" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$base_block_ports" + +# [[ "$base_dns" == "true" ]] && \ +# printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" +# done +# fi + +# # ── Own Rules section ───────────────────────── +# local own_allow_ports own_allow_ips own_block_ips own_block_ports +# own_allow_ports=$(json::get "$rule_file" "allow_ports") +# own_allow_ips=$(json::get "$rule_file" "allow_ips") +# own_block_ips=$(json::get "$rule_file" "block_ips") +# own_block_ports=$(json::get "$rule_file" "block_ports") + +# local has_own=false +# [[ -n "$own_allow_ports" || -n "$own_allow_ips" || \ +# -n "$own_block_ips" || -n "$own_block_ports" ]] && has_own=true + +# if $has_own; then +# if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]}" ]]; then +# cmd::rule::_show_section "Own Rules" +# printf "\n" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "+" "$e" +# done <<< "$own_allow_ports" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "+" "$e" +# done <<< "$own_allow_ips" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$own_block_ips" + +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# net::print_entry "-" "$e" +# done <<< "$own_block_ports" + +# [[ "$dns_redirect" == "true" ]] && \ +# printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" +# else +# # No inheritance — use Allow/Block sections +# if [[ -n "$own_allow_ports" || -n "$own_allow_ips" ]]; then +# cmd::rule::_show_section "Allow" +# printf "\n" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# printf " \033[0;32m+\033[0m %s\n" "$e" +# done <<< "$own_allow_ports" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# printf " \033[0;32m+\033[0m %s\n" "$e" +# done <<< "$own_allow_ips" +# fi + +# if [[ -n "$own_block_ips" || -n "$own_block_ports" ]]; then +# cmd::rule::_show_section "Block" +# printf "\n" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# printf " \033[0;31m-\033[0m %s\n" "$e" +# done <<< "$own_block_ips" +# while IFS= read -r e; do +# [[ -z "$e" ]] && continue +# printf " \033[0;31m-\033[0m %s\n" "$e" +# done <<< "$own_block_ports" +# fi + +# if [[ "$dns_redirect" == "true" ]]; then +# cmd::rule::_show_section "DNS" +# printf "\n \033[0;36m↺\033[0m Redirect all DNS → %s\n" "$(config::dns)" +# fi +# fi +# elif [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]}" ]]; then +# printf "\n" +# ui::row "Access" "full (no restrictions)" +# fi + +# # ── Resolved section (optional) ────────────── +# if $show_resolved; then +# cmd::rule::_show_section "Resolved (applied to peers)" +# local res_allow_ports res_allow_ips res_block_ips res_block_ports +# res_allow_ports=$(rule::get "$name" "allow_ports") +# res_allow_ips=$(rule::get "$name" "allow_ips") +# res_block_ips=$(rule::get "$name" "block_ips") +# res_block_ports=$(rule::get "$name" "block_ports") + +# while IFS= read -r e; do [[ -n "$e" ]] && \ +# printf " \033[0;32m+\033[0m %s\n" "$e"; done <<< "$res_allow_ports" +# while IFS= read -r e; do [[ -n "$e" ]] && \ +# printf " \033[0;32m+\033[0m %s\n" "$e"; done <<< "$res_allow_ips" +# while IFS= read -r e; do [[ -n "$e" ]] && \ +# printf " \033[0;31m-\033[0m %s\n" "$e"; done <<< "$res_block_ips" +# while IFS= read -r e; do [[ -n "$e" ]] && \ +# printf " \033[0;31m-\033[0m %s\n" "$e"; done <<< "$res_block_ports" +# fi + +# # ── Peers section ───────────────────────────── +# cmd::rule::_show_section "Peers" + +# local peer_list=() +# mapfile -t peer_list < <(peers::with_rule "$name") +# local peer_count=${#peer_list[@]} +# ui::row "Assigned" "$peer_count" + +# 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") +# printf " %-28s %s\n" "$peer_name" "$ip" +# done +# fi + +# printf "\n" +# } + function cmd::rule::show() { - local name="" show_peers=true color=false show_resolved=false + local name="" show_peers=true show_resolved=false while [[ $# -gt 0 ]]; do case "$1" in --name) util::require_flag "--name" "${2:-}" || return 1 name="$2"; shift 2 ;; - --no-peers) show_peers=false; shift ;; - --color) color=true; shift ;; + --no-peers) show_peers=false; shift ;; --resolved) show_resolved=true; shift ;; - --help) cmd::rule::help; return ;; + --help) cmd::rule::help; return ;; *) log::error "Unknown flag: $1"; return 1 ;; esac done @@ -332,15 +542,13 @@ function cmd::rule::show() { local rule_file rule_file="$(rule::path "$name")" - local dns_redirect + # ── DNS display ─────────────────────────────── + local dns_redirect resolved_dns dns_display dns_redirect=$(rule::get_own "$name" "dns_redirect") dns_redirect="${dns_redirect:-false}" - - local resolved_dns resolved_dns=$(rule::get "$name" "dns_redirect") resolved_dns="${resolved_dns:-false}" - local dns_display if [[ "${resolved_dns,,}" == "true" && "${dns_redirect,,}" != "true" ]]; then dns_display="true (inherited)" elif [[ "${dns_redirect,,}" == "true" ]]; then @@ -349,157 +557,46 @@ function cmd::rule::show() { dns_display="false" fi - log::section "Rule: ${name}" printf "\n" - # ── Header info ────────────────────────────── - local desc group dns_redirect + local desc group desc=$(json::get "$rule_file" "desc") group=$(json::get "$rule_file" "group") - ui::row "Description" "${desc:-—}" ui::row "Group" "${group:-—}" ui::row "DNS" "$dns_display" - # ── Extends section ────────────────────────── - local extends_raw=() - mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) - - if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]}" ]]; then - cmd::rule::_show_section "Extends" - - for base_name in "${extends_raw[@]}"; do - [[ -z "$base_name" ]] && continue - printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name" - - # Show resolved entries for this base - local base_allow_ports base_allow_ips base_block_ips base_block_ports base_dns - base_allow_ports=$(rule::get "$base_name" "allow_ports" 2>/dev/null || true) - base_allow_ips=$(rule::get "$base_name" "allow_ips" 2>/dev/null || true) - base_block_ips=$(rule::get "$base_name" "block_ips" 2>/dev/null || true) - base_block_ports=$(rule::get "$base_name" "block_ports" 2>/dev/null || true) - base_dns=$(json::get "$(rule::path "$base_name")" "dns_redirect" 2>/dev/null || true) - - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$base_allow_ports" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$base_allow_ips" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$base_block_ips" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$base_block_ports" - [[ "$base_dns" == "true" ]] && \ - printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" - done - fi - - # ── Own Rules section ───────────────────────── - local own_allow_ports own_allow_ips own_block_ips own_block_ports - own_allow_ports=$(json::get "$rule_file" "allow_ports") - own_allow_ips=$(json::get "$rule_file" "allow_ips") - own_block_ips=$(json::get "$rule_file" "block_ips") - own_block_ports=$(json::get "$rule_file" "block_ports") - - local has_own=false - [[ -n "$own_allow_ports" || -n "$own_allow_ips" || \ - -n "$own_block_ips" || -n "$own_block_ports" ]] && has_own=true - - if $has_own; then - if [[ ${#extends_raw[@]} -gt 0 && -n "${extends_raw[0]}" ]]; then - cmd::rule::_show_section "Own Rules" - printf "\n" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$own_allow_ports" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$own_allow_ips" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$own_block_ips" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$own_block_ports" - [[ "$dns_redirect" == "true" ]] && \ - printf " \033[0;36m↺\033[0m DNS → %s\n" "$(config::dns)" - else - # No inheritance — use Allow/Block sections - if [[ -n "$own_allow_ports" || -n "$own_allow_ips" ]]; then - cmd::rule::_show_section "Allow" - printf "\n" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$own_allow_ports" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;32m+\033[0m %s\n" "$e" - done <<< "$own_allow_ips" - fi - - if [[ -n "$own_block_ips" || -n "$own_block_ports" ]]; then - cmd::rule::_show_section "Block" - printf "\n" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$own_block_ips" - while IFS= read -r e; do - [[ -z "$e" ]] && continue - printf " \033[0;31m-\033[0m %s\n" "$e" - done <<< "$own_block_ports" - fi - - if [[ "$dns_redirect" == "true" ]]; then - cmd::rule::_show_section "DNS" - printf "\n \033[0;36m↺\033[0m Redirect all DNS → %s\n" "$(config::dns)" - fi - fi - elif [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]}" ]]; then + # ── Extends + own rules ──────────────────────── + if rule::render_extends_tree "$name"; then + # Has inheritance — tree already rendered printf "\n" - ui::row "Access" "full (no restrictions)" + else + # No inheritance — flat view + rule::render_flat "$name" fi - # ── Resolved section (optional) ────────────── + # ── Resolved ────────────────────────────────── if $show_resolved; then cmd::rule::_show_section "Resolved (applied to peers)" + printf "\n" local res_allow_ports res_allow_ips res_block_ips res_block_ports res_allow_ports=$(rule::get "$name" "allow_ports") res_allow_ips=$(rule::get "$name" "allow_ips") res_block_ips=$(rule::get "$name" "block_ips") res_block_ports=$(rule::get "$name" "block_ports") - - while IFS= read -r e; do [[ -n "$e" ]] && \ - printf " \033[0;32m+\033[0m %s\n" "$e"; done <<< "$res_allow_ports" - while IFS= read -r e; do [[ -n "$e" ]] && \ - printf " \033[0;32m+\033[0m %s\n" "$e"; done <<< "$res_allow_ips" - while IFS= read -r e; do [[ -n "$e" ]] && \ - printf " \033[0;31m-\033[0m %s\n" "$e"; done <<< "$res_block_ips" - while IFS= read -r e; do [[ -n "$e" ]] && \ - printf " \033[0;31m-\033[0m %s\n" "$e"; done <<< "$res_block_ports" + while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "+" "$e"; done \ + <<< "$res_allow_ports"$'\n'"$res_allow_ips" + while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \ + <<< "$res_block_ips"$'\n'"$res_block_ports" fi - # ── Peers section ───────────────────────────── + # ── Peers ───────────────────────────────────── cmd::rule::_show_section "Peers" - local peer_list=() mapfile -t peer_list < <(peers::with_rule "$name") local peer_count=${#peer_list[@]} ui::row "Assigned" "$peer_count" - if $show_peers && [[ $peer_count -gt 0 ]]; then printf "\n" for peer_name in "${peer_list[@]}"; do @@ -508,8 +605,8 @@ function cmd::rule::show() { printf " %-28s %s\n" "$peer_name" "$ip" done fi - printf "\n" + return 0 } # ============================================ @@ -520,6 +617,7 @@ function cmd::rule::add() { local name="" desc="" group="" local extends=() local allow_ips=() block_ips=() block_ports=() allow_ports=() + local block_services=() allow_services=() local dns_redirect=false local is_base=false @@ -537,6 +635,8 @@ function cmd::rule::add() { --allow-port) allow_ports+=("$2"); shift 2 ;; --block-ip) block_ips+=("$2"); shift 2 ;; --block-port) block_ports+=("$2"); shift 2 ;; + --block-service) block_services+=("$2"); shift 2 ;; + --allow-service) allow_services+=("$2"); shift 2 ;; --dns-redirect) dns_redirect=true; shift ;; --help) cmd::rule::help; return ;; *) @@ -566,6 +666,26 @@ function cmd::rule::add() { rule_dir="$(ctx::rules)" fi + for svc in "${block_services[@]}"; do + while IFS= read -r resolved; do + if [[ "$resolved" == *:*:* ]]; then + block_ports+=("${resolved}") + else + block_ips+=("${resolved}/32") + fi + done < <(net::resolve "$svc") + done + + for svc in "${allow_services[@]}"; do + while IFS= read -r resolved; do + if [[ "$resolved" == *:*:* ]]; then + allow_ports+=("${resolved}") + else + allow_ips+=("${resolved}/32") + fi + done < <(net::resolve "$svc") + done + local rule_file="${rule_dir}/${name}.rule" local allow_str block_str port_str allow_port_str extends_str @@ -844,4 +964,4 @@ function cmd::rule::_show_section() { esac fi printf "\n ${color_code}── %s ──────────────────────────────────\033[0m\n" "$title" -} +} \ No newline at end of file diff --git a/commands/test.command.sh b/commands/test.command.sh index 8d8a236..4c4014f 100644 --- a/commands/test.command.sh +++ b/commands/test.command.sh @@ -204,6 +204,34 @@ function cmd::test::section_fw() { cmd::test::run_cmd "fw nat" "PREROUTING" fw nat cmd::test::run_cmd "fw count" "TOTAL" fw count } +function cmd::test::section_net() { + test::section "Net" + + "$WGCTL_BINARY" net rm --name test-svc --force > /dev/null 2>&1 || true + + cmd::test::run_cmd "net add service" "added" \ + net add --name test-svc --ip 10.0.0.99 --desc "Test service" + cmd::test::run_cmd "net add port" "Added" \ + net add --name test-svc:web --port 9999:tcp + cmd::test::run_cmd "net list" "test-svc" \ + net list + cmd::test::run_cmd "net list --detailed" "web" \ + net list --detailed + cmd::test::run_cmd "net show" "9999" \ + net show --name test-svc + cmd::test::run_cmd "net rm port" "Removed" \ + net rm --name test-svc:web --force + cmd::test::run_cmd "net add port again" "Added" \ + net add --name test-svc:web --port 9999:tcp + cmd::test::run_cmd "net rm all ports" "Removed" \ + net rm --name test-svc:ports --force + cmd::test::run_cmd "net rm service" "Removed" \ + net rm --name test-svc --force + cmd::test::run_cmd_fails "net show nonexistent" \ + net show --name nonexistent-svc + cmd::test::run_cmd_fails "net add port no service" \ + net add --name nonexistent:web --port 80:tcp +} function cmd::test::section_destructive() { test::section "Destructive (modifying state)" @@ -432,6 +460,7 @@ function cmd::test::run() { audit) cmd::test::section_audit ;; logs) cmd::test::section_logs ;; fw) cmd::test::section_fw ;; + net) cmd::test::section_net ;; destructive) cmd::test::section_destructive ;; *) log::error "Unknown section: $section" diff --git a/core/context.sh b/core/context.sh index 1367ee1..a05379e 100644 --- a/core/context.sh +++ b/core/context.sh @@ -22,6 +22,7 @@ _CTX_GROUPS="${_CTX_DATA}/groups" _CTX_BLOCKS="${_CTX_DATA}/blocks" _CTX_META="${_CTX_DATA}/meta" _CTX_DAEMON="${_CTX_DATA}/daemon" +_CTX_NET="${_CTX_DATA}/services.json" # ============================================ @@ -41,6 +42,7 @@ function ctx::groups() { echo "$_CTX_GROUPS"; } function ctx::blocks() { echo "$_CTX_BLOCKS"; } function ctx::meta() { echo "$_CTX_META"; } function ctx::daemon() { echo "$_CTX_DAEMON"; } +function ctx::net() { echo "$_CTX_NET"; } function ctx::events_log() { echo "$(ctx::daemon)/events.log"; } function ctx::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; } diff --git a/core/json.sh b/core/json.sh index e7dd348..80837cc 100644 --- a/core/json.sh +++ b/core/json.sh @@ -50,6 +50,15 @@ function json::block_remove_rule() { python3 "$JSON_HELPER" block_remove_ru function json::block_get_rules() { python3 "$JSON_HELPER" block_get_rules "$@" 3 else '', + args[4] if len(args) > 4 else '' + ), + 'net_add_port': lambda args: net_add_port( + args[0], args[1], args[2], args[3], + args[4] if len(args) > 4 else 'tcp', + args[5] if len(args) > 5 else '' + ), + 'net_remove': lambda args: net_remove(args[0], args[1]), + 'net_resolve': lambda args: net_resolve(args[0], args[1]), + 'net_reverse_lookup': lambda args: net_reverse_lookup( + args[0], args[1], + args[2] if len(args) > 2 else '', + args[3] if len(args) > 3 else '' + ), + 'block_is_empty': lambda args: block_is_empty(args[0]), } if __name__ == '__main__': diff --git a/modules/block.module.sh b/modules/block.module.sh index df4e15a..8c3e87e 100644 --- a/modules/block.module.sh +++ b/modules/block.module.sh @@ -16,6 +16,16 @@ function block::has_file() { [[ -f "$(block::file "$name")" ]] } +function block::has_specific_rules() { + local name="${1:?}" + block::has_file "$name" || return 1 + while IFS="|" read -r bname btype target port proto; do + [[ -z "$btype" ]] && continue + [[ "$btype" != "full" ]] && return 0 + done < <(block::get_rules "$name") + return 1 +} + function block::is_blocked() { local name="${1:?}" block::has_file "$name" || return 1 @@ -128,6 +138,19 @@ function block::restore_peer() { return 0 } +function block::restore_rules_for() { + local name="${1:?}" client_ip="${2:?}" + while IFS="|" read -r bname btype target port proto; do + [[ -z "$btype" ]] && continue + case "$btype" in + ip) fw::block_ip "$client_ip" "$target" "append" ;; + port) fw::block_port "$client_ip" "$target" "$port" \ + "${proto:-tcp}" "append" ;; + subnet) fw::block_subnet "$client_ip" "$target" "append" ;; + esac + done < <(block::get_rules "$name") +} + function block::restore_all() { while IFS= read -r peer_name; do block::has_file "$peer_name" || continue @@ -151,17 +174,68 @@ function block::restore_all() { function block::format_rules() { local name="${1:?}" block::has_file "$name" || return 0 + while IFS="|" read -r bname btype target port proto; do [[ -z "$btype" ]] && continue - local display + + local display="" ann="" + case "$btype" in - full) display="all traffic" ;; - ip) display="$target" ;; - port) display="${target}:${port}:${proto}" ;; - subnet) display="$target" ;; + full) + display="all traffic" + ;; + ip) + display="$target" + ann=$(net::annotate "$target") + ;; + port) + display="${target}:${port}:${proto}" + ann=$(net::annotate "${target}:${port}:${proto}") + ;; + subnet) + display="$target" + ann=$(net::annotate "${target%%/*}") + ;; esac - local label="${bname:-$btype}" - printf " \033[0;31m-\033[0m %-30s \033[0;37m%s\033[0m\n" \ - "$display" "$label" + + local label="$bname" + + # If bname wasn't set (equals type default), clear it + case "$label" in + full|ip|port|subnet|"") label="" ;; + esac + + # Suppress label if it matches annotation + if [[ -n "$ann" && -n "$label" && \ + ("$ann" == "$label" || "$ann" == "${label}:"*) ]]; then + label="" + fi + + # log::debug "label='$label' ann='$ann' match=$([ "$ann" == "$label" ] && echo yes || echo no)" + + printf " \033[0;31m-\033[0m %-20s \033[0;37m%s%s\033[0m\n" \ + "$display" \ + "${label:+${label} }" \ + "${ann:+→ ${ann}}" + done < <(block::get_rules "$name") -} \ No newline at end of file + return 0 +} + +# function block::format_rules() { +# local name="${1:?}" +# block::has_file "$name" || return 0 +# while IFS="|" read -r bname btype target port proto; do +# [[ -z "$btype" ]] && continue +# local display +# case "$btype" in +# full) display="all traffic" ;; +# ip) display="$target" ;; +# port) display="${target}:${port}:${proto}" ;; +# subnet) display="$target" ;; +# esac +# local label="${bname:-$btype}" +# printf " \033[0;31m-\033[0m %-30s \033[0;37m%s\033[0m\n" \ +# "$display" "$label" +# done < <(block::get_rules "$name") +# } \ No newline at end of file diff --git a/modules/firewall.module.sh b/modules/firewall.module.sh index c0d7842..44bb42f 100644 --- a/modules/firewall.module.sh +++ b/modules/firewall.module.sh @@ -12,143 +12,108 @@ function fw::on_load() { # Rule Management # ============================================ +# ============================================ +# Block / Unblock + function fw::block_ip() { - local client_ip="${1:-}" target_ip="${2:-}" - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -j DROP \ - || iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j DROP - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ - || iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" + local client_ip="${1:-}" target_ip="${2:-}" mode="${3:-insert}" + fw::_block_pair "$mode" -s "$client_ip" -d "$target_ip" } function fw::unblock_ip() { local client_ip="${1:-}" target_ip="${2:-}" - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ - && iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -j DROP \ - && iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j DROP 2>/dev/null || true + fw::_unblock_pair -s "$client_ip" -d "$target_ip" } function fw::block_port() { - local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP \ - || iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ - || iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" - + local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" \ + proto="${4:-tcp}" mode="${5:-insert}" + fw::_block_pair "$mode" \ + -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" } function fw::unblock_port() { local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ - && iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP \ - && iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP 2>/dev/null || true -} - -function fw::block_all() { - local client_ip="${1:-}" client_name="${2:-}" - - fw::_forward_exists -s "$client_ip" -j DROP \ - || iptables -A FORWARD -s "$client_ip" -j DROP - - log::debug "Blocked all traffic from: ${client_ip}" -} - -function fw::unblock_all() { - local client_ip="${1:-}" - - fw::_forward_exists -s "$client_ip" -j DROP \ - && iptables -D FORWARD -s "$client_ip" -j DROP 2>/dev/null || true - - monitor::unwatch "$client_ip" - log::debug "Unblocked all traffic from: ${client_ip}" + fw::_unblock_pair \ + -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" } function fw::block_subnet() { - local client_ip="${1:-}" target_subnet="${2:-}" - - fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ - || iptables -A FORWARD -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" - - fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j DROP \ - || iptables -A FORWARD -s "$client_ip" -d "$target_subnet" -j DROP - + local client_ip="${1:-}" target_subnet="${2:-}" mode="${3:-append}" + fw::_block_pair "$mode" -s "$client_ip" -d "$target_subnet" log::wg_block "Blocked ${client_ip} → subnet ${target_subnet}" } function fw::unblock_subnet() { local client_ip="${1:-}" target_subnet="${2:-}" - - fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ - && iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true - - fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j DROP \ - && iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j DROP 2>/dev/null || true - + fw::_unblock_pair -s "$client_ip" -d "$target_subnet" log::wg_unblock "Unblocked ${client_ip} → subnet ${target_subnet}" } -function fw::allow_subnet() { - local client_ip="${1:-}" target_subnet="${2:-}" - - fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \ - || iptables -I FORWARD 1 -s "$client_ip" -d "$target_subnet" -j ACCEPT +function fw::block_all() { + local client_ip="${1:-}" client_name="${2:-}" + fw::_forward_exists -s "$client_ip" -j DROP \ + || iptables -A FORWARD -s "$client_ip" -j DROP + log::debug "Blocked all traffic from: ${client_ip}" } -function fw::unallow_subnet() { - local client_ip="${1:-}" target_subnet="${2:-}" - - fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \ - && iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j ACCEPT 2>/dev/null || true +function fw::unblock_all() { + local client_ip="${1:-}" + fw::_forward_exists -s "$client_ip" -j DROP \ + && iptables -D FORWARD -s "$client_ip" -j DROP 2>/dev/null || true + monitor::unwatch "$client_ip" + log::debug "Unblocked all traffic from: ${client_ip}" } +# ============================================ +# Allow / Unallow + function fw::allow_ip() { local client_ip="${1:-}" target_ip="${2:-}" - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -j ACCEPT \ - || iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j ACCEPT + fw::_accept_insert -s "$client_ip" -d "$target_ip" } function fw::unallow_ip() { local client_ip="${1:-}" target_ip="${2:-}" + fw::_accept_remove -s "$client_ip" -d "$target_ip" +} - fw::_forward_exists -s "$client_ip" -d "$target_ip" -j ACCEPT \ - && iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j ACCEPT 2>/dev/null || true +function fw::allow_subnet() { + local client_ip="${1:-}" target_subnet="${2:-}" + fw::_accept_insert -s "$client_ip" -d "$target_subnet" +} + +function fw::unallow_subnet() { + local client_ip="${1:-}" target_subnet="${2:-}" + fw::_accept_remove -s "$client_ip" -d "$target_subnet" } function fw::allow_port() { local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT \ - || iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT + fw::_accept_insert \ + -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" } function fw::unallow_port() { local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" - - fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT \ - && iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true + fw::_accept_remove \ + -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" } +# ============================================ +# Flush +# ============================================ + function fw::flush_peer() { local client_ip="${1:?client_ip required}" log::debug "flush_peer: starting for $client_ip" - # Collect line numbers into array local linenums=() while IFS= read -r linenum; do [[ -n "$linenum" ]] && linenums+=("$linenum") - done < <(iptables -L FORWARD -n --line-numbers | grep -F "$client_ip" | awk '{print $1}') + done < <(iptables -L FORWARD -n --line-numbers \ + | grep -F "$client_ip" | awk '{print $1}') - # Delete in reverse order (highest number first) local count=0 local i for (( i=${#linenums[@]}-1; i>=0; i-- )); do @@ -159,118 +124,6 @@ function fw::flush_peer() { log::debug "flush_peer: removed $count FORWARD rules for: $client_ip" } -# ============================================ -# Guest Subnet Rules -# ============================================ - -function fw::apply_dns_redirect() { - if iptables -t nat -C PREROUTING -s "$(config::subnet_for guest).0/24" -p udp --dport 53 -j DNAT --to-destination "$(config::dns):53" 2>/dev/null; then - log::wg "Guest DNS redirect already applied" - return 0 - fi - - local guest_subnet dns - guest_subnet="$(config::subnet_for "guest").0/24" - dns="$(config::dns)" - - # Log DNS bypass attempts (queries not directed at Pi-hole) - iptables -t nat -A PREROUTING -s "$guest_subnet" -p udp --dport 53 \ - ! -d "$dns" \ - -j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 - iptables -t nat -A PREROUTING -s "$guest_subnet" -p tcp --dport 53 \ - ! -d "$dns" \ - -j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 - - # Redirect all DNS to Pi-hole - iptables -t nat -A PREROUTING -s "$guest_subnet" -p udp --dport 53 -j DNAT --to-destination "${dns}:53" - iptables -t nat -A PREROUTING -s "$guest_subnet" -p tcp --dport 53 -j DNAT --to-destination "${dns}:53" - - log::wg_block "Guest DNS redirected to Pi-hole (${dns}), bypass attempts will be logged" -} - -function fw::remove_dns_redirect() { - local guest_subnet dns - guest_subnet="$(config::subnet_for "guest").0/24" - dns="$(config::dns)" - - iptables -t nat -D PREROUTING -s "$guest_subnet" -p udp --dport 53 \ - ! -d "$dns" \ - -j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 2>/dev/null || true - iptables -t nat -D PREROUTING -s "$guest_subnet" -p tcp --dport 53 \ - ! -d "$dns" \ - -j LOG --log-prefix "wgctl-dns-redirect: " --log-level 4 2>/dev/null || true - iptables -t nat -D PREROUTING -s "$guest_subnet" -p udp --dport 53 -j DNAT --to-destination "${dns}:53" 2>/dev/null || true - iptables -t nat -D PREROUTING -s "$guest_subnet" -p tcp --dport 53 -j DNAT --to-destination "${dns}:53" 2>/dev/null || true - - log::debug "Removed guest DNS redirect" -} - -# ============================================ -# Peer related -# ============================================ - -function fw::list_peer_rules() { - local ip="${1:-}" show_nflog="${2:-false}" - - fw::forward_rules_for_ip "$ip" | while IFS= read -r line; do - [[ -z "$line" ]] && continue - ! $show_nflog && [[ "$line" =~ NFLOG ]] && continue - fw::format_rule "$line" - done -} - -function fw::format_rule() { - local line="${1:-}" - [[ -z "$line" ]] && return 0 - - # Parse verbose iptables format: - # pkts bytes target prot opt in out src dst [extra] - local target prot src dst extra - target=$(awk '{print $3}' <<< "$line") - prot=$(awk '{print $4}' <<< "$line") - src=$(awk '{print $8}' <<< "$line") - dst=$(awk '{print $9}' <<< "$line") - extra=$(awk '{for(i=10;i<=NF;i++) printf $i" "}' <<< "$line" | xargs) - - local prot_name - prot_name=$(fw::proto_name "$prot") - - local dst_fmt="$dst" - if [[ "$extra" =~ dpt:([0-9]+) ]]; then - local port="${BASH_REMATCH[1]}" - dst_fmt="${dst}:${port}:${prot_name}" - fi - - local formatted - formatted=$(printf " %-8s %-15s → %s" "$target" "$src" "$dst_fmt") - ui::firewall_rule "$formatted" -} - -# ============================================ -# Helpers -# ============================================ - -function fw::_nat_exists() { - fw::_rule_exists nat PREROUTING "$@" -} - -function fw::forward_rules_for_ip() { - local ip="${1:-}" - iptables -L FORWARD -n -v /dev/null || true } +# ============================================ +# Display +# ============================================ + +function fw::forward_rules_for_ip() { + local ip="${1:-}" + iptables -L FORWARD -n -v /dev/null +} + function fw::_rule_exists() { local table="${1:-filter}" chain="${2:-FORWARD}" shift 2 iptables -t "$table" -C "$chain" "$@" 2>/dev/null } -function fw::_forward_exists() { - iptables -C FORWARD "$@" 2>/dev/null +function fw::_nat_exists() { + fw::_rule_exists nat PREROUTING "$@" } + +# Core NFLOG+DROP block pair — insert or append +function fw::_block_pair() { + local mode="${1:-insert}" # insert | append + shift + # $@ = match args (no -j) + if [[ "$mode" == "insert" ]]; then + # insert: DROP first at pos 1, NFLOG second at pos 1 → NFLOG ends above DROP + fw::_forward_exists "$@" -j DROP \ + || iptables -I FORWARD 1 "$@" -j DROP + fw::_forward_exists "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ + || iptables -I FORWARD 1 "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" + else + # append: NFLOG first, DROP second → NFLOG ends above DROP + fw::_forward_exists "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ + || iptables -A FORWARD "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" + fw::_forward_exists "$@" -j DROP \ + || iptables -A FORWARD "$@" -j DROP + fi +} + +# Core NFLOG+DROP removal +function fw::_unblock_pair() { + shift 0 # no mode needed for deletion + # $@ = match args + fw::_forward_exists "$@" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ + && iptables -D FORWARD "$@" -j NFLOG \ + --nflog-group 1 --nflog-prefix "wgctl-drop:" 2>/dev/null || true + fw::_forward_exists "$@" -j DROP \ + && iptables -D FORWARD "$@" -j DROP 2>/dev/null || true +} + +# Core ACCEPT insert +function fw::_accept_insert() { + # $@ = match args + fw::_forward_exists "$@" -j ACCEPT \ + || iptables -I FORWARD 1 "$@" -j ACCEPT +} + +# Core ACCEPT removal +function fw::_accept_remove() { + fw::_forward_exists "$@" -j ACCEPT \ + && iptables -D FORWARD "$@" -j ACCEPT 2>/dev/null || true +} + diff --git a/modules/net.module.sh b/modules/net.module.sh new file mode 100644 index 0000000..3eea93c --- /dev/null +++ b/modules/net.module.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +function net::exists() { + local name="${1:?}" + local result + result=$(json::net_exists "$(ctx::net)" "$name") + [[ "$result" == "true" ]] +} + +function net::require_exists() { + local name="${1:?}" + if ! net::exists "$name"; then + log::error "Service not found: ${name}" + return 1 + fi +} + +function net::resolve() { + local name="${1:?}" + json::net_resolve "$(ctx::net)" "$name" +} + +function net::reverse_lookup() { + local ip="${1:-}" port="${2:-}" proto="${3:-}" + [[ -z "$ip" ]] && return 0 + json::net_reverse_lookup "$(ctx::net)" "$ip" "$port" "$proto" +} + +function net::annotation() { + # Returns " → service:port" or "" — for display use + local ip="${1:-}" port="${2:-}" proto="${3:-}" + local match + match=$(net::reverse_lookup "$ip" "$port" "$proto") + [[ -n "$match" ]] && echo " → ${match}" || echo "" +} + +function net::annotate() { + # Returns " → service:port-name" or "" for display use + local entry="${1:-}" + [[ -z "$entry" ]] && return 0 + + local ann="" + if [[ "$entry" == *:*:* ]]; then + # ip:port:proto + local b_ip b_port b_proto + IFS=":" read -r b_ip b_port b_proto <<< "$entry" + ann=$(net::reverse_lookup "$b_ip" "$b_port" "$b_proto") + else + # ip or ip/cidr + local ip="${entry%%/*}" + ann=$(net::reverse_lookup "$ip") + fi + + [[ -n "$ann" ]] && echo "${ann}" || echo "" +} + +# function net::print_entry() { +# local sign="${1:-}" entry="${2:-}" indent="${3:-6}" + +# local ann +# ann=$(net::annotate "$entry") + +# local color +# [[ "$sign" == "+" ]] && color="\033[0;32m" || color="\033[0;31m" + +# local spaces +# spaces=$(printf '%*s' "$indent" '') +# printf "%s%b%s\033[0m %s\033[0;37m%s\033[0m\n" \ +# "$spaces" "$color" "$sign" "$entry" "${ann:+ → ${ann}}" +# } + +function net::print_entry() { + local sign="${1:-}" entry="${2:-}" indent="${3:-6}" + local ann + ann=$(net::annotate "$entry") + local color + [[ "$sign" == "+" ]] && color="\033[0;32m" || color="\033[0;31m" + local spaces + spaces=$(printf '%*s' "$indent" '') + printf "%s%b%s\033[0m %-20s\033[0;37m%s\033[0m\n" \ + "$spaces" "$color" "$sign" "$entry" \ + "${ann:+ → ${ann}}" +} + +function net::print_dns_redirect() { + local ip="${1:-}" indent="${2:-6}" label="${3:-DNS}" + local spaces + spaces=$(printf '%*s' "$indent" '') + local ann + ann=$(net::annotate "$ip") + printf "%s\033[0;36m↺\033[0m %s → %s\033[0;37m%s\033[0m\n" \ + "$spaces" "$label" "$ip" "${ann:+ → ${ann}}" +} + +function net::print_dns_redirect_full() { + # For rule::show — slightly different prefix + local ip="${1:-}" + local ann + ann=$(net::annotate "$ip") + printf " \033[0;36m↺\033[0m Redirect all DNS → %s\033[0;37m%s\033[0m\n" \ + "$ip" "${ann:+ → ${ann}}" +} \ No newline at end of file diff --git a/modules/rule.module.sh b/modules/rule.module.sh index ef6d996..1949c72 100644 --- a/modules/rule.module.sh +++ b/modules/rule.module.sh @@ -288,6 +288,143 @@ function rule::restore_all() { log::wg "Rules restored for all peers" } +# ============================================ +# Rendering +# ============================================ + +function rule::render_flat() { + local rule_name="${1:-}" + + local allow_ports allow_ips block_ips block_ports dns + allow_ports=$(rule::get "$rule_name" "allow_ports") + allow_ips=$(rule::get "$rule_name" "allow_ips") + block_ips=$(rule::get "$rule_name" "block_ips") + block_ports=$(rule::get "$rule_name" "block_ports") + dns=$(rule::get_own "$rule_name" "dns_redirect") + + local has_content=false + [[ -n "${allow_ports}${allow_ips}${block_ips}${block_ports}" ]] && \ + has_content=true + + if ! $has_content; then + printf "\n full access (no restrictions)\n" + return 0 + fi + + if [[ -n "$allow_ports" || -n "$allow_ips" ]]; then + printf "\n" + while IFS= read -r e; do + [[ -z "$e" ]] && continue + net::print_entry "+" "$e" 2 + done <<< "$allow_ports"$'\n'"$allow_ips" + fi + + if [[ -n "$block_ips" || -n "$block_ports" ]]; then + printf "\n" + while IFS= read -r e; do + [[ -z "$e" ]] && continue + net::print_entry "-" "$e" 2 + done <<< "$block_ips"$'\n'"$block_ports" + fi + + [[ "${dns,,}" == "true" ]] && \ + net::print_dns_redirect "$(config::dns)" 6 "DNS" + + return 0 +} + +function rule::render_entries() { + # Renders allow/block entries for a rule name with annotations and DNS + # Usage: rule::render_entries + # indent: 4 for rule show, 4 for inspect (same) + local rule_name="${1:-}" indent="${2:-4}" + local rule_file + rule_file="$(rule::path "$rule_name")" + + local allow_ports allow_ips block_ips block_ports dns + allow_ports=$(rule::get "$rule_name" "allow_ports" 2>/dev/null || true) + allow_ips=$(rule::get "$rule_name" "allow_ips" 2>/dev/null || true) + block_ips=$(rule::get "$rule_name" "block_ips" 2>/dev/null || true) + block_ports=$(rule::get "$rule_name" "block_ports" 2>/dev/null || true) + dns=$(rule::get_own "$rule_name" "dns_redirect") + + while IFS= read -r e; do + [[ -z "$e" ]] && continue + net::print_entry "+" "$e" + done <<< "$allow_ports"$'\n'"$allow_ips" + + while IFS= read -r e; do + [[ -z "$e" ]] && continue + net::print_entry "-" "$e" + done <<< "$block_ips"$'\n'"$block_ports" + + [[ "${dns,,}" == "true" ]] && \ + net::print_dns_redirect "$(config::dns)" 6 "DNS" +} + +function rule::render_own_entries() { + # Renders own (non-inherited) entries for a rule + local rule_name="${1:-}" + local rule_file + rule_file="$(rule::path "$rule_name")" + + local allow_ports allow_ips block_ips block_ports dns + allow_ports=$(json::get "$rule_file" "allow_ports" 2>/dev/null || true) + allow_ips=$(json::get "$rule_file" "allow_ips" 2>/dev/null || true) + block_ips=$(json::get "$rule_file" "block_ips" 2>/dev/null || true) + block_ports=$(json::get "$rule_file" "block_ports" 2>/dev/null || true) + dns=$(json::get "$rule_file" "dns_redirect" 2>/dev/null || true) + + local has_own=false + local combined="${allow_ports}${allow_ips}${block_ips}${block_ports}" + [[ -n "${combined//[$'\n']/}" ]] && has_own=true + + $has_own || return 0 + + while IFS= read -r e; do + [[ -z "$e" ]] && continue + net::print_entry "+" "$e" + done <<< "$allow_ports"$'\n'"$allow_ips" + + while IFS= read -r e; do + [[ -z "$e" ]] && continue + net::print_entry "-" "$e" + done <<< "$block_ips"$'\n'"$block_ports" + + [[ "${dns,,}" == "true" ]] && \ + net::print_dns_redirect "$(config::dns)" 6 "DNS" + + return 0 +} + +function rule::render_extends_tree() { + # Renders full inheritance tree for a rule + local rule_name="${1:-}" + local rule_file + rule_file="$(rule::path "$rule_name")" + + local extends_raw=() + mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) + + [[ ${#extends_raw[@]} -eq 0 || -z "${extends_raw[0]:-}" ]] && return 1 + + for base_name in "${extends_raw[@]}"; do + [[ -z "$base_name" ]] && continue + printf "\n \033[0;37m↳ %s\033[0m\n" "$base_name" + rule::render_entries "$base_name" + done + + # Own rules after inherited + local own_output + own_output=$(rule::render_own_entries "$rule_name") + if [[ -n "$own_output" ]]; then + printf "\n \033[0;37mOwn:\033[0m\n" + printf "%s\n" "$own_output" + fi + + return 0 +} + # ============================================ # DNS Redirect # ============================================ diff --git a/wgctl b/wgctl index a5ac6d4..36e3d43 100755 --- a/wgctl +++ b/wgctl @@ -18,6 +18,7 @@ load_module firewall load_module monitor load_module rule load_module block +load_module net load_module group # ============================================ @@ -51,6 +52,9 @@ declare -A CMD_ALIASES=( [disable]=service ) +block::has_specific_rules "phone-test" && echo "HAS SPECIFIC" || echo "NO SPECIFIC" +block::get_rules "phone-test" + # ============================================ # Dispatch # ============================================