#!/usr/bin/bash
set -euo pipefail

ACTION="${1:-apply}"
ENV_FILE="${FT_ENV_FILE:-/etc/fasttunnel/fasttunnel.env}"
if [[ -r "$ENV_FILE" ]]; then
    set -a
    # shellcheck disable=SC1090
    source "$ENV_FILE"
    set +a
fi
: "${FT_NETWORK:=10.77.0.0/16}"
: "${FT_TUN:=ft0}"
: "${FT_EXIT:=0}"
: "${FT_FORWARD:=auto}"
: "${FT_MASQUERADE:=auto}"
: "${FT_EXT_IF:=auto}"
: "${FT_FIREWALL_BACKEND:=auto}"   # auto|firewalld|iptables

log() { printf '[NET] %s\n' "$*" >&2; }

if [[ "$FT_FORWARD" == "auto" ]]; then
    [[ "$FT_EXIT" == "1" ]] && FT_FORWARD=1 || FT_FORWARD=0
fi
if [[ "$FT_MASQUERADE" == "auto" ]]; then
    [[ "$FT_EXIT" == "1" ]] && FT_MASQUERADE=1 || FT_MASQUERADE=0
fi

if [[ "$FT_EXT_IF" == "auto" || -z "$FT_EXT_IF" ]]; then
    FT_EXT_IF="$(ip -4 route show default 2>/dev/null | awk 'NR==1 {for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}')"
fi

firewalld_active() {
    command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1
}

choose_backend() {
    case "$FT_FIREWALL_BACKEND" in
        auto)
            if firewalld_active; then echo firewalld
            elif command -v iptables >/dev/null 2>&1; then echo iptables
            else echo none
            fi
            ;;
        firewalld|iptables) echo "$FT_FIREWALL_BACKEND" ;;
        *) log "ERROR: FT_FIREWALL_BACKEND must be auto, firewalld or iptables"; exit 2 ;;
    esac
}

# ---------- iptables backend ----------
ipt_ensure_chain() {
    local table="$1" chain="$2"
    iptables -w -t "$table" -N "$chain" 2>/dev/null || true
}
ipt_ensure_jump() {
    local table="$1" parent="$2" child="$3"
    iptables -w -t "$table" -C "$parent" -j "$child" 2>/dev/null || \
        iptables -w -t "$table" -I "$parent" 1 -j "$child"
}
ipt_ensure_rule() {
    local table="$1" chain="$2"; shift 2
    iptables -w -t "$table" -C "$chain" "$@" 2>/dev/null || \
        iptables -w -t "$table" -A "$chain" "$@"
}
ipt_remove_jump_all() {
    local table="$1" parent="$2" child="$3"
    while iptables -w -t "$table" -C "$parent" -j "$child" 2>/dev/null; do
        iptables -w -t "$table" -D "$parent" -j "$child" || break
    done
}

apply_iptables() {
    command -v iptables >/dev/null 2>&1 || { log "ERROR: iptables backend requested but iptables is missing"; exit 1; }
    ipt_ensure_chain filter FASTTUNNEL_FWD
    ipt_ensure_jump  filter FORWARD FASTTUNNEL_FWD
    ipt_ensure_rule  filter FASTTUNNEL_FWD -i "$FT_TUN" -s "$FT_NETWORK" -o "$FT_EXT_IF" -m comment --comment "FastTunnel outbound" -j ACCEPT
    ipt_ensure_rule  filter FASTTUNNEL_FWD -i "$FT_EXT_IF" -o "$FT_TUN" -d "$FT_NETWORK" -m conntrack --ctstate ESTABLISHED,RELATED -m comment --comment "FastTunnel return" -j ACCEPT
    ipt_ensure_rule  filter FASTTUNNEL_FWD -j RETURN

    ipt_ensure_chain nat FASTTUNNEL_NAT
    ipt_ensure_jump  nat POSTROUTING FASTTUNNEL_NAT
    ipt_ensure_rule  nat FASTTUNNEL_NAT -s "$FT_NETWORK" -o "$FT_EXT_IF" -m comment --comment "FastTunnel masquerade" -j MASQUERADE
    ipt_ensure_rule  nat FASTTUNNEL_NAT -j RETURN
}

remove_iptables() {
    command -v iptables >/dev/null 2>&1 || return 0
    ipt_remove_jump_all filter FORWARD FASTTUNNEL_FWD
    iptables -w -t filter -F FASTTUNNEL_FWD 2>/dev/null || true
    iptables -w -t filter -X FASTTUNNEL_FWD 2>/dev/null || true
    ipt_remove_jump_all nat POSTROUTING FASTTUNNEL_NAT
    iptables -w -t nat -F FASTTUNNEL_NAT 2>/dev/null || true
    iptables -w -t nat -X FASTTUNNEL_NAT 2>/dev/null || true
}

# ---------- firewalld backend ----------
# Uses firewalld's direct interface with uniquely named FastTunnel chains.
# We remove/re-add only our exact objects; existing zones, services and rules stay untouched.
fw() { firewall-cmd "$@" >/dev/null; }
fw_try() { firewall-cmd "$@" >/dev/null 2>&1 || true; }

