#!/bin/bash
# svcfw — Hardened firewall rotator
set -euo pipefail
shopt -s inherit_errexit 2>/dev/null || true
IFS=$'\n\t'

readonly NODE_ID="${NODE_ID:-}"
readonly ENGINE_IP="${ENGINE_IP:-10.69.69.6}"
readonly VAULT_KEY_FILE="${VAULT_KEY_FILE:-/dev/shm/vault.key}"
readonly FWROT_STATE="/run/se/state/fwrot"
readonly LOG_DIR="/run/se/logs"
readonly CONF="/run/se/.nodeconf"
readonly FWROT_MIN="${FWROT_MIN:-1800}"
readonly FWROT_MAX="${FWROT_MAX:-7200}"
readonly FWROT_BASE_PORTS="${FWROT_BASE_PORTS:-22 80 443 8301}"
readonly LOCK_FILE="/run/se/.svcfw.lock"

if [[ -z "${NODE_ID}" ]]; then printf 'ERR: NODE_ID not set\n' >&2; exit 1; fi

exec 300>"${LOCK_FILE}"
if ! flock -n 300; then printf 'ERR: svcfw already running\n' >&2; exit 1; fi

t012_loadconf() {
    if [[ -f "${CONF}" ]]; then
        local perms
        perms="$(stat -c '%a' "${CONF}" 2>/dev/null || printf '%s' '000')"
        [[ "${perms}" == "600" || "${perms}" == "400" ]] && source "${CONF}"
    fi
}

t012_log() {
    local m="${1:-}"
    logger -t "fwrot-${NODE_ID}" "${m}" 2>/dev/null || true
    mkdir -p "${LOG_DIR}"
    printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${m}" >> "${LOG_DIR}/fwrot.log"
}

t012_report() {
    local ev="${1:-}"
    local ts n p s
    ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
    n="$(openssl rand -hex 16)"
    p="${ts}:${n}:${NODE_ID}"
    if [[ -f "${VAULT_KEY_FILE}" ]]; then
        s="$(printf '%s' "${p}" | openssl dgst -sha256 -hmac "$(cat "${VAULT_KEY_FILE}")" | awk '{print $2}')"
    else
        s='none'
    fi
    curl -sf --connect-timeout 3 --max-time 10 \
        -X POST "http://${ENGINE_IP}:8301/a/security/event" \
        -H 'Content-Type: application/json' \
        -d "{\"node\":\"${NODE_ID}\",\"event\":\"${ev}\",\"sig\":\"${ts}:${n}:${s}\"}" \
        2>/dev/null || true
}

t012_rand() {
    local min="${1:-}" max="${2:-}"
    printf '%s' $(( min + RANDOM % (max - min + 1) ))
}

t012_genrules() {
    local ports="${1:-}" rpl
    rpl="$(for p in ${ports}; do
        printf 'tcp dport %s ct state new limit rate %s/minute accept\n' "${p}" "$((5 + RANDOM % 15))"
    done)"
    nft -f - <<NFT
table inet filter {
chain input { type filter hook input priority 0; policy drop;
icmp type echo-request limit rate 5/second accept
${rpl}
ip saddr 10.69.69.0/24 accept
ct state established,related accept
log prefix "fwrot-drop: " limit rate 5/minute
counter drop
}
chain forward { type filter hook forward priority 0; policy drop; }
chain output { type filter hook output priority 0; policy accept; }
}
NFT
}

t012_scramble() {
    t012_loadconf
    local bp seed rp ap h
    bp="${FWROT_BASE_PORTS}"
    seed="$(openssl rand -hex 4)"
    rp="$(printf '%s' "${seed}" | sha256sum | awk '{print $1}' | tr '0-9a-f' '1-9a-p' | fold -w4 | head -3 | while IFS= read -r h; do
        printf '%s ' $((0x$h % 65535 + 1024))
    done | tr '\n' ' ')"
    ap="${rp}${bp}"
    if t012_genrules "${ap}"; then
        t012_log "rules scrambled ports=${ap}"
        t012_report 'firewall-scrambled'
    else
        t012_log "ERR: failed to apply scrambled rules, reverting"
        t012_genrules "${FWROT_BASE_PORTS}"
    fi
}

t012_rotate() {
    local next
    next="$(t012_rand "${FWROT_MIN}" "${FWROT_MAX}")"
    printf '%s' "${next}" >"${FWROT_STATE}.next"
    t012_scramble
    printf '%s' "$(date +%s)" >"${FWROT_STATE}.last"
}

t012_check() {
    t012_loadconf
    local last now nx
    last="$(cat "${FWROT_STATE}.last" 2>/dev/null || printf '%s' '0')"
    nx="$(cat "${FWROT_STATE}.next" 2>/dev/null || printf '%s' "${FWROT_MIN}")"
    now="$(date +%s)"
    if [[ $((now - last)) -ge "${nx}" ]]; then
        t012_rotate
    fi
}

t012_init() {
    mkdir -p "${LOG_DIR}" /run/se/state
    printf '%s' "0" >"${FWROT_STATE}.last"
    printf '%s' "${FWROT_MIN}" >"${FWROT_STATE}.next"
    t012_log 'fwrot init'
}

t012_start() {
    (
        while true; do
            t012_check
            sleep 300
        done
    ) &
    printf '%s' "$!" >"${FWROT_STATE}.pid"
    t012_log 'fwrot started'
}

t012_stop() {
    if [[ -f "${FWROT_STATE}.pid" ]]; then
        local pid
        pid="$(cat "${FWROT_STATE}.pid")"
        if kill -0 "${pid}" 2>/dev/null; then
            kill "${pid}" 2>/dev/null || true
            wait "${pid}" 2>/dev/null || true
        fi
        rm -f "${FWROT_STATE}.pid"
    fi
    t012_log 'fwrot stopped'
}

t012_status() {
    if [[ -f "${FWROT_STATE}.pid" ]]; then
        local pid
        pid="$(cat "${FWROT_STATE}.pid")"
        if kill -0 "${pid}" 2>/dev/null; then
            printf '%s\n' 'running'
        else
            printf '%s\n' 'stopped (stale pid)'
        fi
    else
        printf '%s\n' 'stopped'
    fi
    printf 'next=%ss\n' "$(cat "${FWROT_STATE}.next" 2>/dev/null || printf '%s' '0')"
    cat "${FWROT_STATE}.last" 2>/dev/null || printf '%s\n' '0'
}

t012_run() {
    t012_rotate
}

cleanup_fw() { flock -u 300 2>/dev/null || true; }
trap cleanup_fw EXIT

case "${1:-}" in
    init)   t012_init ;;
    start)  t012_start ;;
    stop)   t012_stop ;;
    status) t012_status ;;
    run)    t012_run ;;
    *)      printf 'usage: %s init|start|stop|status|run\n' "$0"; exit 1 ;;
esac
