#!/bin/bash
# Meshvirt Networking: start/stop/status for meshvirtd on DSM.
#
# Kernel networking (TUN meshvirt0) is used when /dev/net/tun exists and
# meshvirtd has CAP_NET_ADMIN (granted by "meshvirt configure-host", run as a
# root boot task). Otherwise meshvirtd runs with userspace networking plus a
# SOCKS5 / HTTP proxy on localhost, and the web UI offers "Enable full
# networking". The chosen mode is recorded in networking-mode for the UI.
#
# If an enrollment code from the install wizard is present, "meshvirt up"
# runs in the background once meshvirtd answers, and the code is deleted
# after it succeeds or is rejected (invalid, expired, already used). Network
# errors are retried a few times, then the code is kept for the next start.
# Start never waits for it.
# Meshvirt Networking: shared helpers for the DSM package scripts.
# Inlined into each script at packaging time in place of its "@include common"
# line (release/dist/synology/meshvirt_package.go); POSIX sh only.

PKGNAME="MeshvirtNetworking"
DEFAULT_SERVER_URL="https://mesh.meshvirt.io"

# meshvirt_pkgvar prints the directory holding state, config and logs.
meshvirt_pkgvar() {
    if [ "${SYNOPKG_DSM_VERSION_MAJOR:-7}" = "6" ]; then
        echo "/var/packages/${PKGNAME}/etc"
    elif [ -n "${SYNOPKG_PKGVAR}" ]; then
        echo "${SYNOPKG_PKGVAR}"
    else
        echo "/var/packages/${PKGNAME}/var"
    fi
}

# meshvirt_quote prints $1 single-quoted for a shell/env file.
meshvirt_quote() {
    printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
}

# meshvirt_valid_url succeeds if $1 is https://host[:port][/path].
meshvirt_valid_url() {
    printf '%s' "$1" | grep -Eq '^https://[A-Za-z0-9.-]+(:[0-9]{1,5})?(/[^[:space:]?#]*)?$'
}

# meshvirt_valid_hostname succeeds if $1 is a DNS label.
meshvirt_valid_hostname() {
    printf '%s' "$1" | grep -Eq '^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?$'
}

