#!/bin/sh
# =============================================================================
# QualityNOC VPN auto-provisioning for Teltonika RUTM10 (RUTOS 7.x)
# -----------------------------------------------------------------------------
# At boot, reads the VPN tag from /etc/qualitynoc/vpn_tag, downloads the
# matching configuration from vpn.qualitynoc.net using the router serial
# number, removes the previous QualityNOC VPN instance, and applies the new
# config via UCI. Restarts the relevant service.
#
# All working files (state, lock, staging downloads, certs) live in
# /etc/qualitynoc/ because some Teltonika models have /tmp or /var read-only.
#
# OpenVPN handling (v1.3.0+): the .ovpn is parsed and mapped to individual
# UCI options so the RUTOS WebUI sees real fields and does NOT wipe the
# section if an admin opens it. Embedded <ca>/<cert>/<key>/<tls-crypt>
# blocks are extracted to /etc/openvpn/<vpn>-*.{crt,key}. A pull-filter is
# injected to ignore server-pushed `redirect-gateway` (would otherwise
# install a default route via the tunnel and break RMS Remote Access).
#
# Tag file content:   "wgvpn" -> WireGuard
#                     "ovpn"  -> OpenVPN
#
# Logs go to syslog with tag "qualitynoc-vpn" (view with: logread | grep qualitynoc-vpn)
# =============================================================================

set -u

LOG_TAG="qualitynoc-vpn"
TAG_FILE="/etc/qualitynoc/vpn_tag"
VPN_NAME="qualitynoc"
WG_BASE_URL="https://vpn.qualitynoc.net/WG-VPN-C3rT"
OVPN_BASE_URL="https://vpn.qualitynoc.net/oVPN-C3rTifIc4t3S"
WG_IFACE="wg_${VPN_NAME}"
OVPN_FILE="/etc/openvpn/${VPN_NAME}.conf"
OVPN_CERT_DIR="/etc/openvpn"
STATE_DIR="/etc/qualitynoc"
NET_HOST="vpn.qualitynoc.net"

mkdir -p "$STATE_DIR"

log()  { logger -t "$LOG_TAG" -- "$*"; echo "[${LOG_TAG}] $*"; }
die()  { log "ERROR: $*"; exit 1; }
warn() { log "WARN: $*"; }

wait_for_network() {
    log "Waiting for network connectivity to ${NET_HOST}..."
    i=0
    while [ "$i" -lt 60 ]; do
        if ping -c 1 -W 2 "$NET_HOST" >/dev/null 2>&1; then
            log "Network is up (host=${NET_HOST})."
            return 0
        fi
        if ping -c 1 -W 2 1.1.1.1 >/dev/null 2>&1 && [ "$i" -gt 5 ]; then
            log "Internet reachable, retrying ${NET_HOST}..."
        fi
        sleep 2
        i=$((i+1))
    done
    die "No network connectivity after ~120 seconds"
}

get_serial() {
    sn=""
    for cmd in "mnf_info -s" "mnf_info --sn" "mnf_info -n"; do
        out=$($cmd 2>/dev/null | tr -d '\r\n"' | sed 's/^[ 	]*//;s/[ 	]*$//')
        case "$out" in
            ""|*[Uu]nknown*|*not\ found*) ;;
            *) sn="$out"; break ;;
        esac
    done
    if [ -z "$sn" ] && [ -r /proc/device-tree/serial-number ]; then
        sn=$(tr -d '\0' < /proc/device-tree/serial-number)
    fi
    [ -z "$sn" ] && die "Could not read router serial number (mnf_info / device-tree both failed)"
    printf '%s' "$sn"
}

read_tag() {
    [ -f "$TAG_FILE" ] || die "Tag file not found: $TAG_FILE (write 'wgvpn' or 'ovpn' into it)"
    head -n1 "$TAG_FILE" | tr -d ' \t\r\n' | tr 'A-Z' 'a-z'
}

