diff options
| author | Lukasz Kasprzak <lukasz.kasprzak@pm.me> | 2026-03-20 11:20:55 +0100 |
|---|---|---|
| committer | Lukasz Kasprzak <lukasz.kasprzak@pm.me> | 2026-03-20 11:20:55 +0100 |
| commit | d68fcbedc8d8250fcd93ed618e4b72ce5942da7d (patch) | |
| tree | 73f51d97f75d0be62449b47855bf6db80728e19e /straper | |
| parent | ece5febebba0ec7963985f32465b698790c58e8f (diff) | |
| download | bin-d68fcbedc8d8250fcd93ed618e4b72ce5942da7d.tar.gz bin-d68fcbedc8d8250fcd93ed618e4b72ce5942da7d.zip | |
feat: smart adaptive firewall restore
Diffstat (limited to 'straper')
| -rwxr-xr-x | straper/restore-configs.sh | 155 |
1 files changed, 149 insertions, 6 deletions
diff --git a/straper/restore-configs.sh b/straper/restore-configs.sh index 55ab81d..64000e7 100755 --- a/straper/restore-configs.sh +++ b/straper/restore-configs.sh @@ -1003,11 +1003,56 @@ ROOTKEY "adaptive DNS restore completed; manual_tasks=${#manual_tasks[@]}" "" } +# ── Firewall output helpers ─────────────────────────────────────────────────── +_fw_ok() { printf ' \033[32m✓\033[0m adapted %s\n' "$*"; } +_fw_dormant(){ printf ' \033[33m⚠\033[0m dormant %s\n' "$*"; } +_fw_manual() { printf ' \033[31m✗\033[0m MANUAL %s\n' "$*"; } +_fw_info() { printf ' \033[36m·\033[0m %s\n' "$*"; } + +_adapt_nftables() { + local src="$1" dst="$2" + local dormant_ifaces=() + local line tmp + tmp="$(mktemp)" + + while IFS= read -r line; do + # Extract quoted interface name from iif/oif/iifname/oifname directives + # Skip wildcard patterns like br-* + # Pattern: keyword followed by whitespace and a quoted non-wildcard name + local ref_iface="" + if echo "$line" | grep -qE '(iif|oif|iifname|oifname)[[:space:]]+"[^"*]+"'; then + # Extract the interface name that immediately follows an iif/oif keyword + ref_iface="$(echo "$line" | sed -E 's/.*\b(iif|oif|iifname|oifname)[[:space:]]+"([^"*]+)".*/\2/' | grep -v "^$line$" || true)" + fi + + if [[ -z "$ref_iface" ]]; then + printf '%s\n' "$line" >> "$tmp" + continue + fi + + if ip link show "$ref_iface" >/dev/null 2>&1; then + printf '%s\n' "$line" >> "$tmp" + else + local reason="dormant" + [[ "$ref_iface" =~ ^wg ]] && reason="dormant-wireguard" + [[ "$ref_iface" == enx* || "$ref_iface" == eth* ]] && reason="dormant-hardware-nic" + printf '# [adapted:%s] %s\n' "$reason" "$line" >> "$tmp" + local already=false + local d + for d in "${dormant_ifaces[@]:-}"; do [[ "$d" == "$ref_iface" ]] && already=true; done + $already || dormant_ifaces+=("$ref_iface") + fi + done < "$src" + + mv "$tmp" "$dst" + printf '%s\n' "${dormant_ifaces[@]:-}" +} + restore_firewall() { local base="${PUBLIC_DIR}/firewall" [[ -d "${base}" ]] || { mark_manual "${SCRIPT_NAME}" "firewall" "db" "missing ${base}"; return 0; } - if ! prompt_yes_no "Restore category 'firewall'? This can disrupt connectivity." no; then + if ! prompt_yes_no "Restore category 'firewall'? This will adapt and apply the captured ruleset." no; then report "${SCRIPT_NAME}" "firewall" "category" "restore" "skipped" "operator skipped category" "" return 0 fi @@ -1017,14 +1062,112 @@ restore_firewall() { return 0 fi - maybe_restore "firewall" "nftables.conf" "${base}/nftables.conf" "/etc/nftables.conf" - maybe_restore "firewall" "nftables.d" "${base}/nftables.d" "/etc/nftables.d" + local fw_manual_tasks=() + + # ── ADAPT AND RESTORE NFTABLES.CONF ──────────────────────────────────────── + if [[ -f "${base}/nftables.conf" ]]; then + log "firewall: adapting nftables.conf..." + if $DRY_RUN; then + printf '[dry] adapt nftables.conf → /etc/nftables.conf\n' + else + local adapted dormant_list + adapted="$(mktemp)" + dormant_list="$(_adapt_nftables "${base}/nftables.conf" "$adapted")" + cp "$adapted" /etc/nftables.conf + rm -f "$adapted" + chown root:root /etc/nftables.conf + chmod 0640 /etc/nftables.conf - if [[ ${DRY_RUN} == false && -f /etc/nftables.conf ]] && have_cmd nft; then - nft -c -f /etc/nftables.conf >/dev/null 2>&1 || note_failure "${SCRIPT_NAME}" "firewall" "validate" "check" "nftables config validation failed" + if [[ -n "$dormant_list" ]]; then + local docker_dormant=false + while IFS= read -r iface; do + [[ -z "$iface" ]] && continue + if [[ "$iface" =~ ^wg ]]; then + _fw_dormant "rules for ${iface} commented out (WireGuard — activate when ${iface} exists)" + fw_manual_tasks+=("FIREWALL: ${iface} rules dormant — activate when WireGuard is up") + elif [[ "$iface" == enx* || "$iface" == eth* ]]; then + _fw_dormant "rules for ${iface} commented out (NIC not present — update if using different interface name)" + fw_manual_tasks+=("FIREWALL: ${iface} rules dormant — replace with actual NIC name in /etc/nftables.conf") + elif [[ "$iface" =~ ^br- || "$iface" == docker0 ]]; then + docker_dormant=true + else + _fw_dormant "rules for ${iface} commented out (interface not present)" + fw_manual_tasks+=("FIREWALL: ${iface} rules dormant — review /etc/nftables.conf") + fi + done <<< "$dormant_list" + if $docker_dormant; then + _fw_dormant "Docker bridge rules commented out (Docker not yet up — will activate automatically when Docker starts)" + fw_manual_tasks+=("FIREWALL: Docker bridge rules dormant — start Docker and rules will activate on next nftables reload") + fi + report "${SCRIPT_NAME}" "firewall" "nftables.conf" "restore" "changed" "adapted: dormant interfaces commented out" "" + else + _fw_ok "nftables.conf (no adaptation needed)" + report "${SCRIPT_NAME}" "firewall" "nftables.conf" "restore" "changed" "restored unchanged" "" + fi + fi + else + mark_manual "${SCRIPT_NAME}" "firewall" "nftables.conf" "not found in DB" + fi + + # ── RESTORE NFTABLES.D ────────────────────────────────────────────────────── + if [[ -d "${base}/nftables.d" ]]; then + if $DRY_RUN; then + printf '[dry] restore nftables.d → /etc/nftables.d\n' + else + restore_path "${SCRIPT_NAME}" "firewall" "nftables.d" \ + "${base}/nftables.d" "/etc/nftables.d" || true + _fw_ok "nftables.d restored" + fi + fi + + # ── VALIDATE ──────────────────────────────────────────────────────────────── + if ! $DRY_RUN && [[ -f /etc/nftables.conf ]] && have_cmd nft; then + local validate_out + validate_out="$(nft -c -f /etc/nftables.conf 2>&1)" + if [[ -z "$validate_out" ]]; then + _fw_ok "nftables.conf validates cleanly" + report "${SCRIPT_NAME}" "firewall" "validate" "check" "ok" "nft -c passed" "" + else + _fw_manual "nftables.conf validation errors:" + while IFS= read -r vline; do _fw_info " $vline"; done <<< "$validate_out" + fw_manual_tasks+=("FIREWALL: fix remaining validation errors: sudo nft -c -f /etc/nftables.conf") + note_failure "${SCRIPT_NAME}" "firewall" "validate" "check" "nft -c failed" + fi + fi + + # ── LOAD ──────────────────────────────────────────────────────────────────── + if ! $DRY_RUN && [[ -f /etc/nftables.conf ]] && have_cmd nft; then + local load_out + load_out="$(nft -f /etc/nftables.conf 2>&1)" + if [[ -z "$load_out" ]]; then + _fw_ok "ruleset loaded successfully" + systemctl enable nftables >/dev/null 2>&1 || true + report "${SCRIPT_NAME}" "firewall" "load" "apply" "ok" "ruleset loaded" "" + else + _fw_manual "ruleset failed to load:" + while IFS= read -r lline; do _fw_info " $lline"; done <<< "$load_out" + fw_manual_tasks+=("FIREWALL: load manually once fixed: sudo nft -f /etc/nftables.conf") + note_failure "${SCRIPT_NAME}" "firewall" "load" "apply" "nft load failed" + fi + fi + + # ── SUMMARY ───────────────────────────────────────────────────────────────── + printf '\n ── Firewall restore summary ───────────────────────────────────────\n' + if [[ ${#fw_manual_tasks[@]} -gt 0 ]]; then + printf '\n ── MANUAL TASKS REQUIRED ──────────────────────────────────────────\n' + local i=1 + for task in "${fw_manual_tasks[@]}"; do + printf ' [%d] %s\n' "$i" "$task" + ((i+=1)) + done + printf '\n' + else + printf ' ── No manual tasks required ✓\n\n' fi -} + report "${SCRIPT_NAME}" "firewall" "category" "restore" "changed" \ + "adaptive firewall restore completed; manual_tasks=${#fw_manual_tasks[@]}" "" +} restore_nginx() { local base="${PUBLIC_DIR}/nginx/etc-nginx" [[ -d "${base}" ]] || { mark_manual "${SCRIPT_NAME}" "nginx" "db" "missing ${base}"; return 0; } |
