fix: make ipset optional, fix ExecStartPre, add prerequisites to README
This commit is contained in:
@@ -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`
|
## ⚙️ 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.
|
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 = <SERVER_PRIVATE_KEY>
|
PrivateKey = <SERVER_PRIVATE_KEY>
|
||||||
|
|
||||||
# PostUp: sync policy + apply engine
|
# 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
|
PostUp = /usr/local/bin/wg-sync-policy.sh && /usr/local/bin/wg-policy-engine.sh
|
||||||
|
|
||||||
# PostDown: safe cleanup
|
# 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
|
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).
|
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
|
```bash
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
systemctl enable wg-policy.service
|
systemctl enable wg-policy.service
|
||||||
|
|||||||
+78
-52
@@ -115,68 +115,94 @@ main() {
|
|||||||
log_info "Chain $CHAIN created and linked to FORWARD"
|
log_info "Chain $CHAIN created and linked to FORWARD"
|
||||||
|
|
||||||
# === POPULATE IPSET (for large-scale whitelist) ===
|
# === 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"
|
ensure_ipset "$IPSET_V4" "inet"
|
||||||
flush_ipset "$IPSET_V4"
|
flush_ipset "$IPSET_V4"
|
||||||
|
|
||||||
# Check if we need IPv6 ipset
|
# Check if we need IPv6 ipset
|
||||||
local use_ipv6=false
|
local use_ipv6=false
|
||||||
if [[ -n "$WG_SUBNET_V6" ]] && command -v ip6tables &>/dev/null; then
|
if [[ -n "$WG_SUBNET_V6" ]] && command -v ip6tables &>/dev/null; then
|
||||||
use_ipv6=true
|
use_ipv6=true
|
||||||
ensure_ipset "$IPSET_V6" "inet6"
|
ensure_ipset "$IPSET_V6" "inet6"
|
||||||
flush_ipset "$IPSET_V6"
|
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"
|
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
|
||||||
local v4_count v6_count
|
# Read all access entries and populate ipset
|
||||||
v4_count=$(ipset list "$IPSET_V4" 2>/dev/null | grep -c '^[0-9]' || echo 0)
|
jq -r '
|
||||||
v6_count=$(ipset list "$IPSET_V6" 2>/dev/null | grep -c '^[0-9a-f:]' || echo 0)
|
.clients // {} | to_entries[] |
|
||||||
log_info "ipset $IPSET_V4: $v4_count entries, $IPSET_V6: $v6_count 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 ===
|
# === RULE 1: ESTABLISHED,RELATED — allow return traffic ===
|
||||||
iptables -A "$CHAIN" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
iptables -A "$CHAIN" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||||
|
|
||||||
# === RULE 2: WHITELIST via ipset (per-client source) ===
|
# === RULE 2: WHITELIST (per-client source) ===
|
||||||
# For each client with access rules, allow only from that client's IP to ipset targets
|
if [[ "$use_ipset" == true ]]; then
|
||||||
jq -r '
|
jq -r '
|
||||||
.clients // {} | to_entries[] |
|
.clients // {} | to_entries[] |
|
||||||
select(.value.access != null and (.value.access | length > 0)) |
|
select(.value.access != null and (.value.access | length > 0)) |
|
||||||
"\(.key)"
|
"\(.key)"
|
||||||
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do
|
' "$POLICY_FILE" 2>/dev/null | while read -r client_ip; do
|
||||||
[[ -z "$client_ip" ]] && continue
|
[[ -z "$client_ip" ]] && continue
|
||||||
|
|
||||||
if [[ "$client_ip" == *":"* ]]; then
|
if [[ "$client_ip" == *":"* ]]; then
|
||||||
# IPv6 client
|
if [[ "$use_ipv6" == true ]]; 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
|
||||||
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
|
fi
|
||||||
else
|
done
|
||||||
# IPv4 client
|
else
|
||||||
iptables -A "$CHAIN" -s "$client_ip" -m set --match-set "$IPSET_V4" dst -j ACCEPT
|
jq -r '
|
||||||
fi
|
.clients // {} | to_entries[] |
|
||||||
done
|
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 ===
|
# === RULE 3: ISOLATION — drop NEW connections between WG clients ===
|
||||||
if [[ -n "$WG_SUBNET" ]]; then
|
if [[ -n "$WG_SUBNET" ]]; then
|
||||||
|
|||||||
@@ -129,8 +129,13 @@ validate_cidr() {
|
|||||||
# IPSET MANAGEMENT
|
# IPSET MANAGEMENT
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
|
has_ipset() {
|
||||||
|
command -v ipset &>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
ensure_ipset() {
|
ensure_ipset() {
|
||||||
local name="$1" family="$2"
|
local name="$1" family="$2"
|
||||||
|
has_ipset || return 0
|
||||||
if ! ipset list "$name" &>/dev/null; then
|
if ! ipset list "$name" &>/dev/null; then
|
||||||
ipset create "$name" hash:net family "$family" hashsize 1024 maxelem 65536 timeout 0
|
ipset create "$name" hash:net family "$family" hashsize 1024 maxelem 65536 timeout 0
|
||||||
log_info "Created ipset: $name (family=$family)"
|
log_info "Created ipset: $name (family=$family)"
|
||||||
@@ -139,6 +144,7 @@ ensure_ipset() {
|
|||||||
|
|
||||||
flush_ipset() {
|
flush_ipset() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
|
has_ipset || return 0
|
||||||
if ipset list "$name" &>/dev/null; then
|
if ipset list "$name" &>/dev/null; then
|
||||||
ipset flush "$name"
|
ipset flush "$name"
|
||||||
fi
|
fi
|
||||||
@@ -146,6 +152,7 @@ flush_ipset() {
|
|||||||
|
|
||||||
destroy_ipset() {
|
destroy_ipset() {
|
||||||
local name="$1"
|
local name="$1"
|
||||||
|
has_ipset || return 0
|
||||||
if ipset list "$name" &>/dev/null; then
|
if ipset list "$name" &>/dev/null; then
|
||||||
ipset destroy "$name"
|
ipset destroy "$name"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ StartLimitBurst=5
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
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
|
ExecStart=/usr/local/bin/wg-sync-watch.sh
|
||||||
ExecStopPost=/usr/local/bin/wg-policy-cleanup.sh
|
ExecStopPost=/usr/local/bin/wg-policy-cleanup.sh
|
||||||
Restart=always
|
Restart=always
|
||||||
|
|||||||
Reference in New Issue
Block a user