# -----------------------------------------------------------------------------
# Configure LAN-via-VPN policy routing and firewall.
# Argument: "ovpn" or "wgvpn" (used to pick the active iface).
# Idempotent — safe to call multiple times.
# Maintains a unified hotplug script that handles both VPN types.
# -----------------------------------------------------------------------------
setup_lan_via_vpn() {
    vpn_kind="$1"

    log "Installing LAN-via-VPN policy routing (kind=${vpn_kind})"
    mkdir -p /etc/hotplug.d/iface
    cat > /etc/hotplug.d/iface/99-qualitynoc-vpn-policy << 'HOTPLUG_EOF'
#!/bin/sh
# Auto-generated by qualitynoc-vpn-provision: route LAN traffic through the active VPN.
# Triggered by netifd / openvpn-hotplug on iface up/down.
case "$INTERFACE" in
    qualitynoc)         tun_dev="$DEVICE" ;;        # OpenVPN instance
    wg_qualitynoc)      tun_dev="wg_qualitynoc" ;;  # WireGuard interface
    *) exit 0 ;;
esac
case "$ACTION" in
    ifup)
        ip rule del iif br-lan table 100 priority 100 2>/dev/null
        ip route flush table 100 2>/dev/null
        ip rule add iif br-lan table 100 priority 100
        ip route add default dev "$tun_dev" table 100
        logger -t qualitynoc-vpn "policy routing applied: br-lan -> $tun_dev via table 100"
        ;;
    ifdown)
        ip rule del iif br-lan table 100 priority 100 2>/dev/null
        ip route flush table 100 2>/dev/null
        logger -t qualitynoc-vpn "policy routing removed ($INTERFACE down)"
        ;;
esac
HOTPLUG_EOF
    chmod +x /etc/hotplug.d/iface/99-qualitynoc-vpn-policy

    # Find the active iface and apply the rules immediately.
    iface=""
    if [ "$vpn_kind" = "wgvpn" ]; then
        iface="wg_qualitynoc"
    else
        # OpenVPN: tun_qualitynoc if named, otherwise any tun*
        for i in 1 2 3 4 5 6 7 8 9 10; do
            if [ -d /sys/class/net/tun_qualitynoc ]; then
                iface="tun_qualitynoc"; break
            fi
            for d in /sys/class/net/tun*; do
                [ -d "$d" ] || continue
                iface=$(basename "$d"); break
            done
            [ -n "$iface" ] && break
            sleep 2
        done
    fi
    if [ -n "$iface" ] && [ -d "/sys/class/net/${iface}" ]; then
        ip rule del iif br-lan table 100 priority 100 2>/dev/null
        ip route flush table 100 2>/dev/null
        ip rule add iif br-lan table 100 priority 100
        ip route add default dev "$iface" table 100
        log "Policy routing applied immediately on ${iface}"
    else
        warn "Could not find active VPN iface; hotplug will apply on next ifup event"
    fi

    # === Firewall: qnoc_vpn zone with masquerade + lan->qnoc_vpn forwarding ===
    # Migration: legacy 'qualitynoc_vpn' (>11 chars, silently dropped by fw3)
    if uci -q get firewall.qualitynoc_vpn >/dev/null 2>&1; then
        log "Removing legacy firewall zone 'qualitynoc_vpn' (renamed to 'qnoc_vpn')"
        uci delete firewall.qualitynoc_vpn
        i=0
        while uci -q get "firewall.@forwarding[$i]" >/dev/null 2>&1; do
            d=$(uci -q get "firewall.@forwarding[$i].dest" 2>/dev/null || echo "")
            [ "$d" = "qualitynoc_vpn" ] && uci set "firewall.@forwarding[$i].dest=qnoc_vpn"
            i=$((i+1))
        done
    fi

    if ! uci -q get firewall.qnoc_vpn >/dev/null 2>&1; then
        log "Creating firewall zone 'qnoc_vpn'"
        uci set firewall.qnoc_vpn=zone
        uci set firewall.qnoc_vpn.name=qnoc_vpn
        uci set firewall.qnoc_vpn.input=REJECT
        uci set firewall.qnoc_vpn.output=ACCEPT
        uci set firewall.qnoc_vpn.forward=REJECT
        uci set firewall.qnoc_vpn.masq=1
        uci set firewall.qnoc_vpn.mtu_fix=1
    fi

    # Ensure both tun+ and wg_qualitynoc are in the device list (idempotent)
    cur_devs=$(uci -q get firewall.qnoc_vpn.device 2>/dev/null || echo "")
    if ! echo "$cur_devs" | grep -qw "tun+"; then
        uci add_list firewall.qnoc_vpn.device='tun+'
    fi
    if ! echo "$cur_devs" | grep -qw "wg_qualitynoc"; then
        uci add_list firewall.qnoc_vpn.device='wg_qualitynoc'
    fi

    fw_exists=0
    i=0
    while uci -q get "firewall.@forwarding[$i]" >/dev/null 2>&1; do
        s=$(uci -q get "firewall.@forwarding[$i].src" 2>/dev/null || echo "")
        d=$(uci -q get "firewall.@forwarding[$i].dest" 2>/dev/null || echo "")
        if [ "$s" = "lan" ] && [ "$d" = "qnoc_vpn" ]; then fw_exists=1; break; fi
        i=$((i+1))
    done
    if [ "$fw_exists" = "0" ]; then
        log "Adding firewall forwarding lan -> qnoc_vpn"
        fw=$(uci add firewall forwarding)
        uci set "firewall.${fw}.src=lan"
        uci set "firewall.${fw}.dest=qnoc_vpn"
    fi

    uci commit firewall
    log "Reloading firewall to apply VPN zone"
    /etc/init.d/firewall reload >/dev/null 2>&1 || warn "firewall reload returned non-zero"
}