# meshvirt_valid_code succeeds if $1 looks like a pre-auth key (8-512
# characters). The length is checked separately: bounds above RE_DUP_MAX
# (255) are rejected by BSD grep.
meshvirt_valid_code() {
    [ ${#1} -ge 8 ] && [ ${#1} -le 512 ] && printf '%s' "$1" | grep -Eq '^[A-Za-z0-9_.:+=/-]+$'
}

# meshvirt_has_net_caps DAEMON succeeds if DAEMON carries cap_net_admin.
# Uses getcap when DSM has it; otherwise the marker "meshvirt configure-host"
# writes after setcap, valid only while it is newer than the binary (an
# upgrade replaces the binary and drops its capabilities).
meshvirt_has_net_caps() {
    for g in /bin/getcap /usr/bin/getcap /sbin/getcap /usr/sbin/getcap; do
        if [ -x "$g" ]; then
            "$g" "$1" 2>/dev/null | grep -q "cap_net_admin"
            return $?
        fi
    done
    marker="$(meshvirt_pkgvar)/net-caps"
    [ -f "${marker}" ] && [ "${marker}" -nt "$1" ]
}

# meshvirt_log appends a line to the package log.
meshvirt_log() {
    echo "$(date '+%Y-%m-%dT%H:%M:%S%z') $*" >>"$(meshvirt_pkgvar)/meshvirt-package.log" 2>/dev/null
}

# meshvirt_notify shows $1 in DSM's result dialog (if DSM gave us a log
# file), the package log and stderr, without failing the operation.
meshvirt_notify() {
    [ -n "${SYNOPKG_TEMP_LOGFILE}" ] && echo "$1" >>"${SYNOPKG_TEMP_LOGFILE}"
    meshvirt_log "$1"
    echo "$1" >&2
}

# meshvirt_fail reports $1 to the DSM wizard and exits non-zero.
meshvirt_fail() {
    [ -n "${SYNOPKG_TEMP_LOGFILE}" ] && echo "$1" >"${SYNOPKG_TEMP_LOGFILE}"
    echo "$1" >&2
    exit 1
}

SERVICE_NAME="meshvirt"
PKGVAR="$(meshvirt_pkgvar)"
PKGDEST="${SYNOPKG_PKGDEST:-/var/packages/${PKGNAME}/target}"
DAEMON="${PKGDEST}/bin/meshvirtd"
CLI="${PKGDEST}/bin/meshvirt"

PID_FILE="${PKGVAR}/meshvirtd.pid"
LOG_FILE="${PKGVAR}/meshvirtd.stdout.log"
STATE_FILE="${PKGVAR}/meshvirtd.state"
SOCKET_FILE="${PKGVAR}/meshvirtd.sock"
CONFIG_FILE="${PKGVAR}/config.env"
ENROLL_KEY="${PKGVAR}/enroll.key"
ENROLL_STATUS="${PKGVAR}/enroll.status"
MODE_FILE="${PKGVAR}/networking-mode"
UPGRADED_FILE="${PKGVAR}/upgraded"
PORT="41642"
PROXY_ADDR="localhost:1055"

# has_net_caps succeeds if meshvirtd carries cap_net_admin (file caps; see
# meshvirt_has_net_caps in common). If neither getcap nor the configure-host
# marker can tell, stay on the safe (userspace) side.
has_net_caps() {
    meshvirt_has_net_caps "${DAEMON}"
}

# choose_mode sets MODE and REASON.
choose_mode() {
    MODE="userspace"
    REASON=""
    if [ "${SYNOPKG_DSM_VERSION_MAJOR}" = "6" ]; then
        # DSM 6 runs packages as root; ensure_tun_created prepares the device.
        if [ -c /dev/net/tun ]; then MODE="kernel"; else REASON="no-tun"; fi
        return
    fi
    if [ ! -c /dev/net/tun ]; then
        REASON="no-tun"
    elif ! has_net_caps; then
        if [ -f "${UPGRADED_FILE}" ]; then REASON="upgraded"; else REASON="no-caps"; fi
    else
        MODE="kernel"
        rm -f "${UPGRADED_FILE}"
    fi
}

build_command() {
    SERVICE_COMMAND="${DAEMON} --state=${STATE_FILE} --socket=${SOCKET_FILE} --port=${PORT}"
    if [ "${MODE}" = "kernel" ]; then
        SERVICE_COMMAND="${SERVICE_COMMAND} --tun=meshvirt0"
    else
        SERVICE_COMMAND="${SERVICE_COMMAND} --tun=userspace-networking --socks5-server=${PROXY_ADDR} --outbound-http-proxy-listen=${PROXY_ADDR}"
    fi
}

write_mode() {
    umask 022
    printf 'MODE=%s\nREASON=%s\n' "${MODE}" "${REASON}" >"${MODE_FILE}.tmp" && mv -f "${MODE_FILE}.tmp" "${MODE_FILE}"
}

# config_value KEY prints KEY's value from config.env (single-quoted values).
config_value() {
    sed -n "s/^$1=//p" "${CONFIG_FILE}" 2>/dev/null | tail -n 1 | sed "s/^'//; s/'\$//; s/'\\\\''/'/g"
}

# code_rejected succeeds if "meshvirt up" output $1 says the control server
# refused the key itself (retrying cannot help; codes are single-use and
# expire after 10 minutes).
code_rejected() {
    printf '%s' "$1" | grep -Eiq 'invalid key|invalid auth ?key|auth ?key (not found|expired|has expired|already used|is not valid)|key (has )?expired|already (been )?used|not valid|unauthori[sz]ed'
}

# enroll_in_background uses the wizard's enrollment code once, without
# blocking start. The code is deleted after "meshvirt up" succeeds or the
# server rejects it; on network errors it is kept for the next start.
enroll_in_background() {
    [ -s "${ENROLL_KEY}" ] || return 0
    url="$(config_value MESHVIRT_SERVER_URL)"
    meshvirt_valid_url "${url}" || url="${DEFAULT_SERVER_URL}"
    host="$(config_value MESHVIRT_HOSTNAME)"
    # One enrollment attempt at a time (stale lock from a crash: >5 min old).
    lock="${PKGVAR}/enroll.lock"
    find "${lock}" -maxdepth 0 -mmin +5 -exec rmdir {} \; 2>/dev/null
    mkdir "${lock}" 2>/dev/null || return 0
    (
        trap 'rmdir "${lock}" 2>/dev/null' EXIT
        # Wait up to 60s for meshvirtd to answer on its socket (a socket
        # file alone may be left over from a crash).
        i=0
        until "${CLI}" --socket="${SOCKET_FILE}" status --json >/dev/null 2>&1; do
            [ $i -ge 60 ] && break
            sleep 1
            i=$((i + 1))
        done
        set -- --socket="${SOCKET_FILE}" up --reset --login-server="${url}" \
            --auth-key="file:${ENROLL_KEY}" --timeout=120s
        [ -n "${host}" ] && set -- "$@" --hostname="${host}"
        # Up to 3 attempts, 20s apart, while the code is still fresh.
        n=1
        while :; do
            if out="$("${CLI}" "$@" 2>&1)"; then
                rm -f "${ENROLL_KEY}"
                printf 'STATUS=ok\n' >"${ENROLL_STATUS}"
                meshvirt_log "enroll: device enrolled with ${url}; enrollment code deleted"
                break
            fi
            err="$(printf '%s' "${out}" | tail -n 1 | tr -d "'\n" | cut -c1-300)"
            if code_rejected "${out}"; then
                rm -f "${ENROLL_KEY}"
                printf "STATUS=failed\nERROR='%s'\n" "${err}" >"${ENROLL_STATUS}"
                meshvirt_log "enroll: code rejected and deleted (get a new one from https://enroll.meshvirt.io or sign in with Meshvirt SSO): ${err}"
                break
            fi
            if [ $n -ge 3 ]; then
                printf "STATUS=failed\nERROR='%s'\n" "${err}" >"${ENROLL_STATUS}"
                meshvirt_log "enroll: failed (code kept for the next start): ${err}"
                break
            fi
            n=$((n + 1))
            sleep 20
        done
    ) </dev/null >/dev/null 2>&1 &
}

start_daemon() {
    choose_mode
    build_command
    write_mode
    ts=$(date '+%Y-%m-%dT%H:%M:%S%z')
    # meshvirtd is not running (checked by the caller), so a socket file
    # here is stale; remove it so the enrollment wait cannot mistake it.
    rm -f "${SOCKET_FILE}"
    echo "${ts} Starting ${SERVICE_NAME} (${MODE}${REASON:+, $REASON}) with: ${SERVICE_COMMAND}" >"${LOG_FILE}"
    # Run in its own subshell so "jobs -p" names the pipeline's group leader.
    STATE_DIRECTORY=${PKGVAR} ${SERVICE_COMMAND} 2>&1 </dev/null | sed -u '1,200p;201s,.*,[further meshvirtd logs suppressed],p;d' >>"${LOG_FILE}" &
    # We pipe meshvirtd's output to sed, so "$!" is sed's PID, not meshvirtd's.
    # Use jobs -p to retrieve the PID of the most recent process group leader.
    jobs -p >"${PID_FILE}"
    enroll_in_background
}

stop_daemon() {
    if [ -r "${PID_FILE}" ]; then
        PID=$(cat "${PID_FILE}")
        ts=$(date '+%Y-%m-%dT%H:%M:%S%z')
        echo "${ts} Stopping ${SERVICE_NAME} service PID=${PID}" >>"${LOG_FILE}"
        kill -TERM $PID >>"${LOG_FILE}" 2>&1
        wait_for_status 1 || kill -KILL $PID >>"${LOG_FILE}" 2>&1
        rm -f "${PID_FILE}" >/dev/null
    fi
}

daemon_status() {
    if [ -r "${PID_FILE}" ]; then
        PID=$(cat "${PID_FILE}")
        if [ -n "${PID}" ] && kill -0 "${PID}" 2>/dev/null; then
            return 0
        fi
        rm -f "${PID_FILE}" >/dev/null
    fi
    return 1
}

wait_for_status() {
    # 20 tries, one second apart.
    counter=20
    while [ ${counter} -gt 0 ]; do
        daemon_status
        [ $? -eq $1 ] && return 0
        counter=$((counter - 1))
        sleep 1
    done
    return 1
}

ensure_tun_created() {
    if [ "${SYNOPKG_DSM_VERSION_MAJOR}" != "6" ]; then
        # DSM 7 runs the package unprivileged; "meshvirt configure-host"
        # (root boot task) creates /dev/net/tun and sets capabilities.
        return
    fi
    # Create the necessary file structure for /dev/net/tun
    if [ ! -c /dev/net/tun ]; then
        [ -d /dev/net ] || mkdir -m 755 /dev/net
        mknod /dev/net/tun c 10 200
        chmod 0755 /dev/net/tun
    fi
    # Load the tun module if not already loaded
    if ! lsmod | grep -q "^tun\s"; then
        insmod /lib/modules/tun.ko
    fi
}

if [ "${SYNOPKG_DSM_VERSION_MAJOR}" = "6" ]; then
    chown -R meshvirt:meshvirt "${PKGVAR}/"
fi

case $1 in
start)
    if daemon_status; then
        exit 0
    fi
    ensure_tun_created
    start_daemon
    exit 0
    ;;
stop)
    if daemon_status; then
        stop_daemon
        exit $?
    fi
    exit 0
    ;;
status)
    if daemon_status; then
        echo "${SERVICE_NAME} is running"
        exit 0
    fi
    echo "${SERVICE_NAME} is not running"
    exit 3
    ;;
log)
    echo "${LOG_FILE}"
    exit 0
    ;;
*)
    echo "command $1 is not implemented"
    exit 0
    ;;
esac