fw_add_chain_both() {
    local family="$1" table="$2" chain="$3"
    fw_try --direct --add-chain "$family" "$table" "$chain"
    fw_try --permanent --direct --add-chain "$family" "$table" "$chain"
}
fw_add_rule_both() {
    local family="$1" table="$2" chain="$3" prio="$4"; shift 4
    # Delete exact rule first so repeated starts are idempotent on firewalld versions
    # that do not provide a convenient direct --query-rule path.
    fw_try --direct --remove-rule "$family" "$table" "$chain" "$prio" "$@"
    fw_try --permanent --direct --remove-rule "$family" "$table" "$chain" "$prio" "$@"
    fw --direct --add-rule "$family" "$table" "$chain" "$prio" "$@"
    fw --permanent --direct --add-rule "$family" "$table" "$chain" "$prio" "$@"
}
fw_remove_rule_both() {
    local family="$1" table="$2" chain="$3" prio="$4"; shift 4
    fw_try --direct --remove-rule "$family" "$table" "$chain" "$prio" "$@"
    fw_try --permanent --direct --remove-rule "$family" "$table" "$chain" "$prio" "$@"
}

apply_firewalld() {
    firewalld_active || { log "ERROR: firewalld backend requested but firewalld is not active"; exit 1; }

    fw_add_chain_both ipv4 filter FASTTUNNEL_FWD
    fw_add_chain_both ipv4 nat FASTTUNNEL_NAT

    fw_add_rule_both ipv4 filter FORWARD 0 -j FASTTUNNEL_FWD
    fw_add_rule_both ipv4 filter FASTTUNNEL_FWD 10 -i "$FT_TUN" -s "$FT_NETWORK" -o "$FT_EXT_IF" -j ACCEPT
    fw_add_rule_both ipv4 filter FASTTUNNEL_FWD 20 -i "$FT_EXT_IF" -o "$FT_TUN" -d "$FT_NETWORK" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
    fw_add_rule_both ipv4 filter FASTTUNNEL_FWD 1000 -j RETURN

    fw_add_rule_both ipv4 nat POSTROUTING 0 -j FASTTUNNEL_NAT
    fw_add_rule_both ipv4 nat FASTTUNNEL_NAT 10 -s "$FT_NETWORK" -o "$FT_EXT_IF" -j MASQUERADE
    fw_add_rule_both ipv4 nat FASTTUNNEL_NAT 1000 -j RETURN
}

remove_firewalld() {
    command -v firewall-cmd >/dev/null 2>&1 || return 0

    fw_remove_rule_both ipv4 filter FORWARD 0 -j FASTTUNNEL_FWD
    fw_remove_rule_both ipv4 filter FASTTUNNEL_FWD 10 -i "$FT_TUN" -s "$FT_NETWORK" -o "$FT_EXT_IF" -j ACCEPT
    fw_remove_rule_both ipv4 filter FASTTUNNEL_FWD 20 -i "$FT_EXT_IF" -o "$FT_TUN" -d "$FT_NETWORK" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
    fw_remove_rule_both ipv4 filter FASTTUNNEL_FWD 1000 -j RETURN
    fw_remove_rule_both ipv4 nat POSTROUTING 0 -j FASTTUNNEL_NAT
    fw_remove_rule_both ipv4 nat FASTTUNNEL_NAT 10 -s "$FT_NETWORK" -o "$FT_EXT_IF" -j MASQUERADE
    fw_remove_rule_both ipv4 nat FASTTUNNEL_NAT 1000 -j RETURN

    fw_try --direct --remove-chain ipv4 filter FASTTUNNEL_FWD
    fw_try --permanent --direct --remove-chain ipv4 filter FASTTUNNEL_FWD
    fw_try --direct --remove-chain ipv4 nat FASTTUNNEL_NAT
    fw_try --permanent --direct --remove-chain ipv4 nat FASTTUNNEL_NAT
}

apply_rules() {
    [[ "$FT_FORWARD" == "1" || "$FT_MASQUERADE" == "1" ]] || { log "forwarding/NAT disabled for this node"; return 0; }

    if [[ "$FT_FORWARD" == "1" ]]; then
        sysctl -q -w net.ipv4.ip_forward=1
        # Mesh IPv6 forwarding is useful for relay nodes. Providers that prohibit
        # IPv6 may reject these writes; that must not break IPv4 operation.
        sysctl -q -w net.ipv6.conf.all.disable_ipv6=0 2>/dev/null || true
        sysctl -q -w net.ipv6.conf.default.disable_ipv6=0 2>/dev/null || true
        sysctl -q -w net.ipv6.conf.all.forwarding=1 2>/dev/null || true
        log "IPv4 forwarding enabled; IPv6 enable/forwarding requested"
    fi
    [[ "$FT_MASQUERADE" == "1" ]] || return 0
    [[ -n "$FT_EXT_IF" ]] || { log "ERROR: cannot detect external interface; set FT_EXT_IF=..."; exit 1; }

    local backend
    backend="$(choose_backend)"
    case "$backend" in
        firewalld) apply_firewalld ;;
        iptables) apply_iptables ;;
        none) log "ERROR: neither active firewalld nor iptables is available"; exit 1 ;;
    esac
    log "NAT ready: backend=$backend network=$FT_NETWORK tun=$FT_TUN ext=$FT_EXT_IF mode=MASQUERADE"
}

remove_rules() {
    # Remove both possible backends. Each function only removes FastTunnel-owned objects.
    remove_firewalld
    remove_iptables
    log "FastTunnel firewall objects removed; existing server firewall rules untouched"
}

case "$ACTION" in
    apply|up|start) apply_rules ;;
    remove|down|stop) remove_rules ;;
    status)
        echo "backend=$(choose_backend) network=$FT_NETWORK tun=$FT_TUN ext=$FT_EXT_IF exit=$FT_EXIT forward=$FT_FORWARD masquerade=$FT_MASQUERADE"
        ;;
    *) echo "Usage: $0 {apply|remove|status}" >&2; exit 2 ;;
esac