provision_openvpn() {
    serial="$1"
    url="${OVPN_BASE_URL}/${serial}.ovpn"
    tmp="${STATE_DIR}/staging.ovpn.new"

    log "Downloading OpenVPN config: $url"
    if ! curl -fsS --retry 3 --retry-delay 5 --max-time 90 -o "$tmp" "$url"; then
        die "Failed to download OpenVPN config from $url"
    fi

    if ! grep -qiE '^[ 	]*remote[ 	]+' "$tmp"; then
        rm -f "$tmp"
        die "Downloaded file is not a valid .ovpn (no 'remote' directive)"
    fi

    # Inject pull-filter to ignore server-pushed redirect-gateway (preserves management).
    if ! grep -q 'pull-filter ignore "redirect-gateway"' "$tmp"; then
        printf '\n# Injected by qualitynoc-vpn-provision: keep management traffic on WAN\npull-filter ignore "redirect-gateway"\n' >> "$tmp"
    fi

    mkdir -p "$OVPN_CERT_DIR"

    # Cert file targets (inline blocks get extracted here)
    ca_file="${OVPN_CERT_DIR}/${VPN_NAME}-ca.crt"
    cert_file="${OVPN_CERT_DIR}/${VPN_NAME}-cert.crt"
    key_file="${OVPN_CERT_DIR}/${VPN_NAME}-key.key"
    tls_crypt_file="${OVPN_CERT_DIR}/${VPN_NAME}-tls-crypt.key"
    tls_auth_file="${OVPN_CERT_DIR}/${VPN_NAME}-tls-auth.key"
    rm -f "$ca_file" "$cert_file" "$key_file" "$tls_crypt_file" "$tls_auth_file"

    # Parsed directive vars
    proto=""; remote_host=""; remote_port=""; dev=""
    cipher=""; auth=""; tls_version_min=""; tls_cipher=""
    verify_x509_name=""; remote_cert_tls=""
    has_persist_key=0; has_persist_tun=0; has_nobind=0; has_tls_client=0
    has_auth_nocache=0; resolv_retry=""; verb=""

    in_block=""
    while IFS= read -r line || [ -n "$line" ]; do
        # Detect block boundaries (case-sensitive, on their own lines)
        case "$line" in
            "<ca>")          in_block="ca"        ; continue ;;
            "</ca>")         in_block=""          ; continue ;;
            "<cert>")        in_block="cert"      ; continue ;;
            "</cert>")       in_block=""          ; continue ;;
            "<key>")         in_block="key"       ; continue ;;
            "</key>")        in_block=""          ; continue ;;
            "<tls-crypt>")   in_block="tls_crypt" ; continue ;;
            "</tls-crypt>")  in_block=""          ; continue ;;
            "<tls-auth>")    in_block="tls_auth"  ; continue ;;
            "</tls-auth>")   in_block=""          ; continue ;;
        esac

        # If inside a block, append the line to the appropriate cert file
        if [ -n "$in_block" ]; then
            case "$in_block" in
                ca)        printf '%s\n' "$line" >> "$ca_file" ;;
                cert)      printf '%s\n' "$line" >> "$cert_file" ;;
                key)       printf '%s\n' "$line" >> "$key_file" ;;
                tls_crypt) printf '%s\n' "$line" >> "$tls_crypt_file" ;;
                tls_auth)  printf '%s\n' "$line" >> "$tls_auth_file" ;;
            esac
            continue
        fi

        # Parse directive (outside blocks). Strip comments (# and ;) and trim.
        cleaned=$(printf '%s' "$line" | sed -e 's/[#;].*$//' -e 's/^[ 	]*//' -e 's/[ 	]*$//')
        [ -z "$cleaned" ] && continue

        directive=$(printf '%s' "$cleaned" | awk '{print $1}')
        rest=$(printf '%s' "$cleaned" | awk '{$1=""; sub(/^ */,""); print}')

        case "$directive" in
            client)            ;;  # implicit when option client=1 is set
            proto)             proto="$rest" ;;
            remote)            remote_host=$(printf '%s' "$rest" | awk '{print $1}')
                               remote_port=$(printf '%s' "$rest" | awk '{print $2}') ;;
            dev)               dev="$rest" ;;
            cipher)            cipher="$rest" ;;
            auth)              auth="$rest" ;;
            tls-version-min)   tls_version_min="$rest" ;;
            tls-cipher)        tls_cipher="$rest" ;;
            verify-x509-name)  verify_x509_name="$rest" ;;
            remote-cert-tls)   remote_cert_tls="$rest" ;;
            persist-key)       has_persist_key=1 ;;
            persist-tun)       has_persist_tun=1 ;;
            nobind)            has_nobind=1 ;;
            tls-client)        has_tls_client=1 ;;
            auth-nocache)      has_auth_nocache=1 ;;
            resolv-retry)      resolv_retry="$rest" ;;
            verb)              verb="$rest" ;;
        esac
    done < "$tmp"

    [ -z "$remote_host" ] && die "OVPN config: missing remote host"
    [ -z "$remote_port" ] && die "OVPN config: missing remote port"

    # Tighten cert perms
    chmod 600 "$ca_file" "$cert_file" "$key_file" "$tls_crypt_file" "$tls_auth_file" 2>/dev/null

    # Save modified .ovpn (with pull-filter) as fallback for `option config`
    mv "$tmp" "$OVPN_FILE"
    chmod 600 "$OVPN_FILE"

    # Disable other openvpn instances to avoid conflicts (best effort)
    instances=$(uci show openvpn 2>/dev/null | awk -F'[.=]' '/=openvpn$/ {print $2}')
    for inst in $instances; do
        [ "$inst" = "$VPN_NAME" ] && continue
        cur=$(uci -q get "openvpn.${inst}.enable" 2>/dev/null || echo "")
        if [ "$cur" = "1" ]; then
            log "Disabling pre-existing OpenVPN instance: ${inst}"
            uci set "openvpn.${inst}.enable=0"
        fi
    done

    # Wipe any prior 'qualitynoc' instance and recreate from parsed values
    if uci -q get "openvpn.${VPN_NAME}" >/dev/null 2>&1; then
        log "Removing previous OpenVPN instance: ${VPN_NAME}"
        uci delete "openvpn.${VPN_NAME}"
    fi

    log "Creating OpenVPN instance ${VPN_NAME} from parsed .ovpn"
    uci set "openvpn.${VPN_NAME}=openvpn"
    uci set "openvpn.${VPN_NAME}.enable=1"
    uci set "openvpn.${VPN_NAME}.client=1"
    [ -n "$proto" ]              && uci set "openvpn.${VPN_NAME}.proto=${proto}"
    uci set "openvpn.${VPN_NAME}.remote=${remote_host}"
    uci set "openvpn.${VPN_NAME}.port=${remote_port}"
    [ -n "$dev" ]                && uci set "openvpn.${VPN_NAME}.dev=${dev}"
    [ "$has_nobind" = "1" ]      && uci set "openvpn.${VPN_NAME}.nobind=1"
    [ "$has_persist_key" = "1" ] && uci set "openvpn.${VPN_NAME}.persist_key=1"
    [ "$has_persist_tun" = "1" ] && uci set "openvpn.${VPN_NAME}.persist_tun=1"
    [ -n "$remote_cert_tls" ]    && uci set "openvpn.${VPN_NAME}.remote_cert_tls=${remote_cert_tls}"
    [ -n "$verify_x509_name" ]   && uci set "openvpn.${VPN_NAME}.verify_x509_name=${verify_x509_name}"
    [ -n "$auth" ]               && uci set "openvpn.${VPN_NAME}.auth=${auth}"
    [ "$has_auth_nocache" = "1" ] && uci set "openvpn.${VPN_NAME}.auth_nocache=1"
    [ -n "$cipher" ]             && uci set "openvpn.${VPN_NAME}.cipher=${cipher}"
    [ "$has_tls_client" = "1" ]  && uci set "openvpn.${VPN_NAME}.tls_client=1"
    [ -n "$tls_version_min" ]    && uci set "openvpn.${VPN_NAME}.tls_version_min=${tls_version_min}"
    [ -n "$tls_cipher" ]         && uci set "openvpn.${VPN_NAME}.tls_cipher=${tls_cipher}"
    [ -n "$resolv_retry" ]       && uci set "openvpn.${VPN_NAME}.resolv_retry=${resolv_retry}"
    [ -n "$verb" ]               && uci set "openvpn.${VPN_NAME}.verb=${verb}"

    # Cert file references (only if the file is non-empty)
    [ -s "$ca_file" ]         && uci set "openvpn.${VPN_NAME}.ca=${ca_file}"
    [ -s "$cert_file" ]       && uci set "openvpn.${VPN_NAME}.cert=${cert_file}"
    [ -s "$key_file" ]        && uci set "openvpn.${VPN_NAME}.key=${key_file}"
    [ -s "$tls_crypt_file" ]  && uci set "openvpn.${VPN_NAME}.tls_crypt=${tls_crypt_file}"
    [ -s "$tls_auth_file" ]   && uci set "openvpn.${VPN_NAME}.tls_auth=${tls_auth_file}"

    # Pull filter to ignore server-pushed redirect-gateway (preserves management)
    uci -q delete "openvpn.${VPN_NAME}.pull_filter"
    uci add_list "openvpn.${VPN_NAME}.pull_filter=ignore \"redirect-gateway\""

    # Safety net: also keep `option config` so even if some UCI option name above
    # is wrong for this RUTOS version, openvpn still reads the .ovpn file.
    uci set "openvpn.${VPN_NAME}.config=${OVPN_FILE}"

    uci commit openvpn

    log "Restarting OpenVPN service"
    /etc/init.d/openvpn restart || warn "openvpn restart returned non-zero"

    setup_lan_via_vpn "ovpn"

    log "OpenVPN provisioning complete (parsed config, certs extracted, LAN-via-VPN policy active)."
}

