wgctl/modules/rule.module.sh

357 lines
No EOL
10 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_entries() {
local rule_name="${1:?}" client_ip="${2:?}"
rule::require_exists "$rule_name" || return 1
if rule::is_applied "$rule_name" "$client_ip"; then
log::debug "Rule '${rule_name}' already applied to: ${client_ip}"
return 0
fi
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")
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")
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")
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")
local dns_redirect
dns_redirect=$(rule::get "$rule_name" "dns_redirect")
if [[ "$dns_redirect" == "true" ]]; then
local peer_name peer_subnet
peer_name=$(peers::find_by_ip "$client_ip")
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"
fi
fi
log::debug "Applied rule '${rule_name}' to: ${client_ip}"
}
function rule::apply_transient() {
# Apply rule entries without touching peer meta
# Used for identity rules and other transient applications
local rule_name="${1:?}" client_ip="${2:?}"
log::debug "rule::apply_transient: $rule_name -> $client_ip"
rule::_apply_entries "$rule_name" "$client_ip"
}
function rule::apply() {
local rule_name="${1:?}" client_ip="${2:?}" peer_name="${3:-}"
rule::require_exists "$rule_name" || return 1
log::debug "rule::apply: peer_name=${peer_name:-<lookup>} ip=$client_ip"
rule::_apply_entries "$rule_name" "$client_ip" || return 1
# Write to peer meta — only for explicit peer rule assignment
if [[ -z "$peer_name" ]]; then
peer_name=$(peers::find_by_ip "$client_ip")
fi
[[ -n "$peer_name" ]] && peers::set_meta "$peer_name" "rule" "$rule_name"
}
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 rules
rules=$(identity::rules "$identity_name")
[[ -z "$rules" ]] && return 0
local strict
strict=$(identity::rule_flags "$identity_name" "strict_rule")
if [[ "$strict" == "true" ]]; then
# Strict: flush and apply only identity rules — peer rule ignored
fw::flush_peer "$client_ip"
while IFS= read -r rule_name; do
[[ -z "$rule_name" ]] && continue
rule::exists "$rule_name" && rule::apply_transient "$rule_name" "$client_ip" || true
done <<< "$rules"
else
# Additive: apply identity rules on top of peer rule
while IFS= read -r rule_name; do
[[ -z "$rule_name" ]] && continue
rule::exists "$rule_name" && rule::apply_transient "$rule_name" "$client_ip" || true
done <<< "$rules"
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")
local strict
strict=$(rule::_get_identity_strict "$peer_name")
if [[ "$strict" == "true" ]]; then
# Strict mode: only identity rules apply
rule::_apply_identity_rule "$peer_name" "$client_ip"
else
# Normal mode: peer rule + identity rules (additive)
[[ -n "$peer_rule" ]] && rule::apply "$peer_rule" "$client_ip" "$peer_name"
rule::_apply_identity_rule "$peer_name" "$client_ip"
fi
block::restore_rules_for "$peer_name" "$client_ip"
}
function rule::_get_identity_strict() {
local peer_name="${1:-}"
local identity_name
identity_name=$(identity::get_name "$peer_name")
[[ -z "$identity_name" ]] && echo "false" && return 0
identity::rule_flags "$identity_name" "strict_rule"
}
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
# ============================================
# ======================================================
# Aliases (for backward compat — remove in cleanup pass)
# ======================================================
function rule::render_flat() { ui::rule::flat "$1"; }
function rule::render_entries() { ui::rule::entries "$1"; }
function rule::render_own_entries() { ui::rule::own_entries "$1"; }
function rule::render_extends_tree() { ui::rule::tree "$1"; }
# ============================================
# 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)"
}