456 lines
No EOL
13 KiB
Bash
456 lines
No EOL
13 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
# ============================================
|
|
# Rule File Parsing
|
|
# ============================================
|
|
|
|
function rule::is_base() {
|
|
local name="${1:-}"
|
|
[[ -f "$(ctx::rules::base)/${name}.rule" ]]
|
|
}
|
|
|
|
function rule::exists() {
|
|
local name="${1:-}"
|
|
local path
|
|
path=$(json::find_rule_file "$(ctx::rules)" "$name")
|
|
[[ -n "$path" ]]
|
|
}
|
|
|
|
function rule::require_assignable() {
|
|
local name="${1:-}"
|
|
if rule::is_base "$name"; then
|
|
log::error "Cannot assign base rule '${name}' — base rules cannot be assigned directly"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
function rule::require_exists() {
|
|
local name="${1:-}"
|
|
if ! rule::exists "$name"; then
|
|
log::error "Rule not found: ${name}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
function rule::get() {
|
|
local name="${1:-}" key="${2:-}"
|
|
json::rule_resolve_field "$(ctx::rules)" "$name" "$key"
|
|
}
|
|
|
|
function rule::get_own() {
|
|
local name="${1:-}" key="${2:-}"
|
|
local file
|
|
file=$(rule::path "$name") || return 0
|
|
json::get_raw "$file" "$key"
|
|
}
|
|
|
|
function rule::get_resolved() {
|
|
local name="${1:-}"
|
|
json::rule_resolve "$(ctx::rules)" "$name"
|
|
}
|
|
|
|
function rule::path() {
|
|
local name="${1:-}"
|
|
local path
|
|
path=$(json::find_rule_file "$(ctx::rules)" "$name")
|
|
[[ -n "$path" ]] && echo "$path" || return 1
|
|
}
|
|
|
|
function rule::get_all() {
|
|
local name="${1:-}"
|
|
rule::get_resolved "$name"
|
|
}
|
|
|
|
function rule::is_applied() {
|
|
local rule_name="${1:-}" client_ip="${2:-}"
|
|
|
|
local first_port
|
|
first_port=$(rule::get "$rule_name" "block_ports" | head -1)
|
|
if [[ -n "$first_port" ]]; then
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$first_port"
|
|
proto="${proto:-tcp}"
|
|
fw::has_block_rule "$client_ip" "$target" "$proto" "$port"
|
|
return $?
|
|
fi
|
|
|
|
local first_ip
|
|
first_ip=$(rule::get "$rule_name" "block_ips" | head -1)
|
|
if [[ -n "$first_ip" ]]; then
|
|
fw::has_block_rule "$client_ip" "$first_ip"
|
|
return $?
|
|
fi
|
|
|
|
local first_allow
|
|
first_allow=$(rule::get "$rule_name" "allow_ports" | head -1)
|
|
if [[ -n "$first_allow" ]]; then
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$first_allow"
|
|
proto="${proto:-tcp}"
|
|
fw::_forward_exists -s "$client_ip" -d "$target" \
|
|
-p "$proto" --dport "$port" -j ACCEPT
|
|
return $?
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# ============================================
|
|
# Rule Application
|
|
# ============================================
|
|
|
|
function rule::apply() {
|
|
local rule_name="${1:?rule_name required}"
|
|
local client_ip="${2:?client_ip required}"
|
|
local peer_name="${3:-}"
|
|
|
|
rule::require_exists "$rule_name" || return 1
|
|
|
|
if [[ -z "$peer_name" ]]; then
|
|
peer_name=$(peers::find_by_ip "$client_ip")
|
|
fi
|
|
|
|
log::debug "rule::apply: peer_name=$peer_name ip=$client_ip"
|
|
|
|
if rule::is_applied "$rule_name" "$client_ip"; then
|
|
log::wg "Rule '${rule_name}' already applied to: ${client_ip}"
|
|
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
|
return 0
|
|
fi
|
|
|
|
# Process block_ips
|
|
while IFS= read -r block_ip; do
|
|
[[ -z "$block_ip" ]] && continue
|
|
fw::block_ip "$client_ip" "$block_ip"
|
|
done < <(rule::get "$rule_name" "block_ips")
|
|
|
|
# Process block_ports
|
|
while IFS= read -r entry; do
|
|
[[ -z "$entry" ]] && continue
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$entry"
|
|
proto="${proto:-tcp}"
|
|
fw::block_port "$client_ip" "$target" "$port" "$proto"
|
|
done < <(rule::get "$rule_name" "block_ports")
|
|
|
|
# Process allow_ips (inserted before blocks)
|
|
while IFS= read -r allow_ip; do
|
|
[[ -z "$allow_ip" ]] && continue
|
|
fw::allow_ip "$client_ip" "$allow_ip"
|
|
done < <(rule::get "$rule_name" "allow_ips")
|
|
|
|
# Process allow_ports (highest priority)
|
|
while IFS= read -r entry; do
|
|
[[ -z "$entry" ]] && continue
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$entry"
|
|
proto="${proto:-tcp}"
|
|
fw::allow_port "$client_ip" "$target" "$port" "$proto"
|
|
done < <(rule::get "$rule_name" "allow_ports")
|
|
|
|
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
|
|
|
|
# DNS redirect
|
|
local dns_redirect
|
|
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
|
|
if [[ "$dns_redirect" == "true" ]]; then
|
|
local peer_subnet
|
|
peer_subnet=$(peers::get_ip "$peer_name" | cut -d'.' -f1-3)
|
|
if ! fw::_nat_exists -i wg0 -s "${peer_subnet}.0/24" \
|
|
-p udp --dport 53 -j DNAT \
|
|
--to-destination "$(config::dns):53" 2>/dev/null; then
|
|
rule::apply_dns_redirect "${peer_subnet}.0/24"
|
|
log::debug "dns_redirect: applied for ${peer_subnet}.0/24"
|
|
else
|
|
log::debug "dns_redirect: already applied for ${peer_subnet}.0/24"
|
|
fi
|
|
fi
|
|
|
|
log::debug "Applied rule '${rule_name}' to: ${client_ip}"
|
|
}
|
|
|
|
function rule::unapply() {
|
|
local rule_name="${1:-}" client_ip="${2:-}"
|
|
|
|
rule::require_exists "$rule_name" || return 1
|
|
|
|
local peer_name
|
|
peer_name=$(peers::find_by_ip "$client_ip")
|
|
|
|
# Remove allow_ports first (reverse order of apply)
|
|
while IFS= read -r entry; do
|
|
[[ -z "$entry" ]] && continue
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$entry"
|
|
proto="${proto:-tcp}"
|
|
fw::unallow_port "$client_ip" "$target" "$port" "$proto"
|
|
done < <(rule::get "$rule_name" "allow_ports")
|
|
|
|
# Remove allow_ips
|
|
while IFS= read -r allow_ip; do
|
|
[[ -z "$allow_ip" ]] && continue
|
|
if [[ "$allow_ip" == *"/"* ]]; then
|
|
fw::unallow_subnet "$client_ip" "$allow_ip"
|
|
else
|
|
fw::unallow_ip "$client_ip" "$allow_ip"
|
|
fi
|
|
done < <(rule::get "$rule_name" "allow_ips")
|
|
|
|
# Remove block_ports
|
|
while IFS= read -r entry; do
|
|
[[ -z "$entry" ]] && continue
|
|
local target port proto
|
|
IFS=":" read -r target port proto <<< "$entry"
|
|
proto="${proto:-tcp}"
|
|
fw::unblock_port "$client_ip" "$target" "$port" "$proto"
|
|
done < <(rule::get "$rule_name" "block_ports")
|
|
|
|
# Remove block_ips
|
|
while IFS= read -r block_ip; do
|
|
[[ -z "$block_ip" ]] && continue
|
|
if [[ "$block_ip" == *"/"* ]]; then
|
|
fw::unblock_subnet "$client_ip" "$block_ip"
|
|
else
|
|
fw::unblock_ip "$client_ip" "$block_ip"
|
|
fi
|
|
done < <(rule::get "$rule_name" "block_ips")
|
|
|
|
# Remove DNS redirect if applicable
|
|
local dns_redirect
|
|
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
|
|
if [[ "$dns_redirect" == "true" ]]; then
|
|
local peer_ip peer_subnet
|
|
peer_ip=$(peers::get_ip "$peer_name")
|
|
peer_subnet=$(echo "$peer_ip" | cut -d'.' -f1-3)
|
|
rule::remove_dns_redirect "${peer_subnet}.0/24"
|
|
fi
|
|
|
|
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" ""
|
|
|
|
log::debug "Removed rule '${rule_name}' from: ${client_ip}"
|
|
}
|
|
|
|
# ============================================
|
|
# Bulk Operations
|
|
# ============================================
|
|
|
|
function rule::_apply_identity_rule() {
|
|
local peer_name="${1:-}" client_ip="${2:-}"
|
|
local identity_name
|
|
identity_name=$(identity::get_name "$peer_name")
|
|
[[ -z "$identity_name" ]] && return 0
|
|
|
|
local identity_rule strict
|
|
identity_rule=$(identity::rule "$identity_name")
|
|
[[ -z "$identity_rule" ]] && return 0
|
|
|
|
strict=$(identity::rule_flags "$identity_name" "strict_rule")
|
|
|
|
if [[ "$strict" == "true" ]]; then
|
|
fw::flush_peer "$client_ip"
|
|
rule::apply "$identity_rule" "$client_ip" "$peer_name"
|
|
else
|
|
rule::apply "$identity_rule" "$client_ip" "$peer_name"
|
|
fi
|
|
}
|
|
|
|
# rule::full_restore_peer <peer_name> <client_ip>
|
|
# Flush and fully restore all fw rules for a peer — rule rules + block rules.
|
|
# Use this instead of calling rule::apply + block::restore_rules_for separately
|
|
# to ensure block rules are never left missing after a flush.
|
|
function rule::full_restore_peer() {
|
|
local peer_name="${1:-}" client_ip="${2:-}"
|
|
[[ -z "$peer_name" || -z "$client_ip" ]] && return 1
|
|
|
|
fw::flush_peer "$client_ip"
|
|
|
|
local peer_rule
|
|
peer_rule=$(peers::get_meta "$peer_name" "rule")
|
|
[[ -n "$peer_rule" ]] && rule::apply "$peer_rule" "$client_ip" "$peer_name"
|
|
|
|
rule::_apply_identity_rule "$peer_name" "$client_ip"
|
|
block::restore_rules_for "$peer_name" "$client_ip"
|
|
}
|
|
|
|
function rule::reapply_all() {
|
|
local rule_name="${1:-}"
|
|
rule::require_exists "$rule_name" || return 1
|
|
|
|
local peers=()
|
|
mapfile -t peers < <(peers::with_rule "$rule_name")
|
|
[[ ${#peers[@]} -eq 0 ]] && return 0
|
|
|
|
local count=0
|
|
for peer_name in "${peers[@]}"; do
|
|
local client_ip
|
|
client_ip=$(peers::get_ip "$peer_name")
|
|
[[ -z "$client_ip" ]] && continue
|
|
rule::full_restore_peer "$peer_name" "$client_ip"
|
|
(( count++ )) || true
|
|
done
|
|
|
|
log::wg_success "Rule '${rule_name}' re-applied to ${count} peers"
|
|
}
|
|
|
|
function rule::restore_all() {
|
|
while IFS= read -r peer_name; do
|
|
block::is_blocked "$peer_name" && continue
|
|
|
|
local rule_name
|
|
rule_name=$(peers::get_meta "$peer_name" "rule")
|
|
[[ -z "$rule_name" ]] && continue
|
|
|
|
if ! rule::exists "$rule_name"; then
|
|
log::wg_warning "Rule '${rule_name}' not found for peer '${peer_name}', skipping"
|
|
continue
|
|
fi
|
|
|
|
local client_ip
|
|
client_ip=$(peers::get_ip "$peer_name")
|
|
[[ -z "$client_ip" ]] && continue
|
|
|
|
# full_restore_peer ensures block rules are restored alongside rule rules
|
|
rule::full_restore_peer "$peer_name" "$client_ip"
|
|
done < <(peers::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() {
|
|
local rule_name="${1:-}" indent="${2:-4}"
|
|
|
|
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() {
|
|
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 combined="${allow_ports}${allow_ips}${block_ips}${block_ports}"
|
|
[[ -z "${combined//[$'\n']/}" ]] && 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() {
|
|
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
|
|
|
|
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
|
|
# ============================================
|
|
|
|
function rule::apply_dns_redirect() {
|
|
local client_subnet="${1:-}"
|
|
fw::nat_add_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
|
}
|
|
|
|
function rule::remove_dns_redirect() {
|
|
local client_subnet="${1:-}"
|
|
fw::nat_remove_dns_redirect "$client_subnet" "$(config::dns)" "$(config::interface)"
|
|
} |