b5c9b180dc
- 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
1224 lines
34 KiB
Bash
1224 lines
34 KiB
Bash
#!/bin/bash
|
|
# WireGuard Policy Firewall Installer/Uninstaller
|
|
# This file is auto-generated. Do not edit directly. Run build.sh or build.bat instead.
|
|
|
|
set -euo pipefail
|
|
|
|
if [[ $EUID -ne 0 ]]; then
|
|
echo "This script must be run as root."
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v wg &>/dev/null; then
|
|
echo "[ERROR] WireGuard is not installed."
|
|
echo " Debian/Ubuntu: apt install wireguard"
|
|
echo " RHEL/CentOS: dnf install wireguard-tools"
|
|
echo " Arch: pacman -S wireguard-tools"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f /etc/wireguard/wg0.conf ]]; then
|
|
echo "[WARN] WireGuard config not found: /etc/wireguard/wg0.conf"
|
|
echo " PostUp/PostDown hooks will not be added automatically."
|
|
echo " Create your wg0.conf first, then re-install."
|
|
fi
|
|
|
|
WG_CONF="/etc/wireguard/wg0.conf"
|
|
|
|
install_hooks() {
|
|
if [[ ! -f "$WG_CONF" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if grep -q "wg-sync-policy.sh" "$WG_CONF" 2>/dev/null; then
|
|
echo "[OK] PostUp/PostDown hooks already present in $WG_CONF"
|
|
return 0
|
|
fi
|
|
|
|
echo "Adding PostUp/PostDown hooks to $WG_CONF..."
|
|
cp "$WG_CONF" "${WG_CONF}.bak.$(date +%Y%m%d%H%M%S)"
|
|
|
|
local peer_line
|
|
peer_line=$(grep -n '^\[Peer\]' "$WG_CONF" | head -1 | cut -d: -f1)
|
|
|
|
if [[ -n "$peer_line" ]]; then
|
|
sed -i "${peer_line}i\\
|
|
# WireGuard Policy Firewall hooks\\
|
|
PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh\\
|
|
PostDown = /usr/local/bin/wg-policy-cleanup.sh" "$WG_CONF"
|
|
else
|
|
{
|
|
echo ""
|
|
echo "# WireGuard Policy Firewall hooks"
|
|
echo "PostUp = /usr/local/bin/wg-sync-policy.sh; /usr/local/bin/wg-policy-engine.sh"
|
|
echo "PostDown = /usr/local/bin/wg-policy-cleanup.sh"
|
|
} >> "$WG_CONF"
|
|
fi
|
|
|
|
echo "[OK] Hooks added to $WG_CONF"
|
|
}
|
|
|
|
remove_hooks() {
|
|
if [[ ! -f "$WG_CONF" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
if ! grep -q "wg-sync-policy.sh" "$WG_CONF" 2>/dev/null; then
|
|
return 0
|
|
fi
|
|
|
|
echo "Removing PostUp/PostDown hooks from $WG_CONF..."
|
|
cp "$WG_CONF" "${WG_CONF}.bak.$(date +%Y%m%d%H%M%S)"
|
|
sed -i '/# WireGuard Policy Firewall hooks/d' "$WG_CONF"
|
|
sed -i '/wg-sync-policy\.sh/d' "$WG_CONF"
|
|
sed -i '/wg-policy-cleanup\.sh/d' "$WG_CONF"
|
|
echo "[OK] Hooks removed from $WG_CONF"
|
|
}
|
|
|
|
install_policy() {
|
|
echo "Installing WireGuard Policy Firewall..."
|
|
|
|
echo "Checking dependencies..."
|
|
apt-get update -y || true
|
|
apt-get install -y jq inotify-tools nftables || true
|
|
|
|
echo "Writing scripts to /usr/local/bin/..."
|
|
|
|
cat << 'EOF_WG_POLICY_LIB_SH' > /usr/local/bin/wg-policy-lib.sh
|
|
#!/bin/bash
|
|
# wg-policy-lib.sh — Shared functions for WireGuard Policy Firewall
|
|
# Source this file; do not execute directly.
|
|
|
|
set -euo pipefail
|
|
|
|
# ============================================================
|
|
# CONFIGURATION
|
|
# ============================================================
|
|
readonly WG_IF="${WG_IF:-wg0}"
|
|
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/minute"
|
|
readonly MAX_RETRY=3
|
|
readonly RETRY_DELAY=2
|
|
readonly DEBOUNCE_SEC=2
|
|
|
|
# ============================================================
|
|
# LOGGING
|
|
# ============================================================
|
|
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*"; }
|
|
log_warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $*" >&2; }
|
|
log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" >&2; }
|
|
|
|
# ============================================================
|
|
# VALIDATION
|
|
# ============================================================
|
|
|
|
validate_ipv4() {
|
|
local ip="$1"
|
|
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
|
|
if [[ "$octet" =~ ^0[0-9] ]]; then
|
|
return 1
|
|
fi
|
|
if (( octet < 0 || octet > 255 )); then
|
|
return 1
|
|
fi
|
|
done
|
|
return 0
|
|
}
|
|
|
|
validate_ipv4_cidr() {
|
|
local cidr="$1"
|
|
local ip prefix
|
|
|
|
if [[ "$cidr" == *"/"* ]]; then
|
|
ip="${cidr%%/*}"
|
|
prefix="${cidr##*/}"
|
|
else
|
|
ip="$cidr"
|
|
prefix="32"
|
|
fi
|
|
|
|
if ! validate_ipv4 "$ip"; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 0 || prefix > 32 )); then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
validate_ipv6() {
|
|
local ip="$1"
|
|
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}:$ ]] || \
|
|
[[ "$ip" == "::" ]] || \
|
|
[[ "$ip" == "::1" ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
validate_ipv6_cidr() {
|
|
local cidr="$1"
|
|
local ip prefix
|
|
|
|
if [[ "$cidr" == *"/"* ]]; then
|
|
ip="${cidr%%/*}"
|
|
prefix="${cidr##*/}"
|
|
else
|
|
ip="$cidr"
|
|
prefix="128"
|
|
fi
|
|
|
|
if ! validate_ipv6 "$ip"; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! "$prefix" =~ ^[0-9]+$ ]] || (( prefix < 0 || prefix > 128 )); then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
validate_cidr() {
|
|
local cidr="$1"
|
|
if [[ "$cidr" == *":"* ]]; then
|
|
validate_ipv6_cidr "$cidr"
|
|
else
|
|
validate_ipv4_cidr "$cidr"
|
|
fi
|
|
}
|
|
|
|
# ============================================================
|
|
# NFTABLES HELPERS
|
|
# ============================================================
|
|
|
|
has_nft() {
|
|
command -v nft &>/dev/null
|
|
}
|
|
|
|
# ============================================================
|
|
# RETRY MECHANISM
|
|
# ============================================================
|
|
|
|
retry() {
|
|
local max_attempts="${MAX_RETRY}"
|
|
local delay="${RETRY_DELAY}"
|
|
local attempt=1
|
|
local exit_code=0
|
|
|
|
while (( attempt <= max_attempts )); do
|
|
if "$@"; then
|
|
return 0
|
|
fi
|
|
exit_code=$?
|
|
log_warn "Attempt $attempt/$max_attempts failed (exit=$exit_code), retrying in ${delay}s..."
|
|
sleep "$delay"
|
|
(( attempt++ ))
|
|
(( delay *= 2 ))
|
|
done
|
|
|
|
log_error "All $max_attempts attempts failed for: $*"
|
|
return "$exit_code"
|
|
}
|
|
|
|
# ============================================================
|
|
# LOCK MANAGEMENT
|
|
# ============================================================
|
|
|
|
acquire_lock() {
|
|
local lock_fd=200
|
|
eval "exec ${lock_fd}>\"${LOCK_FILE}\""
|
|
if ! flock -x -w 10 "$lock_fd"; then
|
|
log_error "Failed to acquire lock: ${LOCK_FILE} (timeout 10s)"
|
|
return 1
|
|
fi
|
|
log_info "Lock acquired: ${LOCK_FILE}"
|
|
}
|
|
|
|
release_lock() {
|
|
rm -f "$LOCK_FILE" 2>/dev/null || true
|
|
}
|
|
|
|
# ============================================================
|
|
# BACKUP
|
|
# ============================================================
|
|
|
|
backup_policy() {
|
|
mkdir -p "$BACKUP_DIR"
|
|
local timestamp
|
|
timestamp="$(date '+%Y%m%d_%H%M%S')"
|
|
|
|
if [[ -f "$POLICY_FILE" ]]; then
|
|
cp "$POLICY_FILE" "${BACKUP_DIR}/policy_${timestamp}.json"
|
|
log_info "Backup created: ${BACKUP_DIR}/policy_${timestamp}.json"
|
|
fi
|
|
|
|
local count
|
|
count=$(find "$BACKUP_DIR" -name 'policy_*.json' -type f | wc -l)
|
|
if (( count > 50 )); then
|
|
find "$BACKUP_DIR" -name 'policy_*.json' -type f -printf '%T@ %p\n' \
|
|
| sort -n \
|
|
| head -n $(( count - 50 )) \
|
|
| awk '{print $2}' \
|
|
| xargs rm -f
|
|
log_info "Pruned old backups (kept 50)"
|
|
fi
|
|
}
|
|
|
|
backup_nftables() {
|
|
mkdir -p "$BACKUP_DIR"
|
|
local timestamp
|
|
timestamp="$(date '+%Y%m%d_%H%M%S')"
|
|
|
|
if nft list ruleset > "${BACKUP_DIR}/nftables_${timestamp}.rules" 2>/dev/null; then
|
|
log_info "nftables backup: ${BACKUP_DIR}/nftables_${timestamp}.rules"
|
|
fi
|
|
|
|
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
|
|
}
|
|
|
|
# ============================================================
|
|
# DETECTION HELPERS
|
|
# ============================================================
|
|
|
|
detect_lan_subnets() {
|
|
ip -o -4 route show scope link \
|
|
| awk '{print $1}' \
|
|
| grep -E '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)' \
|
|
| sort -u
|
|
}
|
|
|
|
detect_wg_subnet() {
|
|
local family="${1:-inet}"
|
|
if [[ "$family" == "inet6" ]]; then
|
|
ip -o -6 addr show "$WG_IF" 2>/dev/null \
|
|
| awk '{print $4; exit}'
|
|
else
|
|
ip -o -4 addr show "$WG_IF" 2>/dev/null \
|
|
| awk '{print $4; exit}'
|
|
fi
|
|
}
|
|
|
|
detect_default_if() {
|
|
local def_if
|
|
def_if=$(ip -4 route ls 2>/dev/null | grep default | grep -Po '(?<=dev )(\S+)' | head -1 || true)
|
|
if [[ -z "$def_if" ]]; then
|
|
echo "eth0"
|
|
else
|
|
echo "$def_if"
|
|
fi
|
|
}
|
|
|
|
# ============================================================
|
|
# HEALTH CHECK
|
|
# ============================================================
|
|
|
|
health_check() {
|
|
local status=0
|
|
local report=""
|
|
|
|
# 1. Check interface exists
|
|
if ip link show "$WG_IF" &>/dev/null; then
|
|
report+="[OK] Interface $WG_IF is UP\n"
|
|
else
|
|
report+="[FAIL] Interface $WG_IF not found\n"
|
|
status=1
|
|
fi
|
|
|
|
# 2. Check policy.json exists and is valid
|
|
if [[ -f "$POLICY_FILE" ]] && jq empty "$POLICY_FILE" 2>/dev/null; then
|
|
local client_count
|
|
client_count=$(jq '(.clients // {}) | length' "$POLICY_FILE")
|
|
report+="[OK] policy.json valid ($client_count clients)\n"
|
|
else
|
|
report+="[FAIL] policy.json missing or corrupt\n"
|
|
status=1
|
|
fi
|
|
|
|
# 3. Check nftables table exists
|
|
if nft list table "$NFT_TABLE_FULL" &>/dev/null; then
|
|
local rule_count
|
|
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] Table $NFT_TABLE not found\n"
|
|
status=1
|
|
fi
|
|
|
|
# 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 not found in $NFT_TABLE\n"
|
|
status=1
|
|
fi
|
|
|
|
# 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=$(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] Set $set_name not created (may not be needed)\n"
|
|
fi
|
|
done
|
|
|
|
# 6. Check watcher service
|
|
if systemctl is-active --quiet wg-policy.service 2>/dev/null; then
|
|
report+="[OK] wg-policy.service is running\n"
|
|
else
|
|
report+="[INFO] wg-policy.service not running\n"
|
|
fi
|
|
|
|
# 7. Check lock file not stale
|
|
if [[ -f "$LOCK_FILE" ]]; then
|
|
local lock_age
|
|
lock_age=$(( $(date +%s) - $(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0) ))
|
|
if (( lock_age > 300 )); then
|
|
report+="[WARN] Stale lock file (${lock_age}s old)\n"
|
|
else
|
|
report+="[OK] Lock file age: ${lock_age}s\n"
|
|
fi
|
|
else
|
|
report+="[OK] No stale lock file\n"
|
|
fi
|
|
|
|
echo -e "$report"
|
|
return $status
|
|
}
|
|
|
|
EOF_WG_POLICY_LIB_SH
|
|
|
|
cat << 'EOF_WG_POLICY_ENGINE_SH' > /usr/local/bin/wg-policy-engine.sh
|
|
#!/bin/bash
|
|
# wg-policy-engine.sh — Applies nftables rules from policy.json
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
|
|
|
NFT_FILE="/tmp/wg-policy.nft"
|
|
NFT_BACKUP="/tmp/wg-policy-backup.nft"
|
|
|
|
# ============================================================
|
|
# ROLLBACK
|
|
# ============================================================
|
|
|
|
rollback() {
|
|
log_error "ROLLBACK triggered! Restoring previous ruleset..."
|
|
trap - ERR
|
|
|
|
nft delete table "$NFT_TABLE_FULL" 2>/dev/null || true
|
|
|
|
if [[ -f "$NFT_BACKUP" ]]; then
|
|
if nft -f "$NFT_BACKUP" 2>/dev/null; then
|
|
log_info "Rollback: restored from backup"
|
|
else
|
|
log_error "Rollback: failed to restore from backup"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ============================================================
|
|
# RULESET GENERATION
|
|
# ============================================================
|
|
|
|
generate_ruleset() {
|
|
local WG_SUBNET="$1"
|
|
local WG_SUBNET_V6="$2"
|
|
local LAN_SUBNETS="$3"
|
|
local DEF_IF="$4"
|
|
|
|
cat > "$NFT_FILE" << 'HEADER'
|
|
#!/usr/sbin/nft -f
|
|
flush ruleset
|
|
|
|
table inet wg_policy {
|
|
|
|
set wg_allowed_v4 {
|
|
type ipv4_addr . ipv4_addr
|
|
flags interval
|
|
}
|
|
|
|
set wg_allowed_v6 {
|
|
type ipv6_addr . ipv6_addr
|
|
flags interval
|
|
}
|
|
|
|
chain forward {
|
|
type filter hook forward priority filter; policy accept;
|
|
|
|
ct state established,related accept
|
|
|
|
iifname "wg0" ip saddr . ip daddr @wg_allowed_v4 accept
|
|
iifname "wg0" ip6 saddr . ip6 daddr @wg_allowed_v6 accept
|
|
|
|
HEADER
|
|
|
|
# Client isolation (IPv4)
|
|
if [[ -n "$WG_SUBNET" ]]; then
|
|
cat >> "$NFT_FILE" << EOF
|
|
|
|
/* isolation: WG client to WG client */
|
|
iifname "wg0" ip saddr $WG_SUBNET ip daddr $WG_SUBNET ct state new drop
|
|
EOF
|
|
fi
|
|
|
|
# Client isolation (IPv6)
|
|
if [[ -n "$WG_SUBNET_V6" ]]; then
|
|
cat >> "$NFT_FILE" << EOF
|
|
|
|
/* IPv6 isolation */
|
|
iifname "wg0" ip6 saddr $WG_SUBNET_V6 ip6 daddr $WG_SUBNET_V6 ct state new drop
|
|
iifname "wg0" ip6 saddr $WG_SUBNET_V6 ip6 daddr fe80::/10 drop
|
|
iifname "wg0" ip6 saddr $WG_SUBNET_V6 ip6 daddr fc00::/7 drop
|
|
EOF
|
|
fi
|
|
|
|
# LAN block (IPv4)
|
|
if [[ -n "$WG_SUBNET" && -n "$LAN_SUBNETS" ]]; then
|
|
local lan_list=""
|
|
while IFS= read -r subnet; do
|
|
[[ -z "$subnet" ]] && continue
|
|
[[ "$subnet" == "$WG_SUBNET" ]] && continue
|
|
if [[ -n "$lan_list" ]]; then
|
|
lan_list+=", $subnet"
|
|
else
|
|
lan_list="$subnet"
|
|
fi
|
|
done <<< "$LAN_SUBNETS"
|
|
|
|
if [[ -n "$lan_list" ]]; then
|
|
cat >> "$NFT_FILE" << EOF
|
|
|
|
/* LAN block */
|
|
iifname "wg0" ip saddr $WG_SUBNET ip daddr { $lan_list } drop
|
|
EOF
|
|
fi
|
|
fi
|
|
|
|
# Internet access per client (inline in chain)
|
|
while IFS= read -r client_ip; do
|
|
[[ -z "$client_ip" ]] && continue
|
|
cat >> "$NFT_FILE" << EOF
|
|
|
|
/* internet access: $client_ip */
|
|
iifname "wg0" ip saddr $client_ip accept
|
|
EOF
|
|
done < <(jq -r '
|
|
.clients // {} | to_entries[] |
|
|
select(.value.internet == "true") |
|
|
.key
|
|
' "$POLICY_FILE" 2>/dev/null)
|
|
|
|
while IFS= read -r client_ip; do
|
|
[[ -z "$client_ip" ]] && continue
|
|
cat >> "$NFT_FILE" << EOF
|
|
|
|
/* internet access v6: $client_ip */
|
|
iifname "wg0" ip6 saddr $client_ip accept
|
|
EOF
|
|
done < <(jq -r '
|
|
.clients // {} | to_entries[] |
|
|
select(.value.internet == "true") |
|
|
select(.key | test(":")) |
|
|
.key
|
|
' "$POLICY_FILE" 2>/dev/null)
|
|
|
|
# Log + drop (final rule)
|
|
cat >> "$NFT_FILE" << EOF
|
|
|
|
/* log + drop */
|
|
iifname "wg0" limit rate ${LOG_RATE} log prefix "${LOG_PREFIX}: " drop
|
|
}
|
|
|
|
chain postrouting {
|
|
type nat hook postrouting priority srcnat; policy accept;
|
|
oifname "${DEF_IF}" masquerade
|
|
}
|
|
}
|
|
EOF
|
|
}
|
|
|
|
# ============================================================
|
|
# MAIN
|
|
# ============================================================
|
|
|
|
main() {
|
|
log_info "Starting policy engine..."
|
|
|
|
# === VALIDATE ===
|
|
if ! ip link show "$WG_IF" &>/dev/null; then
|
|
log_error "Interface $WG_IF is not running. Aborting policy engine."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$POLICY_FILE" ]]; then
|
|
log_error "Policy file not found: $POLICY_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
if ! jq empty "$POLICY_FILE" 2>/dev/null; then
|
|
log_error "policy.json is corrupt"
|
|
exit 1
|
|
fi
|
|
|
|
# Backup current nftables state
|
|
backup_nftables
|
|
|
|
# Set trap for rollback on failure
|
|
trap 'rollback' ERR
|
|
|
|
# === DETECT SUBNETS ===
|
|
local WG_SUBNET WG_SUBNET_V6 LAN_SUBNETS DEF_IF
|
|
|
|
WG_SUBNET="$(detect_wg_subnet inet)"
|
|
WG_SUBNET_V6="$(detect_wg_subnet inet6)"
|
|
LAN_SUBNETS="$(detect_lan_subnets)"
|
|
DEF_IF="$(detect_default_if)"
|
|
|
|
if [[ -z "$WG_SUBNET" ]]; then
|
|
log_warn "Interface $WG_IF has no IPv4, skipping client isolation"
|
|
else
|
|
log_info "WG IPv4 subnet: $WG_SUBNET"
|
|
fi
|
|
|
|
if [[ -n "$WG_SUBNET_V6" ]]; then
|
|
log_info "WG IPv6 subnet: $WG_SUBNET_V6"
|
|
fi
|
|
|
|
if [[ -n "$LAN_SUBNETS" ]]; then
|
|
log_info "Detected LAN subnets:"
|
|
echo "$LAN_SUBNETS" | while read -r s; do log_info " $s"; done
|
|
fi
|
|
|
|
# === BASE ROUTING & NAT ===
|
|
sysctl -w net.ipv4.ip_forward=1 >/dev/null 2>&1 || true
|
|
sysctl -w net.ipv6.conf.all.forwarding=1 >/dev/null 2>&1 || true
|
|
|
|
# === BACKUP EXISTING TABLE ===
|
|
if nft list table "$NFT_TABLE_FULL" &>/dev/null; then
|
|
nft list table "$NFT_TABLE_FULL" > "$NFT_BACKUP" 2>/dev/null || true
|
|
log_info "Backed up existing table to $NFT_BACKUP"
|
|
fi
|
|
|
|
# === DELETE OLD TABLE ===
|
|
nft delete table "$NFT_TABLE_FULL" 2>/dev/null || true
|
|
|
|
# === GENERATE AND LOAD NEW RULESET ===
|
|
generate_ruleset "$WG_SUBNET" "$WG_SUBNET_V6" "$LAN_SUBNETS" "$DEF_IF"
|
|
log_info "Generated ruleset: $NFT_FILE"
|
|
|
|
nft -f "$NFT_FILE"
|
|
log_info "Loaded nftables ruleset from $NFT_FILE"
|
|
|
|
# === POPULATE SETS ===
|
|
log_info "Populating whitelist sets..."
|
|
|
|
local v4_count=0
|
|
local v6_count=0
|
|
|
|
while IFS= read -r line; do
|
|
[[ -z "$line" ]] && continue
|
|
local client_ip target
|
|
client_ip=$(echo "$line" | awk '{print $1}')
|
|
target=$(echo "$line" | awk '{print $2}')
|
|
[[ -z "$client_ip" || -z "$target" ]] && continue
|
|
|
|
if [[ "$target" == *":"* ]]; then
|
|
nft add element "$NFT_TABLE_FULL" "$NFT_SET_V6" { "$client_ip" . "$target" } 2>/dev/null || \
|
|
log_warn "Failed to add ${client_ip} . ${target} to set $NFT_SET_V6"
|
|
(( v6_count++ )) || true
|
|
else
|
|
nft add element "$NFT_TABLE_FULL" "$NFT_SET_V4" { "$client_ip" . "$target" } 2>/dev/null || \
|
|
log_warn "Failed to add ${client_ip} . ${target} to set $NFT_SET_V4"
|
|
(( v4_count++ )) || true
|
|
fi
|
|
done < <(jq -r '
|
|
.clients // {} | to_entries[] |
|
|
select(.value.access != null and (.value.access | length > 0)) |
|
|
.key as $ip |
|
|
.value.access[] |
|
|
"\($ip) \(.)"
|
|
' "$POLICY_FILE" 2>/dev/null)
|
|
|
|
log_info "Set $NFT_SET_V4: $v4_count entries, $NFT_SET_V6: $v6_count entries"
|
|
|
|
# === CLEANUP BACKUP (success path) ===
|
|
rm -f "$NFT_BACKUP" 2>/dev/null || true
|
|
|
|
# Disable ERR trap (success path)
|
|
trap - ERR
|
|
|
|
# === VERIFY ===
|
|
local rule_count
|
|
rule_count=$(nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '^\s*' || echo 0)
|
|
log_info "Policy applied. Table: $NFT_TABLE, Rules: $rule_count"
|
|
|
|
echo "[OK] nftables policy applied. Table: $NFT_TABLE"
|
|
}
|
|
|
|
main "$@"
|
|
|
|
EOF_WG_POLICY_ENGINE_SH
|
|
|
|
cat << 'EOF_WG_POLICY_CLEANUP_SH' > /usr/local/bin/wg-policy-cleanup.sh
|
|
#!/bin/bash
|
|
# wg-policy-cleanup.sh — Clean removal of all policy artifacts
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
|
|
|
main() {
|
|
log_info "Starting cleanup..."
|
|
|
|
nft delete table inet wg_policy 2>/dev/null || true
|
|
log_info "Removed nftables table inet wg_policy"
|
|
|
|
rm -f "$LOCK_FILE" 2>/dev/null || true
|
|
|
|
log_info "Cleanup complete"
|
|
}
|
|
|
|
main "$@"
|
|
|
|
EOF_WG_POLICY_CLEANUP_SH
|
|
|
|
cat << 'EOF_WG_SYNC_POLICY_SH' > /usr/local/bin/wg-sync-policy.sh
|
|
#!/bin/bash
|
|
# wg-sync-policy.sh — Reads wg0.conf, validates, generates policy.json atomically
|
|
# Fixed: IP validation, atomic write, proper locking, error handling
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
|
|
|
# ============================================================
|
|
# MAIN
|
|
# ============================================================
|
|
|
|
main() {
|
|
log_info "Starting policy sync..."
|
|
|
|
# Validate prerequisites
|
|
if [[ ! -f "$WG_CONF" ]]; then
|
|
log_error "WireGuard config not found: $WG_CONF"
|
|
exit 1
|
|
fi
|
|
|
|
if ! command -v jq &>/dev/null; then
|
|
log_error "jq is required but not installed"
|
|
exit 1
|
|
fi
|
|
|
|
# Acquire lock
|
|
acquire_lock
|
|
trap 'release_lock' EXIT
|
|
|
|
# Backup current policy
|
|
backup_policy
|
|
|
|
# Temporary file for atomic write
|
|
local tmp_policy
|
|
tmp_policy="$(mktemp /tmp/wg-policy.XXXXXX)"
|
|
trap "rm -f \"$tmp_policy\" 2>/dev/null; release_lock" EXIT
|
|
|
|
echo '{"clients":{}}' > "$tmp_policy"
|
|
|
|
# Parse peers from wg0.conf
|
|
# AWK extracts IP and #Access comment per [Peer] block
|
|
local parse_errors=0
|
|
|
|
awk '
|
|
BEGIN { RS="\n\\[Peer\\]\n"; FS="\n" }
|
|
NR>1 {
|
|
ip=""; access=""; internet="false"
|
|
for(i=1;i<=NF;i++){
|
|
if($i ~ /^AllowedIPs/) {
|
|
split($i,a," = ")
|
|
gsub(/ /,"",a[2])
|
|
split(a[2],b,",")
|
|
split(b[1],c,"/")
|
|
ip=c[1]
|
|
}
|
|
if($i ~ /^#Access/) {
|
|
sub(/^#Access[ \t]*=?[ \t]*/, "", $i)
|
|
access=$i
|
|
}
|
|
if($i ~ /^#Internet/) {
|
|
if(tolower($i) ~ /true|yes|1|allow/) {
|
|
internet="true"
|
|
}
|
|
}
|
|
}
|
|
if(ip!="" && ip!="0.0.0.0" && ip!="::") {
|
|
printf "%s|%s|%s\n", ip, access, internet
|
|
}
|
|
}
|
|
' "$WG_CONF" | while IFS="|" read -r ip access_string internet_flag; do
|
|
|
|
# === VALIDATE CLIENT IP ===
|
|
if ! validate_cidr "$ip"; then
|
|
log_warn "Invalid client IP skipped: '$ip'"
|
|
(( parse_errors++ )) || true
|
|
continue
|
|
fi
|
|
|
|
# === PARSE AND VALIDATE ACCESS TARGETS ===
|
|
local ACCESS_JSON="[]"
|
|
|
|
if [[ -n "$access_string" ]]; then
|
|
# Split by ; and , then validate each entry
|
|
local valid_targets=()
|
|
local IFS_OLD="$IFS"
|
|
IFS=';,'
|
|
read -ra targets <<< "$access_string"
|
|
IFS="$IFS_OLD"
|
|
|
|
for target in "${targets[@]}"; do
|
|
# Trim whitespace
|
|
target="$(echo "$target" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
|
|
[[ -z "$target" ]] && continue
|
|
|
|
if validate_cidr "$target"; then
|
|
valid_targets+=("$target")
|
|
else
|
|
log_warn "Invalid access target skipped for $ip: '$target'"
|
|
(( parse_errors++ )) || true
|
|
fi
|
|
done
|
|
|
|
if (( ${#valid_targets[@]} > 0 )); then
|
|
ACCESS_JSON=$(printf '%s\n' "${valid_targets[@]}" | jq -R . | jq -s .)
|
|
fi
|
|
fi
|
|
|
|
# Write to temp policy
|
|
jq --arg ip "$ip" --argjson access "$ACCESS_JSON" --argjson internet "$internet_flag" \
|
|
'.clients[$ip] = {"name": $ip, "access": $access, "internet": $internet}' \
|
|
"$tmp_policy" > "${tmp_policy}.tmp" && mv "${tmp_policy}.tmp" "$tmp_policy"
|
|
|
|
done
|
|
|
|
# Validate JSON before atomic move
|
|
if ! jq empty "$tmp_policy" 2>/dev/null; then
|
|
log_error "Generated JSON is invalid, aborting. Check $tmp_policy"
|
|
exit 1
|
|
fi
|
|
|
|
# Atomic move (same filesystem = atomic rename)
|
|
mv -f "$tmp_policy" "$POLICY_FILE"
|
|
log_info "policy.json updated successfully"
|
|
|
|
if (( parse_errors > 0 )); then
|
|
log_warn "$parse_errors validation errors encountered (see warnings above)"
|
|
fi
|
|
|
|
local client_count
|
|
client_count=$(jq '(.clients // {}) | length' "$POLICY_FILE")
|
|
log_info "Total clients in policy: $client_count"
|
|
}
|
|
|
|
main "$@"
|
|
|
|
EOF_WG_SYNC_POLICY_SH
|
|
|
|
cat << 'EOF_WG_SYNC_WATCH_SH' > /usr/local/bin/wg-sync-watch.sh
|
|
#!/bin/bash
|
|
# wg-sync-watch.sh — Watches wg0.conf for changes with debounce
|
|
# Fixed: proper debounce, error isolation, health reporting
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
|
|
|
SYNC_SCRIPT="/usr/local/bin/wg-sync-policy.sh"
|
|
ENGINE_SCRIPT="/usr/local/bin/wg-policy-engine.sh"
|
|
HEALTH_INTERVAL=300 # Health check every 5 minutes
|
|
LAST_HEALTH=0
|
|
|
|
# === Validate prerequisites ===
|
|
if ! command -v inotifywait &>/dev/null; then
|
|
log_error "inotifywait not found. Install: apt install inotify-tools"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$WG_CONF" ]]; then
|
|
log_error "WireGuard config not found: $WG_CONF"
|
|
exit 1
|
|
fi
|
|
|
|
# === Main watcher loop ===
|
|
log_info "Monitoring $WG_CONF for changes (debounce: ${DEBOUNCE_SEC}s)..."
|
|
log_info "Health check interval: ${HEALTH_INTERVAL}s"
|
|
|
|
inotifywait -m -e close_write,move,create \
|
|
--format '%e %f' \
|
|
"$(dirname "$WG_CONF")" 2>/dev/null | \
|
|
while read -r events filename; do
|
|
|
|
# Only react to wg0.conf changes
|
|
[[ "$filename" != "$(basename "$WG_CONF")" ]] && continue
|
|
|
|
log_info "Detected change: $events $filename"
|
|
|
|
# Debounce: wait until no more events for DEBOUNCE_SEC
|
|
while IFS= read -r -t "$DEBOUNCE_SEC" _dummy; do
|
|
: # Drain events within debounce window
|
|
done
|
|
|
|
log_info "Debounce complete, applying changes..."
|
|
|
|
# Run sync
|
|
if retry "$SYNC_SCRIPT"; then
|
|
log_info "Sync successful, running engine..."
|
|
|
|
# Run engine with retry
|
|
if retry "$ENGINE_SCRIPT"; then
|
|
log_info "Policy engine applied successfully"
|
|
else
|
|
log_error "Policy engine FAILED after retries"
|
|
fi
|
|
else
|
|
log_error "Policy sync FAILED after retries"
|
|
fi
|
|
|
|
# Periodic health check
|
|
local now
|
|
now=$(date +%s)
|
|
if (( now - LAST_HEALTH >= HEALTH_INTERVAL )); then
|
|
LAST_HEALTH=$now
|
|
log_info "=== Periodic Health Check ==="
|
|
health_check || log_warn "Health check reported issues"
|
|
fi
|
|
|
|
done
|
|
|
|
EOF_WG_SYNC_WATCH_SH
|
|
|
|
cat << 'EOF_WG_POLICY_CTL' > /usr/local/bin/wg-policy-ctl
|
|
#!/bin/bash
|
|
# wg-policy-ctl — CLI management tool for WireGuard Policy Firewall
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "${SCRIPT_DIR}/wg-policy-lib.sh"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") <command>
|
|
|
|
Commands:
|
|
status Show full health check report
|
|
policy Display current policy.json formatted
|
|
rules Show current nftables rules
|
|
ipset Show nftables set contents
|
|
log Tail WG_DROP logs (last 50 lines)
|
|
reload Force re-sync and re-apply policy
|
|
backup Manual backup of policy + nftables
|
|
stats Show connection and rule statistics
|
|
validate Validate policy.json without applying
|
|
help Show this help
|
|
EOF
|
|
}
|
|
|
|
cmd_status() {
|
|
echo "========================================="
|
|
echo " WireGuard Policy Firewall Status"
|
|
echo " $(date '+%Y-%m-%d %H:%M:%S')"
|
|
echo "========================================="
|
|
echo ""
|
|
health_check
|
|
}
|
|
|
|
cmd_policy() {
|
|
if [[ -f "$POLICY_FILE" ]]; then
|
|
jq '.' "$POLICY_FILE"
|
|
else
|
|
log_error "policy.json not found"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_rules() {
|
|
echo "=== Table: $NFT_TABLE_FULL ==="
|
|
if nft list table "$NFT_TABLE_FULL" 2>/dev/null; then
|
|
echo ""
|
|
else
|
|
echo "(table not found)"
|
|
fi
|
|
}
|
|
|
|
cmd_ipset() {
|
|
for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
|
|
echo "=== nft set: $set_name ==="
|
|
if nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null; then
|
|
echo ""
|
|
else
|
|
echo "(not found)"
|
|
echo ""
|
|
fi
|
|
done
|
|
}
|
|
|
|
cmd_log() {
|
|
echo "=== Recent WG_DROP log entries ==="
|
|
(journalctl -k --no-pager -n 50 2>/dev/null || dmesg | tail -50) | grep "$LOG_PREFIX" || echo "(no entries)"
|
|
}
|
|
|
|
cmd_reload() {
|
|
log_info "Force reloading policy..."
|
|
if retry /usr/local/bin/wg-sync-policy.sh; then
|
|
if retry /usr/local/bin/wg-policy-engine.sh; then
|
|
log_info "Reload complete"
|
|
else
|
|
log_error "Engine failed"
|
|
exit 1
|
|
fi
|
|
else
|
|
log_error "Sync failed"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_backup() {
|
|
backup_policy
|
|
backup_nftables
|
|
log_info "Manual backup complete. Files in: $BACKUP_DIR"
|
|
}
|
|
|
|
cmd_stats() {
|
|
echo "=== Client Count ==="
|
|
jq '(.clients // {}) | length' "$POLICY_FILE" 2>/dev/null || echo "N/A"
|
|
|
|
echo ""
|
|
echo "=== Clients with Access ==="
|
|
jq -r '.clients // {} | to_entries[] | select(.value.access | length > 0) | "\(.key): \(.value.access | join(", "))"' "$POLICY_FILE" 2>/dev/null || echo "N/A"
|
|
|
|
echo ""
|
|
echo "=== Clients without Access (Internet Only) ==="
|
|
jq -r '.clients // {} | to_entries[] | select(.value.access | length == 0) | .key' "$POLICY_FILE" 2>/dev/null || echo "N/A"
|
|
|
|
echo ""
|
|
echo "=== Active nftables rules ==="
|
|
nft list chain "$NFT_TABLE_FULL" forward 2>/dev/null | grep -c '#' || echo "N/A"
|
|
|
|
echo ""
|
|
echo "=== nft set entries ==="
|
|
for set_name in "$NFT_SET_V4" "$NFT_SET_V6"; do
|
|
local count
|
|
count=$(nft list set "$NFT_TABLE_FULL" "$set_name" 2>/dev/null | grep -c -E '^\s+[0-9a-f]' || echo 0)
|
|
echo " $set_name: $count entries"
|
|
done
|
|
}
|
|
|
|
cmd_validate() {
|
|
log_info "Validating policy.json..."
|
|
|
|
if [[ ! -f "$POLICY_FILE" ]]; then
|
|
log_error "File not found: $POLICY_FILE"
|
|
exit 1
|
|
fi
|
|
|
|
if ! jq empty "$POLICY_FILE" 2>/dev/null; then
|
|
log_error "Invalid JSON"
|
|
exit 1
|
|
fi
|
|
|
|
local errors=0
|
|
local total=0
|
|
|
|
jq -r '.clients // {} | to_entries[] | "\(.key)|\(.value.access // [] | join(","))"' "$POLICY_FILE" | \
|
|
while IFS="|" read -r ip access_str; do
|
|
(( total++ ))
|
|
|
|
if ! validate_cidr "$ip"; then
|
|
log_error "Invalid client IP: $ip"
|
|
(( errors++ )) || true
|
|
fi
|
|
|
|
if [[ -n "$access_str" ]]; then
|
|
IFS=',' read -ra targets <<< "$access_str"
|
|
for target in "${targets[@]}"; do
|
|
if ! validate_cidr "$target"; then
|
|
log_error "Invalid access target for $ip: $target"
|
|
(( errors++ )) || true
|
|
fi
|
|
done
|
|
fi
|
|
done
|
|
|
|
if (( errors > 0 )); then
|
|
log_error "Validation failed: $errors errors"
|
|
exit 1
|
|
else
|
|
log_info "Validation passed: $total clients, 0 errors"
|
|
fi
|
|
}
|
|
|
|
# === DISPATCH ===
|
|
case "${1:-help}" in
|
|
status) cmd_status ;;
|
|
policy) cmd_policy ;;
|
|
rules) cmd_rules ;;
|
|
ipset) cmd_ipset ;;
|
|
log) cmd_log ;;
|
|
reload) cmd_reload ;;
|
|
backup) cmd_backup ;;
|
|
stats) cmd_stats ;;
|
|
validate) cmd_validate ;;
|
|
help|*) usage ;;
|
|
esac
|
|
|
|
EOF_WG_POLICY_CTL
|
|
|
|
echo "Writing systemd units to /etc/systemd/system/..."
|
|
|
|
cat << 'EOF_WG_POLICY_SERVICE' > /etc/systemd/system/wg-policy.service
|
|
[Unit]
|
|
Description=WireGuard Dynamic Policy Firewall Watcher
|
|
After=network-online.target wg-quick@wg0.service
|
|
Wants=wg-quick@wg0.service network-online.target
|
|
StartLimitIntervalSec=60
|
|
StartLimitBurst=5
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStartPre=/usr/local/bin/wg-sync-policy.sh
|
|
ExecStart=/usr/local/bin/wg-sync-watch.sh
|
|
ExecStopPost=/usr/local/bin/wg-policy-cleanup.sh
|
|
Restart=always
|
|
RestartSec=10
|
|
User=root
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=wg-policy
|
|
|
|
# Hardening
|
|
ProtectSystem=strict
|
|
ReadWritePaths=/etc/wireguard /var/lock /tmp
|
|
ProtectHome=yes
|
|
NoNewPrivileges=no
|
|
PrivateTmp=yes
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
|
|
EOF_WG_POLICY_SERVICE
|
|
|
|
cat << 'EOF_WG_POLICY_HEALTH_SERVICE' > /etc/systemd/system/wg-policy-health.service
|
|
[Unit]
|
|
Description=WireGuard Policy Health Check
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/bin/bash -c 'source /usr/local/bin/wg-policy-lib.sh && health_check'
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=wg-policy-health
|
|
|
|
EOF_WG_POLICY_HEALTH_SERVICE
|
|
|
|
cat << 'EOF_WG_POLICY_HEALTH_TIMER' > /etc/systemd/system/wg-policy-health.timer
|
|
[Unit]
|
|
Description=WireGuard Policy Health Check Timer
|
|
|
|
[Timer]
|
|
OnBootSec=60
|
|
OnUnitActiveSec=300
|
|
AccuracySec=30
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
|
|
EOF_WG_POLICY_HEALTH_TIMER
|
|
|
|
chmod +x /usr/local/bin/wg-*.sh /usr/local/bin/wg-policy-ctl
|
|
|
|
echo "Reloading systemd daemon..."
|
|
systemctl daemon-reload
|
|
|
|
echo "Enabling and starting services..."
|
|
systemctl enable --now wg-policy.service
|
|
systemctl enable --now wg-policy-health.timer
|
|
|
|
install_hooks
|
|
|
|
echo "Installation complete!"
|
|
echo "You can check status with: wg-policy-ctl status"
|
|
}
|
|
|
|
uninstall_policy() {
|
|
echo "Uninstalling WireGuard Policy Firewall..."
|
|
|
|
echo "Stopping and disabling services..."
|
|
systemctl disable --now wg-policy.service wg-policy-health.timer wg-policy-health.service 2>/dev/null || true
|
|
|
|
echo "Running cleanup script..."
|
|
if [ -x /usr/local/bin/wg-policy-cleanup.sh ]; then
|
|
/usr/local/bin/wg-policy-cleanup.sh || true
|
|
fi
|
|
|
|
remove_hooks
|
|
|
|
echo "Removing systemd units..."
|
|
rm -f /etc/systemd/system/wg-policy.service
|
|
rm -f /etc/systemd/system/wg-policy-health.service
|
|
rm -f /etc/systemd/system/wg-policy-health.timer
|
|
systemctl daemon-reload
|
|
|
|
echo "Removing scripts from /usr/local/bin/..."
|
|
rm -f /usr/local/bin/wg-policy-lib.sh
|
|
rm -f /usr/local/bin/wg-sync-policy.sh
|
|
rm -f /usr/local/bin/wg-policy-engine.sh
|
|
rm -f /usr/local/bin/wg-policy-cleanup.sh
|
|
rm -f /usr/local/bin/wg-sync-watch.sh
|
|
rm -f /usr/local/bin/wg-policy-ctl
|
|
|
|
echo "Uninstallation complete!"
|
|
}
|
|
|
|
case "${1:-}" in
|
|
install)
|
|
install_policy
|
|
;;
|
|
uninstall)
|
|
uninstall_policy
|
|
;;
|
|
*)
|
|
echo "Usage: $0 {install|uninstall}"
|
|
exit 1
|
|
;;
|
|
esac
|