From 93b781b17c0e71747ccd8187f93792598f4c4293 Mon Sep 17 00:00:00 2001 From: datadunia Date: Thu, 30 Apr 2026 20:57:42 +0700 Subject: [PATCH] fix: make ipset optional, fix ExecStartPre, add prerequisites to README --- README.md | 36 +++++++++++- wg-policy-engine.sh | 130 ++++++++++++++++++++++++++------------------ wg-policy-lib.sh | 7 +++ wg-policy.service | 2 +- 4 files changed, 121 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 2386803..fb878f0 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,18 @@ chmod +x /usr/local/bin/wg-*.sh /usr/local/bin/wg-policy-ctl --- +## 📦 Prerequisites + +| Package | Required | Install | +|---------|----------|---------| +| `jq` | **Yes** | `apt install jq` | +| `inotify-tools` | **Yes** (for watcher daemon) | `apt install inotify-tools` | +| `ipset` | Optional | `apt install ipset` | + +If `ipset` is not installed, the engine will automatically fall back to per-rule `iptables` whitelist entries. This works fine for small deployments. For large numbers of clients/targets, `ipset` is recommended for O(1) lookup performance. + +--- + ## ⚙️ Integrasi ke `wg0.conf` To integrate the engine, you need to append hooks into your `wg0.conf` interface block, and declare the `#Access` tags under each peer. @@ -49,9 +61,15 @@ ListenPort = 51820 PrivateKey = # PostUp: sync policy + apply engine +# default PostUp +iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; +# modification PostUp = /usr/local/bin/wg-sync-policy.sh && /usr/local/bin/wg-policy-engine.sh # PostDown: safe cleanup +# default PostUp +iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; +# modification PostDown = /usr/local/bin/wg-policy-cleanup.sh ``` @@ -117,7 +135,23 @@ wg-policy-ctl validate If you are using the daemon mode to auto-sync changes instantly upon editing `wg0.conf` (without needing to run `wg-policy-ctl reload` or restarting the interface). -Enable the systemd services: +### 1. File Installation +Place the three provided systemd unit files into `/etc/systemd/system/`. + +| Systemd File | Location | Description | +|--------------|----------|-------------| +| `wg-policy.service` | `/etc/systemd/system/wg-policy.service` | The main daemon that runs `wg-sync-watch.sh` | +| `wg-policy-health.timer` | `/etc/systemd/system/wg-policy-health.timer` | Triggers the health check every 5 minutes | +| `wg-policy-health.service`| `/etc/systemd/system/wg-policy-health.service`| Executes the actual health check logic | + +```bash +# Example copy command +cp wg-policy.service wg-policy-health.timer wg-policy-health.service /etc/systemd/system/ +``` + +### 2. Enable & Start Services +After copying the files, reload systemd to recognize them, then enable and start the services. + ```bash systemctl daemon-reload systemctl enable wg-policy.service diff --git a/wg-policy-engine.sh b/wg-policy-engine.sh index 12810e7..476d40f 100644 --- a/wg-policy-engine.sh +++ b/wg-policy-engine.sh @@ -115,68 +115,94 @@ main() { log_info "Chain $CHAIN created and linked to FORWARD" # === POPULATE IPSET (for large-scale whitelist) === - log_info "Populating ipsets..." + local use_ipset=false + if has_ipset; then + use_ipset=true + log_info "Populating ipsets..." - ensure_ipset "$IPSET_V4" "inet" - flush_ipset "$IPSET_V4" + ensure_ipset "$IPSET_V4" "inet" + flush_ipset "$IPSET_V4" - # Check if we need IPv6 ipset - local use_ipv6=false - if [[ -n "$WG_SUBNET_V6" ]] && command -v ip6tables &>/dev/null; then - use_ipv6=true - ensure_ipset "$IPSET_V6" "inet6" - flush_ipset "$IPSET_V6" - fi - - # Read all access entries and populate ipset - 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 | while read -r client_ip target; do - [[ -z "$client_ip" || -z "$target" ]] && continue - - # Determine if v4 or v6 - if [[ "$target" == *":"* ]]; then - if [[ "$use_ipv6" == true ]]; then - ipset add "$IPSET_V6" "$target" 2>/dev/null || \ - log_warn "Failed to add $target to ipset $IPSET_V6" - fi - else - ipset add "$IPSET_V4" "$target" 2>/dev/null || \ - log_warn "Failed to add $target to ipset $IPSET_V4" + # Check if we need IPv6 ipset + local use_ipv6=false + if [[ -n "$WG_SUBNET_V6" ]] && command -v ip6tables &>/dev/null; then + use_ipv6=true + ensure_ipset "$IPSET_V6" "inet6" + flush_ipset "$IPSET_V6" fi - done - local v4_count v6_count - v4_count=$(ipset list "$IPSET_V4" 2>/dev/null | grep -c '^[0-9]' || echo 0) - v6_count=$(ipset list "$IPSET_V6" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0) - log_info "ipset $IPSET_V4: $v4_count entries, $IPSET_V6: $v6_count entries" + # Read all access entries and populate ipset + 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 | while read -r client_ip target; do + [[ -z "$client_ip" || -z "$target" ]] && continue + + if [[ "$target" == *":"* ]]; then + if [[ "$use_ipv6" == true ]]; then + ipset add "$IPSET_V6" "$target" 2>/dev/null || \ + log_warn "Failed to add $target to ipset $IPSET_V6" + fi + else + ipset add "$IPSET_V4" "$target" 2>/dev/null || \ + log_warn "Failed to add $target to ipset $IPSET_V4" + fi + done + + local v4_count v6_count + v4_count=$(ipset list "$IPSET_V4" 2>/dev/null | grep -c '^[0-9]' || echo 0) + v6_count=$(ipset list "$IPSET_V6" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0) + log_info "ipset $IPSET_V4: $v4_count entries, $IPSET_V6: $v6_count entries" + else + log_warn "ipset not installed, falling back to per-rule iptables whitelist" + local use_ipv6=false + if [[ -n "$WG_SUBNET_V6" ]] && command -v ip6tables &>/dev/null; then + use_ipv6=true + fi + fi # === RULE 1: ESTABLISHED,RELATED — allow return traffic === iptables -A "$CHAIN" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT - # === RULE 2: WHITELIST via ipset (per-client source) === - # For each client with access rules, allow only from that client's IP to ipset targets - jq -r ' - .clients // {} | to_entries[] | - select(.value.access != null and (.value.access | length > 0)) | - "\(.key)" - ' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do - [[ -z "$client_ip" ]] && continue + # === RULE 2: WHITELIST (per-client source) === + if [[ "$use_ipset" == true ]]; then + jq -r ' + .clients // {} | to_entries[] | + select(.value.access != null and (.value.access | length > 0)) | + "\(.key)" + ' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do + [[ -z "$client_ip" ]] && continue - if [[ "$client_ip" == *":"* ]]; then - # IPv6 client - if [[ "$use_ipv6" == true ]]; then - ip6tables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V6" dst -j ACCEPT 2>/dev/null || true + if [[ "$client_ip" == *":"* ]]; then + if [[ "$use_ipv6" == true ]]; then + ip6tables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V6" dst -j ACCEPT 2>/dev/null || true + fi + else + iptables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V4" dst -j ACCEPT fi - else - # IPv4 client - iptables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V4" dst -j ACCEPT - fi - done + done + else + 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 | while read -r client_ip target; do + [[ -z "$client_ip" || -z "$target" ]] && continue + + if [[ "$client_ip" == *":"* ]]; then + if [[ "$use_ipv6" == true ]]; then + ip6tables -A "$CHAIN" -s "$client_ip" -d "$target" -j ACCEPT 2>/dev/null || true + fi + else + iptables -A "$CHAIN" -s "$client_ip" -d "$target" -j ACCEPT + fi + done + fi # === RULE 3: ISOLATION — drop NEW connections between WG clients === if [[ -n "$WG_SUBNET" ]]; then diff --git a/wg-policy-lib.sh b/wg-policy-lib.sh index 7d0268f..66a9b39 100644 --- a/wg-policy-lib.sh +++ b/wg-policy-lib.sh @@ -129,8 +129,13 @@ validate_cidr() { # IPSET MANAGEMENT # ============================================================ +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 family "$family" hashsize 1024 maxelem 65536 timeout 0 log_info "Created ipset: $name (family=$family)" @@ -139,6 +144,7 @@ ensure_ipset() { flush_ipset() { local name="$1" + has_ipset || return 0 if ipset list "$name" &>/dev/null; then ipset flush "$name" fi @@ -146,6 +152,7 @@ flush_ipset() { destroy_ipset() { local name="$1" + has_ipset || return 0 if ipset list "$name" &>/dev/null; then ipset destroy "$name" fi diff --git a/wg-policy.service b/wg-policy.service index eb360d7..ded64e0 100644 --- a/wg-policy.service +++ b/wg-policy.service @@ -7,7 +7,7 @@ StartLimitBurst=5 [Service] Type=simple -ExecStartPre=/usr/local/bin/wg-policy-engine.sh --health-check +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