Benutzer-Werkzeuge

Webseiten-Werkzeuge


public:f2b-dyn-v6-prefix-ignore

Fail2Ban: Dynamic IPv6 /56 Prefix in ignoreip (via DNS AAAA)

Why this exists

Some ISPs delegate a dynamic IPv6 prefix (commonly /56). After a reconnect the prefix can change. If you administer servers behind that ISP connection and you access them from home, Fail2Ban may ban your current client IPv6 address. After a prefix change you can lock yourself out again, because your „new“ IPv6 address is different but still inside the delegated prefix.

Goal:

  • Derive the currently delegated IPv6 prefix from a DNS record (AAAA)
  • Inject that prefix into Fail2Ban „ignoreip“ dynamically
  • Reload Fail2Ban only when the prefix changes

High-level flow:

DNS AAAA (vpn.example.tld) -> derive /56 prefix -> write Fail2Ban include -> reload Fail2Ban

Requirements

  • Fail2Ban installed and running
  • A DNS AAAA record that always points to an IPv6 address inside the current delegated prefix
  • A script that can derive the /56 prefix (e.g. output: 2001:db8:abcd:1200::/56)

File layout

  • /etc/fail2ban/jail.local
  • /etc/fail2ban/ignore-ip.gua-dynamic.conf (auto-generated, contains dyn_gua_prefix)
  • /usr/local/sbin/update-f2b-ignore-gua.sh (updates include file + reload)
  • systemd service + timer

Step 1: DNS record

Create a DNS record (AAAA) such as:

vpn.example.tld AAAA 2001:db8:abcd:12ab:xxxx:xxxx:xxxx:xxxx

Important:

  • The AAAA must always be within your *current* delegated prefix.
  • If your prefix changes, update this AAAA record (DynDNS).
  • TTL ~300s is a good starting point.

OR: Checkout > Blueprint: IPv6 WireGuard Endpoint with Dynamic ISP Prefix (/56) + OVH DynDNS (AAAA)

Step 2: Fail2Ban include file (auto-generated)

This file is written by the updater script:

/etc/fail2ban/ignore-ip.gua-dynamic.conf

Content example:

# Auto-generated – DO NOT EDIT
# Source: AAAA vpn.example.tld
# 2026-01-17T23:04:21+01:00
[DEFAULT]
dyn_gua_prefix = 2001:db8:abcd:1200::/56

Step 3: Wire it into Fail2Ban (jail.local)

In /etc/fail2ban/jail.local:

[INCLUDES]
before = paths-debian.conf
after  = /etc/fail2ban/ignore-ip.gua-dynamic.conf

[DEFAULT]
backend     = systemd
ignoreself  = true

# Static ignore ranges (examples)
# RFC1918 placeholder:
# 192.168.0.0/16
# ULA placeholder:
# fdXX:YYYY:ZZZZ::/48
ignoreip = 127.0.0.1/8 ::1 192.168.0.0/16 fdXX:YYYY:ZZZZ::/48 %(dyn_gua_prefix)s

Reload Fail2Ban after editing:

fail2ban-client reload || systemctl restart fail2ban

Verify:

fail2ban-client get sshd ignoreip

Step 4: Updater script

This script generates the include file and reloads Fail2Ban only if it changed. As hook implemeted is an „unban routine“ for all hosts within the new prefix.

Store as:

/usr/local/sbin/update-f2b-ignore-gua.sh
#!/usr/bin/env bash
set -euo pipefail
 
HOST="vpn.example.tld"
OUT="/etc/fail2ban/ignore-ip.gua-dynamic.conf"
 
AAAA="$(dig +short AAAA "$HOST" | head -n1)"
if [[ -z "${AAAA}" ]]; then
  echo "ERROR: no AAAA record for ${HOST}" >&2
  exit 1
fi
 
# Canonical /56 from whatever IPv6 string dig returns
PREFIX="$(python3 -c 'import ipaddress,sys; print(ipaddress.ip_network(sys.argv[1]+"/56", strict=False))' "$AAAA")"
 
