feat: migrate policy firewall from iptables/ipset to nftables
- Rewrite wg-policy-lib.sh: replace ipset functions with nft helpers, add backup_nftables() - Rewrite wg-policy-engine.sh: generate nft ruleset file, atomic load via nft -f, rollback support - Simplify wg-policy-cleanup.sh: single nft delete table inet wg_policy - Update wg-policy-ctl: nft commands for rules/ipset/stats/backup - Rebuild install.sh: nftables dependency, WireGuard pre-check, PostUp/PostDown auto-integration - Update build.sh/build.bat: match install.sh changes - Update README.md: nftables prerequisites and references
This commit is contained in:
+35
-78
@@ -8,16 +8,16 @@ set -euo pipefail
|
||||
# CONFIGURATION
|
||||
# ============================================================
|
||||
readonly WG_IF="${WG_IF:-wg0}"
|
||||
readonly CHAIN="WG_POLICY"
|
||||
readonly CHAIN_BACKUP="WG_POLICY_BAK"
|
||||
readonly NFT_TABLE="wg_policy"
|
||||
readonly NFT_TABLE_FULL="inet wg_policy"
|
||||
readonly NFT_SET_V4="wg_allowed_v4"
|
||||
readonly NFT_SET_V6="wg_allowed_v6"
|
||||
readonly POLICY_FILE="/etc/wireguard/policy.json"
|
||||
readonly WG_CONF="/etc/wireguard/wg0.conf"
|
||||
readonly LOCK_FILE="/var/lock/wg-policy.lock"
|
||||
readonly BACKUP_DIR="/etc/wireguard/backups"
|
||||
readonly LOG_PREFIX="WG_DROP"
|
||||
readonly LOG_RATE="10/min"
|
||||
readonly IPSET_V4="wg_allowed_v4"
|
||||
readonly IPSET_V6="wg_allowed_v6"
|
||||
readonly LOG_RATE="10/minute"
|
||||
readonly MAX_RETRY=3
|
||||
readonly RETRY_DELAY=2
|
||||
readonly DEBOUNCE_SEC=2
|
||||
@@ -33,17 +33,14 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; }
|
||||
# VALIDATION
|
||||
# ============================================================
|
||||
|
||||
# Validate IPv4 address (strict: 0-255 per octet, no leading zeros)
|
||||
validate_ipv4() {
|
||||
local ip="$1"
|
||||
# Match basic pattern
|
||||
if [[ ! "$ip" =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
local IFS='.'
|
||||
read -ra octets <<< "$ip"
|
||||
for octet in "${octets[@]}"; do
|
||||
# Reject leading zeros (except "0" itself)
|
||||
if [[ "$octet" =~ ^0[0-9] ]]; then
|
||||
return 1
|
||||
fi
|
||||
@@ -54,7 +51,6 @@ validate_ipv4() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate IPv4 CIDR (e.g., 192.168.1.0/24)
|
||||
validate_ipv4_cidr() {
|
||||
local cidr="$1"
|
||||
local ip prefix
|
||||
@@ -63,7 +59,6 @@ validate_ipv4_cidr() {
|
||||
ip="${cidr%%/*}"
|
||||
prefix="${cidr##*/}"
|
||||
else
|
||||
# Single IP treated as /32
|
||||
ip="$cidr"
|
||||
prefix="32"
|
||||
fi
|
||||
@@ -78,10 +73,8 @@ validate_ipv4_cidr() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Validate IPv6 address (basic check)
|
||||
validate_ipv6() {
|
||||
local ip="$1"
|
||||
# Basic IPv6 pattern — covers full, compressed, and mixed notation
|
||||
if [[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$ ]] || \
|
||||
[[ "$ip" =~ ^::([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$ ]] || \
|
||||
[[ "$ip" =~ ^([0-9a-fA-F]{0,4}:){1,7}:$ ]] || \
|
||||
@@ -92,7 +85,6 @@ validate_ipv6() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Validate IPv6 CIDR
|
||||
validate_ipv6_cidr() {
|
||||
local cidr="$1"
|
||||
local ip prefix
|
||||
@@ -115,7 +107,6 @@ validate_ipv6_cidr() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Generic CIDR validator — dispatches to v4 or v6
|
||||
validate_cidr() {
|
||||
local cidr="$1"
|
||||
if [[ "$cidr" == *":"* ]]; then
|
||||
@@ -126,36 +117,11 @@ validate_cidr() {
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# IPSET MANAGEMENT
|
||||
# NFTABLES HELPERS
|
||||
# ============================================================
|
||||
|
||||
has_ipset() {
|
||||
command -v ipset &>/dev/null
|
||||
}
|
||||
|
||||
ensure_ipset() {
|
||||
local name="$1" family="$2"
|
||||
has_ipset || return 0
|
||||
if ! ipset list "$name" &>/dev/null; then
|
||||
ipset create "$name" hash:net,net family "$family" hashsize 1024 maxelem 65536 timeout 0
|
||||
log_info "Created ipset: $name (family=$family)"
|
||||
fi
|
||||
}
|
||||
|
||||
flush_ipset() {
|
||||
local name="$1"
|
||||
has_ipset || return 0
|
||||
if ipset list "$name" &>/dev/null; then
|
||||
ipset flush "$name"
|
||||
fi
|
||||
}
|
||||
|
||||
destroy_ipset() {
|
||||
local name="$1"
|
||||
has_ipset || return 0
|
||||
if ipset list "$name" &>/dev/null; then
|
||||
ipset destroy "$name"
|
||||
fi
|
||||
has_nft() {
|
||||
command -v nft &>/dev/null
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -176,7 +142,7 @@ retry() {
|
||||
log_warn "Attempt $attempt/$max_attempts failed (exit=$exit_code), retrying in ${delay}s..."
|
||||
sleep "$delay"
|
||||
(( attempt++ ))
|
||||
(( delay *= 2 )) # exponential backoff
|
||||
(( delay *= 2 ))
|
||||
done
|
||||
|
||||
log_error "All $max_attempts attempts failed for: $*"
|
||||
@@ -198,7 +164,6 @@ acquire_lock() {
|
||||
}
|
||||
|
||||
release_lock() {
|
||||
# Lock released automatically when fd closes, but we clean up file
|
||||
rm -f "$LOCK_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
@@ -216,7 +181,6 @@ backup_policy() {
|
||||
log_info "Backup created: ${BACKUP_DIR}/policy_${timestamp}.json"
|
||||
fi
|
||||
|
||||
# Keep only last 50 backups
|
||||
local count
|
||||
count=$(find "$BACKUP_DIR" -name 'policy_*.json' -type f | wc -l)
|
||||
if (( count > 50 )); then
|
||||
@@ -229,31 +193,24 @@ backup_policy() {
|
||||
fi
|
||||
}
|
||||
|
||||
backup_iptables() {
|
||||
backup_nftables() {
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
local timestamp
|
||||
timestamp="$(date '+%Y%m%d_%H%M%S')"
|
||||
|
||||
if iptables-save > "${BACKUP_DIR}/iptables_${timestamp}.rules" 2>/dev/null; then
|
||||
log_info "iptables backup: ${BACKUP_DIR}/iptables_${timestamp}.rules"
|
||||
if nft list ruleset > "${BACKUP_DIR}/nftables_${timestamp}.rules" 2>/dev/null; then
|
||||
log_info "nftables backup: ${BACKUP_DIR}/nftables_${timestamp}.rules"
|
||||
fi
|
||||
|
||||
if command -v ip6tables-save &>/dev/null; then
|
||||
ip6tables-save > "${BACKUP_DIR}/ip6tables_${timestamp}.rules" 2>/dev/null || true
|
||||
local count
|
||||
count=$(find "$BACKUP_DIR" -name 'nftables_*.rules' -type f | wc -l)
|
||||
if (( count > 20 )); then
|
||||
find "$BACKUP_DIR" -name 'nftables_*.rules' -type f -printf '%T@ %p\n' \
|
||||
| sort -n \
|
||||
| head -n $(( count - 20 )) \
|
||||
| awk '{print $2}' \
|
||||
| xargs rm -f
|
||||
fi
|
||||
|
||||
# Keep only last 20 iptables backups
|
||||
for prefix in iptables ip6tables; do
|
||||
local count
|
||||
count=$(find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f | wc -l)
|
||||
if (( count > 20 )); then
|
||||
find "$BACKUP_DIR" -name "${prefix}_*.rules" -type f -printf '%T@ %p\n' \
|
||||
| sort -n \
|
||||
| head -n $(( count - 20 )) \
|
||||
| awk '{print $2}' \
|
||||
| xargs rm -f
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
@@ -314,32 +271,32 @@ health_check() {
|
||||
status=1
|
||||
fi
|
||||
|
||||
# 3. Check chain exists
|
||||
if iptables -L "$CHAIN" -n &>/dev/null; then
|
||||
# 3. Check nftables table exists
|
||||
if nft list table "$NFT_TABLE_FULL" &>/dev/null; then
|
||||
local rule_count
|
||||
rule_count=$(iptables -L "$CHAIN" -n 2>/dev/null | tail -n +3 | wc -l)
|
||||
report+="[OK] Chain $CHAIN active ($rule_count rules)\n"
|
||||
rule_count=$(nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '^\s*' || echo 0)
|
||||
report+="[OK] Table $NFT_TABLE active ($rule_count rules)\n"
|
||||
else
|
||||
report+="[WARN] Chain $CHAIN not found\n"
|
||||
report+="[WARN] Table $NFT_TABLE not found\n"
|
||||
status=1
|
||||
fi
|
||||
|
||||
# 4. Check FORWARD reference
|
||||
if iptables -L FORWARD -n 2>/dev/null | grep -q "$CHAIN"; then
|
||||
report+="[OK] FORWARD chain references $CHAIN\n"
|
||||
# 4. Check forward chain exists in table
|
||||
if nft list chain "$NFT_TABLE_FULL" forward &>/dev/null; then
|
||||
report+="[OK] Forward chain exists in $NFT_TABLE\n"
|
||||
else
|
||||
report+="[WARN] FORWARD chain has no reference to $CHAIN\n"
|
||||
report+="[WARN] Forward chain not found in $NFT_TABLE\n"
|
||||
status=1
|
||||
fi
|
||||
|
||||
# 5. Check ipset
|
||||
for set_name in "$IPSET_V4" "$IPSET_V6"; do
|
||||
if ipset list "$set_name" &>/dev/null; then
|
||||
# 5. Check nftables sets
|
||||
for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
|
||||
if nft list set "$NFT_TABLE_FULL" "$set_name" &>/dev/null; then
|
||||
local entry_count
|
||||
entry_count=$(ipset list "$set_name" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0)
|
||||
report+="[OK] ipset $set_name active ($entry_count entries)\n"
|
||||
entry_count=$(nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null | grep -c '^\s*' || echo 0)
|
||||
report+="[OK] Set $set_name active ($entry_count entries)\n"
|
||||
else
|
||||
report+="[INFO] ipset $set_name not created (may not be needed)\n"
|
||||
report+="[INFO] Set $set_name not created (may not be needed)\n"
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
Reference in New Issue
Block a user