feat: net command, service annotations, block::restore_rules_for, fw refactor, restricted status, block system cleanup

This commit is contained in:
Nuno Duque Nunes 2026-05-15 08:04:06 +00:00
parent cf90ab22db
commit 9a3ac2ae47
14 changed files with 1476 additions and 506 deletions

View file

@ -14,6 +14,9 @@ function cmd::block::on_load() {
flag::register --proto flag::register --proto
flag::register --subnet flag::register --subnet
flag::register --block-name flag::register --block-name
# System - NET Services
flag::register --service
} }
# ============================================ # ============================================
@ -51,13 +54,9 @@ EOF
# ============================================ # ============================================
function cmd::block::run() { function cmd::block::run() {
local name="" local name="" type="" block_name=""
local type="" local ips=() subnets=() ports=() services=()
local block_name="" local quiet=false force=false
local ips=()
local subnets=()
local ports=()
local quiet=false
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@ -65,6 +64,7 @@ function cmd::block::run() {
--type) type="$2"; shift 2 ;; --type) type="$2"; shift 2 ;;
--ip) ips+=("$2"); shift 2 ;; --ip) ips+=("$2"); shift 2 ;;
--block-name) block_name="$2"; shift 2 ;; --block-name) block_name="$2"; shift 2 ;;
--service) services+=("$2"); shift 2 ;;
--force) force=true; shift ;; --force) force=true; shift ;;
--quiet) quiet=true; shift ;; --quiet) quiet=true; shift ;;
--subnet) subnets+=("$2"); shift 2 ;; --subnet) subnets+=("$2"); shift 2 ;;
@ -73,72 +73,100 @@ function cmd::block::run() {
*) *)
log::error "Unknown flag: $1" log::error "Unknown flag: $1"
cmd::block::help cmd::block::help
return 1 return 1 ;;
;;
esac esac
done done
if [[ -z "$name" ]]; then [[ -z "$name" ]] && log::error "Missing required flag: --name" && \
log::error "Missing required flag: --name" cmd::block::help && return 1
cmd::block::help
return 1
fi
name=$(peers::resolve_and_require "$name" "$type") || 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 local client_ip
client_ip=$(peers::get_ip "$name") || return 1 client_ip=$(peers::get_ip "$name") || return 1
# No specific target — block everything # Full block if no specific targets
# Only full block if no specific targets provided if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && \
if [[ ${#ips[@]} -eq 0 && ${#ports[@]} -eq 0 && ${#subnets[@]} -eq 0 ]]; then ${#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" cmd::block::_block_all "$name" "$client_ip" "$quiet"
return 0 return 0
fi fi
# Specific rules — don't full block # Specific rules — check if already fully blocked
block::set_direct "$name" "$client_ip" "false" # ensure not marked as full block 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 # Block specific IPs
for ip in "${ips[@]}"; do for ip in "${ips[@]}"; do
ip::require_valid "$ip" ip::require_valid "$ip"
fw::block_ip "$client_ip" "$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 done
# Block specific subnets # Block specific subnets
for subnet in "${subnets[@]}"; do for subnet in "${subnets[@]}"; do
ip::require_valid "$subnet" ip::require_valid "$subnet"
fw::block_subnet "$client_ip" "$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 done
# Block specific ports # Block specific ports
for entry in "${ports[@]}"; do for entry in "${ports[@]}"; do
local target port proto local b_target b_port b_proto
IFS=":" read -r target port proto <<< "$entry" IFS=":" read -r b_target b_port b_proto <<< "$entry"
ip::require_valid "$target" ip::require_valid "$b_target"
fw::block_port "$client_ip" "$b_target" "$b_port" "${b_proto:-tcp}"
fw::block_port "$client_ip" "$target" "$port" "${proto:-tcp}" block::add_rule "$name" "$client_ip" "port" "${block_name:-}" \
block::add_rule "$name" "$client_ip" "port" "" "$target" "$port" "${proto:-tcp}" "$b_target" "$b_port" "${b_proto:-tcp}"
done 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() { function cmd::block::_get_endpoint() {

View file

@ -56,9 +56,12 @@ function cmd::inspect::_peer_info() {
peers::is_blocked "$name" && is_blocked="true" || is_blocked="false" peers::is_blocked "$name" && is_blocked="true" || is_blocked="false"
last_ts=$(monitor::last_attempt "$name") 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 local status last_seen endpoint
status=$(peers::format_status "$name" "$public_key" \ 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" \ last_seen=$(peers::format_last_seen "$name" "$public_key" \
"$is_blocked" "$last_ts" "" "$handshake_ts") "$is_blocked" "$last_ts" "" "$handshake_ts")
endpoint=$(monitor::get_cached_endpoint "$name") endpoint=$(monitor::get_cached_endpoint "$name")
@ -109,6 +112,107 @@ function cmd::inspect::_peer_info() {
return 0 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() { function cmd::inspect::_rule_info() {
local name="${1:-}" local name="${1:-}"
local rule local rule
@ -118,93 +222,12 @@ function cmd::inspect::_rule_info() {
cmd::inspect::_section "Rule: ${rule}" cmd::inspect::_section "Rule: ${rule}"
local rule_file if rule::render_extends_tree "$rule"; then
rule_file="$(rule::path "$rule")" printf "\n"
# 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
else else
# No inheritance — flat view # No inheritance — flat view
local allow_ports allow_ips block_ips block_ports rule::render_flat "$rule"
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 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)"
fi
return 0 return 0
} }
@ -317,11 +340,13 @@ function cmd::inspect::_firewall_info() {
"$(color::red "-${drops}")" \ "$(color::red "-${drops}")" \
"$(printf '─%.0s' {1..28})" "$(printf '─%.0s' {1..28})"
if [[ ${#rules_output[@]} -gt 0 ]]; then fw::list_peer_rules "$ip" false
for line in "${rules_output[@]}"; do
fw::format_rule "$line" # if [[ ${#rules_output[@]} -gt 0 ]]; then
done # for line in "${rules_output[@]}"; do
fi # fw::format_rule "$line"
# done
# fi
return 0 return 0
} }

View file

@ -381,22 +381,7 @@ function cmd::list::_precompute_all() {
# Block/restricted status # Block/restricted status
declare -gA p_blocked=() p_restricted=() declare -gA p_blocked=() p_restricted=()
local wg_peers cmd::list::_precompute_block_status p_blocked p_restricted
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)
# Public keys # Public keys
declare -gA p_pubkeys=() declare -gA p_pubkeys=()
@ -440,8 +425,7 @@ function cmd::list::_precompute_block_status() {
wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null) wg_peers=$(wg show "$(config::interface)" peers 2>/dev/null)
while IFS= read -r name; do while IFS= read -r name; do
# Restricted = has block rules but still in server (partial block) if block::has_specific_rules "$name" 2>/dev/null; then
if block::is_blocked "$name" 2>/dev/null; then
_restricted["$name"]=true _restricted["$name"]=true
else else
_restricted["$name"]=false _restricted["$name"]=false

299
commands/net.command.sh Normal file
View file

@ -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 <<EOF
Usage: wgctl net <subcommand> [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 <name> Show service details
add --name <name> --ip <ip> Add a service
add --name <svc:port-name> --port <port:proto>
Add a port to a service
rm --name <name> Remove service or port
rm --name <svc:ports> Remove all ports from service
Options for add (service):
--name <name> Service name (e.g. proxmox)
--ip <ip> Service IP address
--desc <description> Optional description
--tag <tag> Optional tag (repeatable)
Options for add (port):
--name <svc:port-name> Service:port-name (e.g. proxmox:web-ui)
--port <port:proto> Port and protocol (e.g. 8006:tcp)
--desc <description> Optional description
Options for list:
--detailed Show ports for each service
--tag <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
}

View file

@ -12,6 +12,8 @@ function cmd::rule::on_load() {
flag::register --remove-extends flag::register --remove-extends
flag::register --block-ip flag::register --block-ip
flag::register --allow-ip flag::register --allow-ip
flag::register --block-service
flag::register --allow-service
flag::register --block-port flag::register --block-port
flag::register --allow-port flag::register --allow-port
flag::register --remove-block-ip flag::register --remove-block-ip
@ -311,15 +313,223 @@ function cmd::rule::list() {
# Show # 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() { 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 while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--name) util::require_flag "--name" "${2:-}" || return 1 --name) util::require_flag "--name" "${2:-}" || return 1
name="$2"; shift 2 ;; name="$2"; shift 2 ;;
--no-peers) show_peers=false; shift ;; --no-peers) show_peers=false; shift ;;
--color) color=true; shift ;;
--resolved) show_resolved=true; shift ;; --resolved) show_resolved=true; shift ;;
--help) cmd::rule::help; return ;; --help) cmd::rule::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;; *) log::error "Unknown flag: $1"; return 1 ;;
@ -332,15 +542,13 @@ function cmd::rule::show() {
local rule_file local rule_file
rule_file="$(rule::path "$name")" 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=$(rule::get_own "$name" "dns_redirect")
dns_redirect="${dns_redirect:-false}" dns_redirect="${dns_redirect:-false}"
local resolved_dns
resolved_dns=$(rule::get "$name" "dns_redirect") resolved_dns=$(rule::get "$name" "dns_redirect")
resolved_dns="${resolved_dns:-false}" resolved_dns="${resolved_dns:-false}"
local dns_display
if [[ "${resolved_dns,,}" == "true" && "${dns_redirect,,}" != "true" ]]; then if [[ "${resolved_dns,,}" == "true" && "${dns_redirect,,}" != "true" ]]; then
dns_display="true (inherited)" dns_display="true (inherited)"
elif [[ "${dns_redirect,,}" == "true" ]]; then elif [[ "${dns_redirect,,}" == "true" ]]; then
@ -349,157 +557,46 @@ function cmd::rule::show() {
dns_display="false" dns_display="false"
fi fi
log::section "Rule: ${name}" log::section "Rule: ${name}"
printf "\n" printf "\n"
# ── Header info ────────────────────────────── local desc group
local desc group dns_redirect
desc=$(json::get "$rule_file" "desc") desc=$(json::get "$rule_file" "desc")
group=$(json::get "$rule_file" "group") group=$(json::get "$rule_file" "group")
ui::row "Description" "${desc:-}" ui::row "Description" "${desc:-}"
ui::row "Group" "${group:-}" ui::row "Group" "${group:-}"
ui::row "DNS" "$dns_display" ui::row "DNS" "$dns_display"
# ── Extends section ────────────────────────── # ── Extends + own rules ────────────────────────
local extends_raw=() if rule::render_extends_tree "$name"; then
mapfile -t extends_raw < <(json::get "$rule_file" "extends" 2>/dev/null || true) # Has inheritance — tree already rendered
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" 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 else
# No inheritance — use Allow/Block sections # No inheritance — flat view
if [[ -n "$own_allow_ports" || -n "$own_allow_ips" ]]; then rule::render_flat "$name"
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 fi
if [[ -n "$own_block_ips" || -n "$own_block_ports" ]]; then # ── Resolved ──────────────────────────────────
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 if $show_resolved; then
cmd::rule::_show_section "Resolved (applied to peers)" cmd::rule::_show_section "Resolved (applied to peers)"
printf "\n"
local res_allow_ports res_allow_ips res_block_ips res_block_ports local res_allow_ports res_allow_ips res_block_ips res_block_ports
res_allow_ports=$(rule::get "$name" "allow_ports") res_allow_ports=$(rule::get "$name" "allow_ports")
res_allow_ips=$(rule::get "$name" "allow_ips") res_allow_ips=$(rule::get "$name" "allow_ips")
res_block_ips=$(rule::get "$name" "block_ips") res_block_ips=$(rule::get "$name" "block_ips")
res_block_ports=$(rule::get "$name" "block_ports") res_block_ports=$(rule::get "$name" "block_ports")
while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "+" "$e"; done \
while IFS= read -r e; do [[ -n "$e" ]] && \ <<< "$res_allow_ports"$'\n'"$res_allow_ips"
printf " \033[0;32m+\033[0m %s\n" "$e"; done <<< "$res_allow_ports" while IFS= read -r e; do [[ -n "$e" ]] && net::print_entry "-" "$e"; done \
while IFS= read -r e; do [[ -n "$e" ]] && \ <<< "$res_block_ips"$'\n'"$res_block_ports"
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 fi
# ── Peers section ───────────────────────────── # ── Peers ─────────────────────────────────────
cmd::rule::_show_section "Peers" cmd::rule::_show_section "Peers"
local peer_list=() local peer_list=()
mapfile -t peer_list < <(peers::with_rule "$name") mapfile -t peer_list < <(peers::with_rule "$name")
local peer_count=${#peer_list[@]} local peer_count=${#peer_list[@]}
ui::row "Assigned" "$peer_count" ui::row "Assigned" "$peer_count"
if $show_peers && [[ $peer_count -gt 0 ]]; then if $show_peers && [[ $peer_count -gt 0 ]]; then
printf "\n" printf "\n"
for peer_name in "${peer_list[@]}"; do for peer_name in "${peer_list[@]}"; do
@ -508,8 +605,8 @@ function cmd::rule::show() {
printf " %-28s %s\n" "$peer_name" "$ip" printf " %-28s %s\n" "$peer_name" "$ip"
done done
fi fi
printf "\n" printf "\n"
return 0
} }
# ============================================ # ============================================
@ -520,6 +617,7 @@ function cmd::rule::add() {
local name="" desc="" group="" local name="" desc="" group=""
local extends=() local extends=()
local allow_ips=() block_ips=() block_ports=() allow_ports=() local allow_ips=() block_ips=() block_ports=() allow_ports=()
local block_services=() allow_services=()
local dns_redirect=false local dns_redirect=false
local is_base=false local is_base=false
@ -537,6 +635,8 @@ function cmd::rule::add() {
--allow-port) allow_ports+=("$2"); shift 2 ;; --allow-port) allow_ports+=("$2"); shift 2 ;;
--block-ip) block_ips+=("$2"); shift 2 ;; --block-ip) block_ips+=("$2"); shift 2 ;;
--block-port) block_ports+=("$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 ;; --dns-redirect) dns_redirect=true; shift ;;
--help) cmd::rule::help; return ;; --help) cmd::rule::help; return ;;
*) *)
@ -566,6 +666,26 @@ function cmd::rule::add() {
rule_dir="$(ctx::rules)" rule_dir="$(ctx::rules)"
fi 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 rule_file="${rule_dir}/${name}.rule"
local allow_str block_str port_str allow_port_str extends_str local allow_str block_str port_str allow_port_str extends_str

View file

@ -204,6 +204,34 @@ function cmd::test::section_fw() {
cmd::test::run_cmd "fw nat" "PREROUTING" fw nat cmd::test::run_cmd "fw nat" "PREROUTING" fw nat
cmd::test::run_cmd "fw count" "TOTAL" fw count 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() { function cmd::test::section_destructive() {
test::section "Destructive (modifying state)" test::section "Destructive (modifying state)"
@ -432,6 +460,7 @@ function cmd::test::run() {
audit) cmd::test::section_audit ;; audit) cmd::test::section_audit ;;
logs) cmd::test::section_logs ;; logs) cmd::test::section_logs ;;
fw) cmd::test::section_fw ;; fw) cmd::test::section_fw ;;
net) cmd::test::section_net ;;
destructive) cmd::test::section_destructive ;; destructive) cmd::test::section_destructive ;;
*) *)
log::error "Unknown section: $section" log::error "Unknown section: $section"