# Read current prefix from OUT (if it exists)
CURRENT_PREFIX=""
if [[ -r "$OUT" ]]; then
  CURRENT_PREFIX="$(awk -F= '
    /^[[:space:]]*dyn_gua_prefix[[:space:]]*=/ {
      v=$2; gsub(/^[[:space:]]+|[[:space:]]+$/,"",v);
      print v; exit
    }' "$OUT" || true)"
fi
 
# No change? Do nothing, silently.
if [[ -n "${CURRENT_PREFIX}" && "${CURRENT_PREFIX}" == "${PREFIX}" ]]; then
  exit 0
fi
 
unban_prefix_ips_global() {
  local prefix="$1"
  local ips_to_unban
 
  ips_to_unban="$(fail2ban-client banned 2>/dev/null | python3 - <<'PY' "$prefix"
import sys, ast, ipaddress
 
net = ipaddress.ip_network(sys.argv[1], strict=False)
data = sys.stdin.read().strip()
 
try:
    obj = ast.literal_eval(data)
except Exception:
    print("")
    sys.exit(0)
 
ips=set()
if isinstance(obj, list):
    for item in obj:
        if isinstance(item, dict):
            for _jail, lst in item.items():
                if isinstance(lst, list):
                    for s in lst:
                        try:
                            ip = ipaddress.ip_address(s)
                        except Exception:
                            continue
                        if ip.version == 6 and ip in net:
                            ips.add(str(ip))
 
print(" ".join(sorted(ips)))
PY
)"
 
  [[ -z "${ips_to_unban}" ]] && return 0
 
  local count=0 ip
  for ip in ${ips_to_unban}; do
    fail2ban-client unban "$ip" >/dev/null || true
    count=$((count+1))
  done
 
  logger -t fail2ban-dynprefix "unban-hook: unbanned ${count} IPs from ${prefix}"
}
 
# Write updated config (timestamp is fine now, we no longer compare full file)
tmp="$(mktemp)"
cat >"$tmp" <<EOF
# Auto-generated – DO NOT EDIT
# Source: AAAA ${HOST}
# $(date -Is)
[DEFAULT]
dyn_gua_prefix = ${PREFIX}
EOF
install -m 0644 "$tmp" "$OUT"
rm -f "$tmp"
 
# Healthcheck: is fail2ban responsive?
if ! fail2ban-client ping >/dev/null 2>&1; then
  # Service might be down/hung - try restart once
  systemctl restart fail2ban
fi
 
# Reload (or restart fallback)
systemctl reload fail2ban || systemctl restart fail2ban
 
# Unban any already-banned IPs from your PD
unban_prefix_ips_global "$PREFIX"
 
logger -t fail2ban-dynprefix "updated ignoreip with ${PREFIX} (from ${AAAA})"


Permissions:

chmod 0755 /usr/local/sbin/update-f2b-ignore-gua.sh

Manual test:

/usr/local/sbin/update-f2b-ignore-gua.sh
journalctl -t fail2ban-dynprefix -n 20 --no-pager

Step 5: systemd service + timer

Create:

/etc/systemd/system/f2b-dynprefix.service
[Unit]
Description=Fail2Ban dynamic IPv6 /56 ignoreip updater (via DNS)
After=network-online.target
Wants=network-online.target
 
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/update-f2b-ignore-gua.sh

Create:

/etc/systemd/system/f2b-dynprefix.timer
[Unit]
Description=Run Fail2Ban dynprefix updater periodically
 
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
Persistent=true
 
[Install]
WantedBy=timers.target

Enable:

systemctl daemon-reload
systemctl enable --now f2b-dynprefix.timer
systemctl list-timers --all | grep f2b-dynprefix

Logs:

journalctl -u f2b-dynprefix.service -n 50 --no-pager


Don't forget to adjust all placeholders to your environment!

Note: This prefix-match is intentionally simple. If you want it perfect, implement real IPv6 CIDR matching.

Enjoy!

public/f2b-dyn-v6-prefix-ignore.txt · Zuletzt geändert: von gerson

Seiten-Werkzeuge