provision_wireguard() {
    serial="$1"
    url="${WG_BASE_URL}/${serial}.conf"
    tmp="${STATE_DIR}/staging.wg.conf"

    log "Downloading WireGuard config: $url"
    if ! curl -fsS --retry 3 --retry-delay 5 --max-time 90 -o "$tmp" "$url"; then
        die "Failed to download WireGuard config from $url"
    fi

    grep -qE '^\[Interface\]'  "$tmp" || die "WG config missing [Interface] section"
    grep -qE '^\[Peer\]'       "$tmp" || die "WG config missing [Peer] section"

    section=""
    iface_priv=""; iface_addr=""; iface_dns=""; iface_mtu=""; iface_port=""
    peer_pub="";   peer_psk="";   peer_endpoint=""; peer_allowed=""; peer_keepalive=""

    while IFS= read -r line || [ -n "$line" ]; do
        cleaned=$(printf '%s' "$line" | sed -e 's/#.*$//' -e 's/^[ 	]*//' -e 's/[ 	]*$//')
        [ -z "$cleaned" ] && continue
        case "$cleaned" in
            \[Interface\]) section="iface" ; continue ;;
            \[Peer\])      section="peer"  ; continue ;;
        esac
        key=$(printf '%s' "$cleaned" | cut -d= -f1 | tr -d ' \t\r\n' | tr 'A-Z' 'a-z')
        val=$(printf '%s' "$cleaned" | cut -d= -f2- | sed -e 's/^[ 	]*//' -e 's/[ 	]*$//')
        if [ "$section" = "iface" ]; then
            case "$key" in
                privatekey) iface_priv="$val" ;;
                address)    iface_addr="$val" ;;
                dns)        iface_dns="$val"  ;;
                mtu)        iface_mtu="$val"  ;;
                listenport) iface_port="$val" ;;
            esac
        elif [ "$section" = "peer" ]; then
            case "$key" in
                publickey)            peer_pub="$val"      ;;
                presharedkey)         peer_psk="$val"      ;;
                endpoint)             peer_endpoint="$val" ;;
                allowedips)           peer_allowed="$val"  ;;
                persistentkeepalive)  peer_keepalive="$val";;
            esac
        fi
    done < "$tmp"

    [ -z "$iface_priv" ]    && die "WG config: missing PrivateKey"
    [ -z "$iface_addr" ]    && die "WG config: missing Address"
    [ -z "$peer_pub" ]      && die "WG config: missing peer PublicKey"
    [ -z "$peer_endpoint" ] && die "WG config: missing peer Endpoint"

    case "$peer_endpoint" in
        \[*\]:*) ep_host=$(printf '%s' "$peer_endpoint" | sed -n 's/^\[\(.*\)\]:.*/\1/p')
                 ep_port=$(printf '%s' "$peer_endpoint" | sed -n 's/^\[.*\]:\(.*\)/\1/p') ;;
        *:*)     ep_host=$(printf '%s' "$peer_endpoint" | sed 's/:[0-9]*$//')
                 ep_port=$(printf '%s' "$peer_endpoint" | sed 's/.*://') ;;
        *)       die "Endpoint missing port: $peer_endpoint" ;;
    esac
    [ -z "$ep_host" ] && die "Could not parse endpoint host from $peer_endpoint"
    [ -z "$ep_port" ] && die "Could not parse endpoint port from $peer_endpoint"

    if uci -q get "network.${WG_IFACE}" >/dev/null 2>&1; then
        log "Removing previous WireGuard interface: ${WG_IFACE}"
        uci delete "network.${WG_IFACE}"
    fi
    guard=0
    while uci -q get "network.@wireguard_${WG_IFACE}[0]" >/dev/null 2>&1; do
        uci delete "network.@wireguard_${WG_IFACE}[0]"
        guard=$((guard+1))
        [ "$guard" -gt 32 ] && break
    done

    log "Creating WireGuard interface: ${WG_IFACE}"
    uci set "network.${WG_IFACE}=interface"
    uci set "network.${WG_IFACE}.proto=wireguard"
    uci set "network.${WG_IFACE}.private_key=${iface_priv}"
    [ -n "$iface_mtu" ]  && uci set "network.${WG_IFACE}.mtu=${iface_mtu}"
    [ -n "$iface_port" ] && uci set "network.${WG_IFACE}.listen_port=${iface_port}"

    OLDIFS="$IFS"; IFS=','
    for a in $iface_addr; do
        a=$(printf '%s' "$a" | tr -d ' \t\r\n')
        [ -n "$a" ] && uci add_list "network.${WG_IFACE}.addresses=${a}"
    done
    IFS="$OLDIFS"

    if [ -n "$iface_dns" ]; then
        OLDIFS="$IFS"; IFS=','
        for d in $iface_dns; do
            d=$(printf '%s' "$d" | tr -d ' \t\r\n')
            [ -n "$d" ] && uci add_list "network.${WG_IFACE}.dns=${d}"
        done
        IFS="$OLDIFS"
    fi

    peer_section=$(uci add network "wireguard_${WG_IFACE}")
    uci set "network.${peer_section}.public_key=${peer_pub}"
    [ -n "$peer_psk" ]       && uci set "network.${peer_section}.preshared_key=${peer_psk}"
    uci set "network.${peer_section}.endpoint_host=${ep_host}"
    uci set "network.${peer_section}.endpoint_port=${ep_port}"
    [ -n "$peer_keepalive" ] && uci set "network.${peer_section}.persistent_keepalive=${peer_keepalive}"
    if [ -n "$peer_allowed" ]; then
        OLDIFS="$IFS"; IFS=','
        for a in $peer_allowed; do
            a=$(printf '%s' "$a" | tr -d ' \t\r\n')
            [ -n "$a" ] && uci add_list "network.${peer_section}.allowed_ips=${a}"
        done
        IFS="$OLDIFS"
        # Note: route_allowed_ips intentionally NOT set here.
        # With AllowedIPs=0.0.0.0/0 (full tunnel), letting OpenWrt auto-install
        # the default route via WG breaks RMS Remote Access and SSH management.
    fi

    uci commit network

    log "Restarting network service to apply WireGuard"
    /etc/init.d/network restart || warn "network restart returned non-zero"

    setup_lan_via_vpn "wgvpn"

    log "WireGuard provisioning complete (LAN-via-VPN policy active)."
}

main() {
    log "QualityNOC VPN provisioning starting (pid=$$)"

    LOCK="${STATE_DIR}/.lock"
    if [ -f "$LOCK" ] && kill -0 "$(cat "$LOCK")" 2>/dev/null; then
        log "Another provisioning run is in progress (pid $(cat "$LOCK")). Exiting."
        exit 0
    fi
    echo $$ > "$LOCK"
    trap 'rm -f "$LOCK"' EXIT INT TERM

    wait_for_network
    tag=$(read_tag)
    serial=$(get_serial)
    log "Tag='${tag}'  Serial='${serial}'"

    case "$tag" in
        wgvpn|wireguard)
            provision_wireguard "$serial"
            ;;
        ovpn|openvpn)
            provision_openvpn "$serial"
            ;;
        *)
            die "Unknown tag '${tag}' (expected 'wgvpn' or 'ovpn')"
            ;;
    esac

    date '+%Y-%m-%dT%H:%M:%S%z' > "${STATE_DIR}/last_provision_ok"
    log "Provisioning finished successfully."
}

main "$@"