View file

@ -22,6 +22,7 @@ _CTX_GROUPS="${_CTX_DATA}/groups"
_CTX_BLOCKS="${_CTX_DATA}/blocks" _CTX_BLOCKS="${_CTX_DATA}/blocks"
_CTX_META="${_CTX_DATA}/meta" _CTX_META="${_CTX_DATA}/meta"
_CTX_DAEMON="${_CTX_DATA}/daemon" _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::blocks() { echo "$_CTX_BLOCKS"; }
function ctx::meta() { echo "$_CTX_META"; } function ctx::meta() { echo "$_CTX_META"; }
function ctx::daemon() { echo "$_CTX_DAEMON"; } function ctx::daemon() { echo "$_CTX_DAEMON"; }
function ctx::net() { echo "$_CTX_NET"; }
function ctx::events_log() { echo "$(ctx::daemon)/events.log"; } function ctx::events_log() { echo "$(ctx::daemon)/events.log"; }
function ctx::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; } function ctx::fw_events_log() { echo "$(ctx::daemon)/fw_events.log"; }

View file

@ -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 "$@" </dev/null; } function json::block_get_rules() { python3 "$JSON_HELPER" block_get_rules "$@" </dev/null; }
function json::block_get_groups() { python3 "$JSON_HELPER" block_get_groups "$@" </dev/null; } function json::block_get_groups() { python3 "$JSON_HELPER" block_get_groups "$@" </dev/null; }
function json::block_get_direct() { python3 "$JSON_HELPER" block_get_direct "$@" </dev/null; } function json::block_get_direct() { python3 "$JSON_HELPER" block_get_direct "$@" </dev/null; }
function json::net_list() { python3 "$JSON_HELPER" net_list "$@" </dev/null; }
function json::net_show() { python3 "$JSON_HELPER" net_show "$@" </dev/null; }
function json::net_exists() { python3 "$JSON_HELPER" net_exists "$@" </dev/null; }
function json::net_add_service() { python3 "$JSON_HELPER" net_add_service "$@" </dev/null; }
function json::net_add_port() { python3 "$JSON_HELPER" net_add_port "$@" </dev/null; }
function json::net_remove() { python3 "$JSON_HELPER" net_remove "$@" </dev/null; }
function json::net_resolve() { python3 "$JSON_HELPER" net_resolve "$@" </dev/null; }
function json::net_reverse_lookup() { python3 "$JSON_HELPER" net_reverse_lookup "$@" </dev/null; }
function json::block_is_empty() { python3 "$JSON_HELPER" block_is_empty "$@" </dev/null; }
function json::peer_transfer() { function json::peer_transfer() {
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \ ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \

View file

@ -1312,6 +1312,164 @@ def block_get_direct(file):
return return
print('true' if data.get('blocked_direct', False) else 'false') print('true' if data.get('blocked_direct', False) else 'false')
# ============================================
# Net / Services
# ============================================
def _net_read(file):
"""Read services.json, return dict or empty dict"""
try:
if not os.path.exists(file):
return {}
with open(file) as f:
content = f.read().strip()
if not content:
return {}
return json.loads(content)
except Exception:
return {}
def _net_write(file, data):
"""Write services.json"""
os.makedirs(os.path.dirname(file), exist_ok=True)
with open(file, 'w') as f:
json.dump(data, f, indent=2)
def net_list(file):
"""List all service names with IP and port count"""
data = _net_read(file)
for name, svc in sorted(data.items()):
ip = svc.get('ip', '')
desc = svc.get('desc', '')
tags = ','.join(svc.get('tags', []))
ports = len(svc.get('ports', {}))
print(f"{name}|{ip}|{desc}|{tags}|{ports}")
def net_show(file, name):
"""Show full service details"""
data = _net_read(file)
if name not in data:
print(f"Error: Service not found: {name}", file=sys.stderr)
sys.exit(1)
svc = data[name]
print(f"name|{name}")
print(f"ip|{svc.get('ip','')}")
print(f"desc|{svc.get('desc','')}")
print(f"tags|{','.join(svc.get('tags',[]))}")
for port_name, port_def in svc.get('ports', {}).items():
port = port_def.get('port', '')
proto = port_def.get('proto', 'tcp')
desc = port_def.get('desc', '')
print(f"port|{port_name}|{port}|{proto}|{desc}")
def net_exists(file, name):
"""Check if service exists"""
data = _net_read(file)
# Handle service:port syntax
if ':' in name:
svc_name, port_name = name.split(':', 1)
if port_name == 'ports':
print('true' if svc_name in data else 'false')
else:
svc = data.get(svc_name, {})
print('true' if port_name in svc.get('ports', {}) else 'false')
else:
print('true' if name in data else 'false')
def net_add_service(file, name, ip, desc='', tags=''):
"""Add or update a service"""
data = _net_read(file)
if name not in data:
data[name] = {'ip': ip, 'ports': {}}
else:
data[name]['ip'] = ip
if desc:
data[name]['desc'] = desc
if tags:
data[name]['tags'] = [t.strip() for t in tags.split(',') if t.strip()]
_net_write(file, data)
def net_add_port(file, service, port_name, port, proto='tcp', desc=''):
"""Add or update a port on a service"""
data = _net_read(file)
if service not in data:
print(f"Error: Service not found: {service}", file=sys.stderr)
sys.exit(1)
if 'ports' not in data[service]:
data[service]['ports'] = {}
entry = {'port': int(port), 'proto': proto}
if desc:
entry['desc'] = desc
data[service]['ports'][port_name] = entry
_net_write(file, data)
def net_remove(file, name):
"""Remove service or port"""
data = _net_read(file)
if ':' in name:
svc_name, port_name = name.split(':', 1)
if svc_name not in data:
print(f"Error: Service not found: {svc_name}", file=sys.stderr)
sys.exit(1)
if port_name == 'ports':
# Remove all ports
data[svc_name]['ports'] = {}
else:
if port_name not in data[svc_name].get('ports', {}):
print(f"Error: Port not found: {port_name}", file=sys.stderr)
sys.exit(1)
del data[svc_name]['ports'][port_name]
else:
if name not in data:
print(f"Error: Service not found: {name}", file=sys.stderr)
sys.exit(1)
del data[name]
_net_write(file, data)
def net_resolve(file, name):
"""Resolve service name to ip or ip:port:proto lines"""
data = _net_read(file)
if ':' in name:
svc_name, port_name = name.split(':', 1)
if svc_name not in data:
print(f"Error: Service not found: {svc_name}", file=sys.stderr)
sys.exit(1)
svc = data[svc_name]
ip = svc.get('ip', '')
if port_name == 'ports':
# All ports
for pname, pdef in svc.get('ports', {}).items():
print(f"{ip}:{pdef['port']}:{pdef.get('proto','tcp')}")
else:
if port_name not in svc.get('ports', {}):
print(f"Error: Port not found: {port_name}", file=sys.stderr)
sys.exit(1)
pdef = svc['ports'][port_name]
print(f"{ip}:{pdef['port']}:{pdef.get('proto','tcp')}")
else:
if name not in data:
print(f"Error: Service not found: {name}", file=sys.stderr)
sys.exit(1)
print(data[name].get('ip', ''))
def net_reverse_lookup(file, ip, port='', proto=''):
"""Reverse lookup IP/port to service name"""
data = _net_read(file)
for svc_name, svc in data.items():
if svc.get('ip') != ip:
continue
if not port:
print(svc_name)
return
for port_name, port_def in svc.get('ports', {}).items():
if (str(port_def.get('port','')) == str(port) and
port_def.get('proto','tcp') == proto):
print(f"{svc_name}:{port_name}")
return
# IP matched but no port match — return service name
print(svc_name)
return
commands = { commands = {
'get': lambda args: get(args[0], args[1]), 'get': lambda args: get(args[0], args[1]),
'set': lambda args: set_key(args[0], args[1], args[2]), 'set': lambda args: set_key(args[0], args[1], args[2]),
@ -1381,6 +1539,27 @@ commands = {
'block_get_rules': lambda args: block_get_rules(args[0]), 'block_get_rules': lambda args: block_get_rules(args[0]),
'block_get_groups': lambda args: block_get_groups(args[0]), 'block_get_groups': lambda args: block_get_groups(args[0]),
'block_get_direct': lambda args: block_get_direct(args[0]), 'block_get_direct': lambda args: block_get_direct(args[0]),
'net_list': lambda args: net_list(args[0]),
'net_show': lambda args: net_show(args[0], args[1]),
'net_exists': lambda args: net_exists(args[0], args[1]),
'net_add_service': lambda args: net_add_service(
args[0], args[1], args[2],
args[3] if len(args) > 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__': if __name__ == '__main__':

View file

@ -16,6 +16,16 @@ function block::has_file() {
[[ -f "$(block::file "$name")" ]] [[ -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() { function block::is_blocked() {
local name="${1:?}" local name="${1:?}"
block::has_file "$name" || return 1 block::has_file "$name" || return 1
@ -128,6 +138,19 @@ function block::restore_peer() {
return 0 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() { function block::restore_all() {
while IFS= read -r peer_name; do while IFS= read -r peer_name; do
block::has_file "$peer_name" || continue block::has_file "$peer_name" || continue
@ -151,17 +174,68 @@ function block::restore_all() {
function block::format_rules() { function block::format_rules() {
local name="${1:?}" local name="${1:?}"
block::has_file "$name" || return 0 block::has_file "$name" || return 0
while IFS="|" read -r bname btype target port proto; do while IFS="|" read -r bname btype target port proto; do
[[ -z "$btype" ]] && continue [[ -z "$btype" ]] && continue
local display
local display="" ann=""
case "$btype" in case "$btype" in
full) display="all traffic" ;; full)
ip) display="$target" ;; display="all traffic"
port) display="${target}:${port}:${proto}" ;; ;;
subnet) display="$target" ;; 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 esac
local label="${bname:-$btype}"
printf " \033[0;31m-\033[0m %-30s \033[0;37m%s\033[0m\n" \ local label="$bname"
"$display" "$label"
# 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") done < <(block::get_rules "$name")
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")
# }

View file

@ -12,143 +12,108 @@ function fw::on_load() {
# Rule Management # Rule Management
# ============================================ # ============================================
# ============================================
# Block / Unblock
function fw::block_ip() { function fw::block_ip() {
local client_ip="${1:-}" target_ip="${2:-}" local client_ip="${1:-}" target_ip="${2:-}" mode="${3:-insert}"
fw::_block_pair "$mode" -s "$client_ip" -d "$target_ip"
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:"
} }
function fw::unblock_ip() { function fw::unblock_ip() {
local client_ip="${1:-}" target_ip="${2:-}" local client_ip="${1:-}" target_ip="${2:-}"
fw::_unblock_pair -s "$client_ip" -d "$target_ip"
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
} }
function fw::block_port() { function fw::block_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" \
proto="${4:-tcp}" mode="${5:-insert}"
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP \ fw::_block_pair "$mode" \
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j DROP -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
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:"
} }
function fw::unblock_port() { function fw::unblock_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
fw::_unblock_pair \
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j NFLOG --nflog-group 1 --nflog-prefix "wgctl-drop:" \ -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
&& 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}"
} }
function fw::block_subnet() { function fw::block_subnet() {
local client_ip="${1:-}" target_subnet="${2:-}" local client_ip="${1:-}" target_subnet="${2:-}" mode="${3:-append}"
fw::_block_pair "$mode" -s "$client_ip" -d "$target_subnet"
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
log::wg_block "Blocked ${client_ip} → subnet ${target_subnet}" log::wg_block "Blocked ${client_ip} → subnet ${target_subnet}"
} }
function fw::unblock_subnet() { function fw::unblock_subnet() {
local client_ip="${1:-}" target_subnet="${2:-}" local client_ip="${1:-}" target_subnet="${2:-}"
fw::_unblock_pair -s "$client_ip" -d "$target_subnet"
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
log::wg_unblock "Unblocked ${client_ip} → subnet ${target_subnet}" log::wg_unblock "Unblocked ${client_ip} → subnet ${target_subnet}"
} }
function fw::allow_subnet() { function fw::block_all() {
local client_ip="${1:-}" target_subnet="${2:-}" local client_ip="${1:-}" client_name="${2:-}"
fw::_forward_exists -s "$client_ip" -j DROP \
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \ || iptables -A FORWARD -s "$client_ip" -j DROP
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_subnet" -j ACCEPT log::debug "Blocked all traffic from: ${client_ip}"
} }
function fw::unallow_subnet() { function fw::unblock_all() {
local client_ip="${1:-}" target_subnet="${2:-}" local client_ip="${1:-}"
fw::_forward_exists -s "$client_ip" -j DROP \
fw::_forward_exists -s "$client_ip" -d "$target_subnet" -j ACCEPT \ && iptables -D FORWARD -s "$client_ip" -j DROP 2>/dev/null || true
&& iptables -D FORWARD -s "$client_ip" -d "$target_subnet" -j ACCEPT 2>/dev/null || true monitor::unwatch "$client_ip"
log::debug "Unblocked all traffic from: ${client_ip}"
} }
# ============================================
# Allow / Unallow
function fw::allow_ip() { function fw::allow_ip() {
local client_ip="${1:-}" target_ip="${2:-}" local client_ip="${1:-}" target_ip="${2:-}"
fw::_accept_insert -s "$client_ip" -d "$target_ip"
fw::_forward_exists -s "$client_ip" -d "$target_ip" -j ACCEPT \
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -j ACCEPT
} }
function fw::unallow_ip() { function fw::unallow_ip() {
local client_ip="${1:-}" target_ip="${2:-}" 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 \ function fw::allow_subnet() {
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -j ACCEPT 2>/dev/null || true 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() { function fw::allow_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
fw::_accept_insert \
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT \ -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
|| iptables -I FORWARD 1 -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT
} }
function fw::unallow_port() { function fw::unallow_port() {
local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}" local client_ip="${1:-}" target_ip="${2:-}" port="${3:-}" proto="${4:-tcp}"
fw::_accept_remove \
fw::_forward_exists -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT \ -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port"
&& iptables -D FORWARD -s "$client_ip" -d "$target_ip" -p "$proto" --dport "$port" -j ACCEPT 2>/dev/null || true
} }
# ============================================
# Flush
# ============================================
function fw::flush_peer() { function fw::flush_peer() {
local client_ip="${1:?client_ip required}" local client_ip="${1:?client_ip required}"
log::debug "flush_peer: starting for $client_ip" log::debug "flush_peer: starting for $client_ip"
# Collect line numbers into array
local linenums=() local linenums=()
while IFS= read -r linenum; do while IFS= read -r linenum; do
[[ -n "$linenum" ]] && linenums+=("$linenum") [[ -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 count=0
local i local i
for (( i=${#linenums[@]}-1; i>=0; i-- )); do 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" 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 | grep -F "$ip"
}
function fw::proto_name() {
local proto="${1:-0}"
case "$proto" in
6) echo "tcp" ;;
17) echo "udp" ;;
1) echo "icmp" ;;
0) echo "all" ;;
tcp|udp|icmp|all) echo "$proto" ;;
*) echo "$proto" ;;
esac
}
function fw::flush_forward() { function fw::flush_forward() {
iptables -F FORWARD iptables -F FORWARD
log::debug "Flushed FORWARD chain" log::debug "Flushed FORWARD chain"
@ -286,6 +139,10 @@ function fw::flush_all() {
fw::flush_nat fw::flush_nat
} }
# ============================================
# NAT / DNS Redirect
# ============================================
function fw::nat_add_dns_redirect() { function fw::nat_add_dns_redirect() {
local subnet="${1:-}" dns="${2:-}" interface="${3:-wg0}" local subnet="${1:-}" dns="${2:-}" interface="${3:-wg0}"
iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \ iptables -t nat -A PREROUTING -i "$interface" -s "$subnet" \
@ -311,16 +168,137 @@ function fw::nat_remove_dns_redirect() {
--to-destination "${dns}:53" 2>/dev/null || true --to-destination "${dns}:53" 2>/dev/null || true
} }
# ============================================
# Display
# ============================================
function fw::forward_rules_for_ip() {
local ip="${1:-}"
iptables -L FORWARD -n -v </dev/null | grep -F "$ip"
}
function fw::proto_name() {
local proto="${1:-0}"
case "$proto" in
6) echo "tcp" ;;
17) echo "udp" ;;
1) echo "icmp" ;;
0) echo "all" ;;
tcp|udp|icmp|all) echo "$proto" ;;
*) echo "$proto" ;;
esac
}
function fw::format_rule() {
local line="${1:-}"
[[ -z "$line" ]] && return 0
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"
}
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
}
# ============================================
# Counts
# ============================================
function fw::count_peer_rules() {
local ip="${1:-}"
local total=0 accepts=0 drops=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
[[ "$line" =~ NFLOG ]] && continue
(( total++ )) || true
[[ "$line" =~ ACCEPT ]] && (( accepts++ )) || true
[[ "$line" =~ DROP ]] && (( drops++ )) || true
done < <(fw::forward_rules_for_ip "$ip")
echo "${total}|${accepts}|${drops}"
}
# ============================================ # ============================================
# private # private
# ============================================ # ============================================
function fw::_forward_exists() {
iptables -C FORWARD "$@" 2>/dev/null
}
function fw::_rule_exists() { function fw::_rule_exists() {
local table="${1:-filter}" chain="${2:-FORWARD}" local table="${1:-filter}" chain="${2:-FORWARD}"
shift 2 shift 2
iptables -t "$table" -C "$chain" "$@" 2>/dev/null iptables -t "$table" -C "$chain" "$@" 2>/dev/null
} }
function fw::_forward_exists() { function fw::_nat_exists() {
iptables -C FORWARD "$@" 2>/dev/null 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
}

102
modules/net.module.sh Normal file
View file

@ -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}}"
}

View file

@ -288,6 +288,143 @@ function rule::restore_all() {
log::wg "Rules restored for all peers" 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 <rule_name> <indent>
# 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 # DNS Redirect
# ============================================ # ============================================

4
wgctl
View file

@ -18,6 +18,7 @@ load_module firewall
load_module monitor load_module monitor
load_module rule load_module rule
load_module block load_module block
load_module net
load_module group load_module group
# ============================================ # ============================================
@ -51,6 +52,9 @@ declare -A CMD_ALIASES=(
[disable]=service [disable]=service
) )
block::has_specific_rules "phone-test" && echo "HAS SPECIFIC" || echo "NO SPECIFIC"
block::get_rules "phone-test"
# ============================================ # ============================================
# Dispatch # Dispatch
# ============================================ # ============================================