#!/bin/bash # ============================================================ # st-clean — AutoscRIPT VPN bersih (Xray VMess/VLess/Trojan + SSH) # Versi: 1.1 | Lisensi: bebas digunakan & dimodifikasi # # CATATAN VERSI SIAPAN VULTR (audit 2026-10-03, Xray-core 26.9.30): # - Nginx yang terminasi TLS di 443; Xray WS polos (security none) # hanya listen di 127.0.0.1:10086-10088 (tidak terekspos publik). # - Routing nginx diperbaiki: /vmess→10086, /vless→10087, /trojan→10088. # - Akun Trojan sekarang memakai field "password" (bukan "id"). # - Field transport modern: method "websocket" (pengganti network "ws"). # - Menu CLI sekarang benar-benar memasang /usr/local/sbin/stclean. # - Domain bersifat WAJIB (tanpa sertifikat, nginx/Xray tidak jalan). # # Bedanya dengan script admin lainnya: # - TIDAK mengganti password root kamu # - TIDAK mematikan firewall (ufw/firewalld tetap utuh) # - TIDAK mengirim data VPS ke bot Telegram admin # - Notifikasi Telegram hanya saat BUAT AKUN BARU, # dan dikirim ke bot & chat milikmu sendiri # - Terkunci password instalasi (minta ke pemilik via DM), # tanpa auto-reboot harian # (boleh diaktifkan sendiri di cron kalau mau) # # Kebutuhan: Ubuntu 22.04 / Debian 11-12, dijalankan sebagai root, # sudah punya domain yang mengarah ke IP VPS ini (untuk SSL). # ============================================================ set -euo pipefail # ---------- Warna ---------- R='\033[0;31m'; G='\033[0;32m'; Y='\033[0;33m'; C='\033[0;36m'; N='\033[0m' info() { echo -e "${G}[INFO]${N} $*"; } warn() { echo -e "${Y}[WARN]${N} $*"; } err() { echo -e "${R}[ERROR]${N} $*"; } if [[ $EUID -ne 0 ]]; then err "Jalankan sebagai root: sudo bash $0" exit 1 fi # ============================================================ # GERBANG PASSWORD INSTALASI # Script ini terkunci oleh pemiliknya. Siapa pun yang memasang # wajib memasukkan password — minta ke pemilik lewat DM. # Yang tersimpan di bawah ini hanya HASH SHA-256 password, # password aslinya TIDAK tertulis di dalam script. # ============================================================ INSTALL_HASH="51cb992cf7a49f65b83d95c3739a3d14ff17e87e2384fde92b6107b1e5334f98" gate_ok=0 for attempt in 1 2 3; do printf "%b" "${Y}Script ini terkunci. Masukkan password instalasi (percobaan ${attempt}/3): ${N}" IFS= read -r -s input_pw < /dev/tty || input_pw="" printf "\n" input_hash="$(printf '%s' "$input_pw" | sha256sum | awk '{print $1}')" unset input_pw if [[ "$input_hash" == "$INSTALL_HASH" ]]; then gate_ok=1 break fi err "Password salah." done unset input_hash if [[ $gate_ok -ne 1 ]]; then err "Instalasi dibatalkan. Minta password ke pemilik script lewat DM." exit 1 fi info "Password benar. Instalasi dilanjutkan..." # ---------- Pengaturan ---------- BASE_DIR="/etc/stclean" # konfigurasi & database akun XRAY_DIR="/etc/xray" DB_DIR="$BASE_DIR/db" TG_CONF="$BASE_DIR/tg.conf" # berisi TG_BOT_TOKEN & TG_CHAT_ID mkdir -p "$XRAY_DIR" "$DB_DIR" "$DB_DIR/vmess" "$DB_DIR/vless" "$DB_DIR/trojan" "$DB_DIR/ssh" # ============================================================ # NOTIFIKASI TELEGRAM # ============================================================ load_tg() { if [[ -f $TG_CONF ]]; then # shellcheck source=/dev/null source "$TG_CONF" fi } tg_notify() { # tg_notify load_tg if [[ -z "${TG_BOT_TOKEN:-}" || -z "${TG_CHAT_ID:-}" ]]; then warn "Telegram belum dikonfigurasi — notifikasi dilewati." warn "Isi $TG_CONF lalu coba lagi." return 1 fi local TEXT TEXT="💬 Notifikasi ST-CLEAN $(printf '%s' "$1") ⏰ Waktu : $(date +'%d-%m-%Y %H:%M:%S') 🖥 Server : $(hostname) 🌐 IP : $(curl -s --max-time 5 https://api.ipify.org || echo 'tidak diketahui')" curl -s --max-time 10 \ -d "chat_id=${TG_CHAT_ID}" \ -d "text=${TEXT}" \ -d "parse_mode=html" \ "https://api.telegram.org/bot${TG_BOT_TOKEN}/sendMessage" >/dev/null } setup_tg() { echo "" info "Setup Notifikasi Telegram" echo " 1) Buat bot via @BotFather di Telegram → dapatkan TOKEN" echo " 2) Kirim /start ke bot-mu, lalu ambil chat_id via @userinfobot" echo "" read -rp "Masukkan BOT TOKEN : " token read -rp "Masukkan CHAT ID : " chatid if [[ -z $token || -z $chatid ]]; then err "Token/chat_id kosong — dilewati." return 1 fi cat > "$TG_CONF" </dev/null systemctl enable --now cron chrony vnstat >/dev/null 2>&1 || true info "Paket dasar terpasang (ufw & fail2ban tetap aktif)." } set_timezone() { timedatectl set-timezone Asia/Jakarta 2>/dev/null && info "Zona waktu: Asia/Jakarta" || warn "Gagal set timezone." } setup_domain() { echo "" info "Konfigurasi Domain" read -rp "Masukkan domain/subdomain (contoh: vpn.contoh.com): " DOMAIN if [[ -z $DOMAIN ]]; then err "Domain kosong — instalasi SSL dilewati. Xray akan tetap terpasang tapi tanpa TLS." DOMAIN="" return fi SERVER_IP=$(curl -s --max-time 8 https://api.ipify.org) DOMAIN_IP=$(dig +short "$DOMAIN" | tail -n1) if [[ "$SERVER_IP" != "$DOMAIN_IP" ]]; then warn "Domain '$DOMAIN' mengarah ke $DOMAIN_IP, sedangkan IP server $SERVER_IP." warn "Arahkan A record domain ke $SERVER_IP lalu ulangi instalasi, atau lanjut tanpa SSL." read -rp "Lanjut tanpa SSL? (y/n): " ok [[ $ok == y ]] && DOMAIN="" || exit 1 else info "Domain valid ($DOMAIN → $SERVER_IP)" fi } install_xray() { info "Menginstal Xray Core..." bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" \ @ install -u www-data >/dev/null mkdir -p /usr/local/share/xray wget -q -O /usr/local/share/xray/geosite.dat \ https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat wget -q -O /usr/local/share/xray/geoip.dat \ https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat info "Xray Core: $(/usr/local/bin/xray version | head -n1 | awk '{print $2}')" } install_acme() { [[ -z $DOMAIN ]] && return info "Memasang SSL Let's Encrypt untuk $DOMAIN ..." if [[ ! -x /root/.acme.sh/acme.sh ]]; then curl -s https://get.acme.sh | sh -s email=admin@"$DOMAIN" >/dev/null 2>&1 fi systemctl stop nginx 2>/dev/null || true /root/.acme.sh/acme.sh --set-default-ca --server letsencrypt /root/.acme.sh/acme.sh --issue -d "$DOMAIN" --standalone -k ec-256 >/dev/null 2>&1 /root/.acme.sh/acme.sh --installcert -d "$DOMAIN" \ --fullchainpath "$XRAY_DIR/xray.crt" \ --keypath "$XRAY_DIR/xray.key" \ --ecc >/dev/null 2>&1 chmod 644 "$XRAY_DIR/xray.crt" chmod 600 "$XRAY_DIR/xray.key" systemctl start nginx 2>/dev/null || true info "SSL terpasang: $XRAY_DIR/xray.crt" echo "$DOMAIN" > /root/domain } make_xray_config() { info "Membuat konfigurasi Xray..." local UUID_MAIN UUID_MAIN=$(cat "$XRAY_DIR/xray.crt" 2>/dev/null >/dev/null; uuidgen) # UUID dipakai sebagai contoh-konfigurasi; tiap akun punya UUID sendiri mkdir -p "$DB_DIR" cat > "$XRAY_DIR/config.json" < /etc/systemd/system/xray.service <<'EOF' [Unit] Description=Xray Service After=network.target nss-lookup.target [Service] User=www-data CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true ExecStart=/usr/local/bin/xray run -config /etc/xray/config.json Restart=on-failure LimitNPROC=10000 LimitNOFILE=1000000 [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now xray info "Xray berjalan di localhost (VMess :10086, VLess :10087, Trojan :10088 — WS polos, TLS diterminasi nginx di 443)." } install_menu() { info "Membuat menu utilitas di /usr/local/sbin..." # Pasang script utama sebagai CLI "stclean" (wrapper di bawah memanggilnya) install -m 755 "$(readlink -f "$0")" /usr/local/sbin/stclean local MENUS=(add-vmess add-vless add-trojan add-ssh list tg-setup) mkdir -p /usr/local/sbin for m in "${MENUS[@]}"; do # menu diambil dari fungsi CLI bawah cat > "/usr/local/sbin/$m" < /etc/nginx/sites-available/xray-ws </dev/null 2>&1 && systemctl enable --now nginx && systemctl reload nginx \ || warn "Konfigurasi nginx gagal — cek 'nginx -t'" info "Nginx siap di port 443." } setup_ufw() { info "Memastikan port yang dibutuhkan terbuka di UFW..." for p in 22 80 443; do ufw allow "$p"/tcp >/dev/null 2>&1 || true done ufw --force enable >/dev/null 2>&1 || true info "UFW aktif — hanya port 22/80/443 terbuka (Xray 10086-10088 khusus localhost)." } # ============================================================ # MANAJEMEN AKUN # ============================================================ add_client() { # add_client local proto=$1 user=$2 days=$3 uuid if [[ -z "${DOMAIN:-}" ]]; then if [[ -s /root/domain ]]; then DOMAIN=$(cat /root/domain); else err "Domain belum dikonfigurasi — jalankan instalasi dulu."; return 1; fi fi uuid=$(uuidgen) local expires expires=$(date -d "+${days} days" +'%Y-%m-%d') # Tambahkan client ke config.json # Trojan memakai field "password"; VMess/VLess memakai "id". local client_json if [[ $proto == trojan ]]; then client_json=$(jq -n --arg pw "$uuid" --arg email "${user}@stclean" \ '{password: $pw, email: $email}') else client_json=$(jq -n --arg id "$uuid" --arg email "${user}@stclean" \ '{id: $id, email: $email}') fi local tmp tmp=$(mktemp) jq --argjson client "$client_json" \ --arg tag "$proto-ws" \ '.inbounds |= map(if .tag == $tag then .settings.clients += [$client] else . end)' \ "$XRAY_DIR/config.json" > "$tmp" || { rm -f "$tmp"; err "Gagal memperbarui config.json"; return 1; } mv "$tmp" "$XRAY_DIR/config.json" # Simpan database akun cat > "$DB_DIR/$proto/$user" <AKUN BARU DIBUAT 👤 User : $user 🔐 Protokol : $proto (WS+TLS) 🌐 Domain : $DOMAIN 🆔 UUID : $uuid 🔌 Port : 443 📅 Aktif : $(date +'%d-%m-%Y') s/d $expires " || true echo "" echo "════════════════════════════════════════" echo " Akun : $user ($proto)" echo " UUID : $uuid" echo " Path : /$proto Port : 443 (TLS)" echo " Server : $DOMAIN" echo " Aktif sampai : $expires" echo "════════════════════════════════════════" } add_ssh_user() { local user=$1 pass=$2 days=$3 local expires expires=$(date -d "+${days} days" +'%Y-%m-%d') useradd -m -s /bin/bash -e "$expires" "$user" 2>/dev/null \ && echo "$user:$pass" | chpasswd \ || { err "User '$user' sudah ada atau gagal dibuat."; return 1; } echo "user=$user proto=ssh created=$(date +'%Y-%m-%d') expires=$expires ip_limit=0 bw_limit_gb=0 bw_used_bytes=0" > "$DB_DIR/ssh/$user" info "Akun SSH '$user' dibuat (berlaku sampai $expires)." tg_notify " ✅ AKUN SSH BARU 👤 User : $user 🔑 Password : $pass 🌐 Server : $(hostname -I | awk '{print $1}') 🔌 Port : 22 📅 Aktif s/d : $expires " || true echo "Akun SSH: $user / $pass (aktif sampai $expires)" } list_accounts() { info "Daftar Akun Terdaftar:" for proto in vmess vless trojan ssh; do echo "" echo "--- $proto ---" local found=0 for f in "$DB_DIR/$proto"/*; do [[ -f $f ]] || continue found=1 ip_limit=0; bw_limit_gb=0; bw_used_bytes=0 source "$f" echo " • $user (berlaku s/d $expires)" if [[ ${ip_limit:-0} != 0 || ${bw_limit_gb:-0} != 0 ]]; then echo " Limit IP: ${ip_limit:-0} • Bandwidth: ${bw_limit_gb:-0} GB (terpakai $(bytes_ke_teks "${bw_used_bytes:-0}"))" fi done [[ $found == 0 ]] && echo " (belum ada)" done } # ============================================================ # FITUR v1.1 — HAPUS/EDIT AKUN, KUOTA BANDWIDTH, KEDALUWARSA, # AUTO REBOOT + LOG, SPEED TEST, RESTART SERVICE, CEK PORT # ============================================================ LOG_REBOOT="$BASE_DIR/log-reboot.txt" LOG_AKUN="$BASE_DIR/log-akun.txt" ARB_CONF="$BASE_DIR/autoreboot.conf" CRON_KUOTA="/etc/cron.d/stclean-kuota" CRON_EXP="/etc/cron.d/stclean-kedaluwarsa" CRON_ARB="/etc/cron.d/stclean-autoreboot" db_set() { # db_set — tulis/ganti satu field di file db akun local f=$1 k=$2 v=$3 tmp tmp=$(mktemp) grep -v "^${k}=" "$f" > "$tmp" 2>/dev/null || true echo "${k}=${v}" >> "$tmp" mv "$tmp" "$f" } db_get() { # db_get local f=$1 k=$2 d=${3:-} val val=$(grep -oP "^${k}=\K.*" "$f" 2>/dev/null | tail -n1 || true) if [[ -n $val ]]; then echo "$val"; else echo "$d"; fi } bytes_ke_teks() { awk -v b="${1:-0}" 'BEGIN { if (b >= 1073741824) printf "%.2f GB", b/1073741824; else if (b >= 1048576) printf "%.2f MB", b/1048576; else if (b >= 1024) printf "%.2f KB", b/1024; else printf "%d B", b; }' } delete_client() { # delete_client [norestart] local proto=$1 user=$2 norestart=${3:-} if [[ $proto == ssh ]]; then userdel -r "$user" 2>/dev/null || userdel "$user" 2>/dev/null || true else local tmp tmp=$(mktemp) if jq --arg email "${user}@stclean" \ '.inbounds |= map(if .settings.clients then .settings.clients |= map(select(.email != $email)) else . end)' \ "$XRAY_DIR/config.json" > "$tmp"; then mv "$tmp" "$XRAY_DIR/config.json" else rm -f "$tmp" fi if [[ -z $norestart ]]; then systemctl restart xray 2>/dev/null || true; fi fi rm -f "$DB_DIR/$proto/$user" echo "$(date +'%d-%m-%Y %H:%M:%S') — akun $proto '$user' dihapus" >> "$LOG_AKUN" } pick_account() { # pick_account — UI ke stderr, username pilihan ke stdout (kosong = batal) local proto=$1 f users=() pilih exp for f in "$DB_DIR/$proto"/*; do [[ -f $f ]] || continue users+=("$(basename "$f")") done if [[ ${#users[@]} -eq 0 ]]; then warn "Belum ada akun $proto." >&2 return 1 fi echo "" >&2 echo "Pilih akun $proto:" >&2 local i for i in "${!users[@]}"; do exp=$(db_get "$DB_DIR/$proto/${users[$i]}" expires '-') echo " [$((i + 1))] ${users[$i]} (aktif s/d $exp)" >&2 done echo " [0] Kembali" >&2 printf "Pilih nomor: " >&2 IFS= read -r pilih < /dev/tty || pilih="0" if [[ $pilih =~ ^[0-9]+$ ]] && (( pilih >= 1 && pilih <= ${#users[@]} )); then echo "${users[$((pilih - 1))]}" fi } pilih_protokol() { # UI ke stderr, nama protokol ke stdout (kosong = batal) local p echo "" >&2 echo "Pilih protokol:" >&2 echo " [1] VMess" >&2 echo " [2] VLess" >&2 echo " [3] Trojan" >&2 echo " [4] SSH" >&2 echo " [0] Kembali" >&2 printf "Pilih nomor: " >&2 IFS= read -r p < /dev/tty || p="0" case $p in 1) echo vmess ;; 2) echo vless ;; 3) echo trojan ;; 4) echo ssh ;; *) echo "" ;; esac } menu_hapus() { local proto=$1 user yn user=$(pick_account "$proto") || return [[ -z $user ]] && return printf "Hapus akun %s '%s'? (y/n): " "$proto" "$user" IFS= read -r yn < /dev/tty || yn="n" if [[ $yn == y ]]; then delete_client "$proto" "$user" info "Akun $proto '$user' sudah dihapus." tg_notify " 🗑 AKUN DIHAPUS 👤 User : $user 🔐 Protokol : $proto " || true else info "Dibatalkan." fi } menu_edit() { local proto=$1 user f pilih exp baru tgl user=$(pick_account "$proto") || return [[ -z $user ]] && return f="$DB_DIR/$proto/$user" while true; do exp=$(db_get "$f" expires '-') echo "" echo "Edit akun $proto '$user':" echo " Masa aktif s/d : $exp" echo " Limit IP : $(db_get "$f" ip_limit 0) (0 = tanpa batas; tersimpan sebagai catatan)" echo " Limit bandwidth : $(db_get "$f" bw_limit_gb 0) GB (0 = tanpa batas)" echo " Bandwidth pakai : $(bytes_ke_teks "$(db_get "$f" bw_used_bytes 0)")" echo "" echo " [1] Ubah masa aktif (tambah hari)" echo " [2] Ubah limit IP" echo " [3] Ubah limit bandwidth" echo " [0] Kembali" printf "Pilih nomor: " IFS= read -r pilih < /dev/tty || pilih="0" case $pilih in 1) printf "Tambah berapa hari (dihitung dari tanggal aktif terakhir)? " IFS= read -r baru < /dev/tty || baru="" if [[ $baru =~ ^[0-9]+$ ]]; then tgl=$(date -d "$exp +$baru days" +'%Y-%m-%d' 2>/dev/null || date -d "+$baru days" +'%Y-%m-%d') db_set "$f" expires "$tgl" if [[ $proto == ssh ]]; then chage -E "$tgl" "$user" 2>/dev/null || true; fi info "Masa aktif '$user' sekarang s/d $tgl." else err "Isian harus berupa angka hari." fi ;; 2) printf "Limit IP baru (0 = tanpa batas): " IFS= read -r baru < /dev/tty || baru="" if [[ $baru =~ ^[0-9]+$ ]]; then db_set "$f" ip_limit "$baru" info "Limit IP '$user' tersimpan: $baru." else err "Isian harus berupa angka." fi ;; 3) printf "Limit bandwidth baru dalam GB (0 = tanpa batas): " IFS= read -r baru < /dev/tty || baru="" if [[ $baru =~ ^[0-9]+$ ]]; then db_set "$f" bw_limit_gb "$baru" info "Limit bandwidth '$user' tersimpan: $baru GB." else err "Isian harus berupa angka." fi ;; 0) break ;; *) warn "Pilihan tidak dikenal: $pilih" ;; esac done } cek_kedaluwarsa() { # Dipanggil cron harian: hapus akun Xray/SSH yang masa aktifnya sudah lewat local proto f user exp today changed=0 today=$(date +'%Y-%m-%d') for proto in vmess vless trojan ssh; do for f in "$DB_DIR/$proto"/*; do [[ -f $f ]] || continue user=$(basename "$f") exp=$(db_get "$f" expires '9999-12-31') if [[ $exp < $today ]]; then delete_client "$proto" "$user" norestart changed=1 tg_notify " ⌛ AKUN KEDALUWARSA DIHAPUS 👤 User : $user 🔐 Protokol : $proto 📅 Kedaluwarsa : $exp " || true fi done done if [[ $changed == 1 ]]; then systemctl restart xray 2>/dev/null || true; fi } cek_kuota() { # Dipanggil cron tiap 10 menit: kumpulkan pemakaian bandwidth dari # statistik Xray, matikan akun yang melewati limit bandwidth-nya command -v xray >/dev/null 2>&1 || return 0 local out proto f user email lim used up down total batas changed=0 out=$(xray api statsquery --server=127.0.0.1:10085 -pattern 'user>>>' -reset 2>/dev/null || true) [[ -z $out ]] && return 0 for proto in vmess vless trojan; do for f in "$DB_DIR/$proto"/*; do [[ -f $f ]] || continue user=$(basename "$f") email="${user}@stclean" up=$(echo "$out" | jq -r --arg n "user>>>${email}>>>traffic>>>uplink" '[.stat[]? | select(.name == $n) | .value | tonumber] | add // 0' 2>/dev/null || echo 0) down=$(echo "$out" | jq -r --arg n "user>>>${email}>>>traffic>>>downlink" '[.stat[]? | select(.name == $n) | .value | tonumber] | add // 0' 2>/dev/null || echo 0) [[ $up == 0 && $down == 0 ]] && continue used=$(db_get "$f" bw_used_bytes 0) total=$((used + up + down)) db_set "$f" bw_used_bytes "$total" lim=$(db_get "$f" bw_limit_gb 0) if [[ $lim =~ ^[0-9]+$ ]] && (( lim > 0 )); then batas=$((lim * 1073741824)) if (( total >= batas )); then delete_client "$proto" "$user" norestart changed=1 tg_notify " ⛔ AKUN DIMATIKAN — KUOTA BANDWIDTH HABIS 👤 User : $user 🔐 Protokol : $proto 📊 Terpakai : $(bytes_ke_teks "$total") dari $lim GB " || true fi fi done done if [[ $changed == 1 ]]; then systemctl restart xray 2>/dev/null || true; fi } speed_test() { echo "" info "Speed Test VPS — tunggu sebentar..." if command -v speedtest-cli >/dev/null 2>&1; then speedtest-cli --simple 2>/dev/null || err "Speed test gagal dijalankan." else warn "speedtest-cli belum terpasang — memakai uji unduh cepat." ping -c 3 -q 1.1.1.1 2>/dev/null | tail -n1 || true local spd spd=$(curl -o /dev/null -s -w '%{speed_download}' --max-time 30 'http://cachefly.cachefly.net/10mb.test' || echo 0) awk -v b="$spd" 'BEGIN { printf "Download : %.2f MB/s (%.1f Mbps)\n", b/1048576, b*8/1000000 }' fi } restart_layanan() { local svc=$1 nama=$2 st systemctl restart "$svc" 2>/dev/null || true st=$(systemctl is-active "$svc" 2>/dev/null || true) if [[ $st == active ]]; then info "$nama berhasil di-restart (aktif)." else err "$nama GAGAL di-restart — periksa: systemctl status $svc" fi } menu_restart() { local pilih sshsvc=ssh if systemctl list-unit-files 2>/dev/null | grep -q '^sshd\.service'; then sshsvc=sshd; fi while true; do echo "" echo "Restart Service:" echo " [1] Xray" echo " [2] Nginx" echo " [3] SSH" echo " [4] Semua (Xray + Nginx + SSH)" echo " [0] Kembali" printf "Pilih nomor: " IFS= read -r pilih < /dev/tty || pilih="0" case $pilih in 1) restart_layanan xray "Xray" ;; 2) restart_layanan nginx "Nginx" ;; 3) restart_layanan "$sshsvc" "SSH" ;; 4) restart_layanan xray "Xray"; restart_layanan nginx "Nginx"; restart_layanan "$sshsvc" "SSH" ;; 0) break ;; *) warn "Pilihan tidak dikenal: $pilih" ;; esac done } cek_port() { local p state echo "" info "Port penting untuk tunneling:" for p in 22 80 443 10085 10086 10087 10088; do if ss -tln 2>/dev/null | grep -q ":$p "; then state="${G}TERBUKA${N}"; else state="${R}tertutup${N}"; fi printf " Port %-6s : %b\n" "$p" "$state" done echo "" info "Semua port yang sedang listen:" ss -tulnp 2>/dev/null | awk 'NR==1 || /LISTEN|UNCONN/' || true } arb_status() { if [[ -f $ARB_CONF ]]; then db_get "$ARB_CONF" mode off; else echo off; fi } arb_teks() { case $(arb_status) in 6h) echo "Aktif — tiap 6 jam" ;; 12h) echo "Aktif — tiap 12 jam" ;; 24h) echo "Aktif — tiap 1 hari (03:00)" ;; *) echo "Nonaktif" ;; esac } set_auto_reboot() { local mode=$1 label=$2 cronline="" case $mode in 6h) cronline='0 */6 * * *' ;; 12h) cronline='0 */12 * * *' ;; 24h) cronline='0 3 * * *' ;; esac if [[ $mode == off ]]; then rm -f "$CRON_ARB" "$ARB_CONF" info "Auto reboot DIMATIKAN." return fi echo "mode=$mode" > "$ARB_CONF" echo "$cronline root /usr/local/sbin/stclean auto-reboot-run >/dev/null 2>&1" > "$CRON_ARB" chmod 644 "$CRON_ARB" info "Auto reboot AKTIF: $label." } auto_reboot_run() { # Dipanggil cron sesuai jadwal yang dipilih di menu Auto Reboot echo "$(date +'%d-%m-%Y %H:%M:%S') — AUTO-REBOOT terjadwal dijalankan (jadwal: $(arb_status))" >> "$LOG_REBOOT" sync sleep 5 /sbin/reboot 2>/dev/null || reboot 2>/dev/null || systemctl reboot 2>/dev/null || true } boot_log() { # Dipanggil service stclean-bootlog setiap server menyala echo "$(date +'%d-%m-%Y %H:%M:%S') — BOOT: server menyala" >> "$LOG_REBOOT" } menu_auto_reboot() { local pilih proto while true; do echo "" echo "Pengaturan Auto Reboot — status saat ini: $(arb_teks)" echo " [1] Reboot tiap 6 jam" echo " [2] Reboot tiap 12 jam" echo " [3] Reboot tiap 1 hari (pukul 03:00)" echo " [4] Matikan auto reboot" echo " [5] Lihat log reboot" echo " [0] Kembali" printf "Pilih nomor: " IFS= read -r pilih < /dev/tty || pilih="0" case $pilih in 1) set_auto_reboot 6h "tiap 6 jam" ;; 2) set_auto_reboot 12h "tiap 12 jam" ;; 3) set_auto_reboot 24h "tiap 1 hari (pukul 03:00)" ;; 4) set_auto_reboot off "" ;; 5) echo "" if [[ -s $LOG_REBOOT ]]; then tail -n 20 "$LOG_REBOOT"; else echo "(log reboot masih kosong)"; fi ;; 0) break ;; *) warn "Pilihan tidak dikenal: $pilih" ;; esac done } setup_fitur_tambahan() { info "Memasang fitur tambahan (cek kuota, cek kedaluwarsa, log boot)..." echo '*/10 * * * * root /usr/local/sbin/stclean cek-kuota >/dev/null 2>&1' > "$CRON_KUOTA" echo '5 0 * * * root /usr/local/sbin/stclean cek-kedaluwarsa >/dev/null 2>&1' > "$CRON_EXP" chmod 644 "$CRON_KUOTA" "$CRON_EXP" cat > /etc/systemd/system/stclean-bootlog.service <<'EOF' [Unit] Description=ST-Clean boot log After=network.target [Service] Type=oneshot ExecStart=/usr/local/sbin/stclean boot-log [Install] WantedBy=multi-user.target EOF systemctl daemon-reload 2>/dev/null || true systemctl enable stclean-bootlog.service >/dev/null 2>&1 || true info "Fitur tambahan aktif: cek kuota tiap 10 menit, cek kedaluwarsa tiap hari, log reboot." } # ============================================================ # TAMPILAN MENU INTERAKTIF — NEAR CLOUD # ============================================================ P='\033[1;35m'; W='\033[1;37m'; DM='\033[0;90m' count_accounts() { local d="$DB_DIR/$1" n=0 f if [[ -d $d ]]; then for f in "$d"/*; do [[ -f $f ]] && n=$((n + 1)); done fi echo "$n" } menu_header() { local host os up ram disk ip dom xs ns xcol ncol host=$(hostname 2>/dev/null || echo '-') os=$(grep -oP 'PRETTY_NAME="\K[^"]+' /etc/os-release 2>/dev/null || true); [[ -z $os ]] && os='Linux' up=$(uptime -p 2>/dev/null | sed 's/^up //' || true); [[ -z $up ]] && up='-' ram=$(free -m 2>/dev/null | awk '/^Mem:/ {printf "%s/%s MB", $3, $2}' || true); [[ -z $ram ]] && ram='-' disk=$(df -h / 2>/dev/null | awk 'NR==2 {printf "%s/%s (%s)", $3, $2, $5}' || true); [[ -z $disk ]] && disk='-' ip=$(curl -s --max-time 4 https://api.ipify.org 2>/dev/null || true); [[ -z $ip ]] && ip='tidak diketahui' if [[ -s /root/domain ]]; then dom=$(cat /root/domain); else dom='(belum diset)'; fi xs=$(systemctl is-active xray 2>/dev/null || true) ns=$(systemctl is-active nginx 2>/dev/null || true) if [[ $xs == active ]]; then xcol="${G}● Aktif${N}"; else xcol="${R}● Mati${N}"; fi if [[ $ns == active ]]; then ncol="${G}● Aktif${N}"; else ncol="${R}● Mati${N}"; fi echo -e "${P}╔══════════════════════════════════════════════════════════════╗${N}" echo -e "${P}║${N}${C} N E A R C L O U D ${N}${P}║${N}" echo -e "${P}║${N}${DM} Panel Script Tunneling — ST-CLEAN ${N}${P}║${N}" echo -e "${P}╠══════════════════════════════════════════════════════════════╣${N}" echo -e "${P}║${N} ${W}INFO VPS${N} ${P}║${N}" printf "${P}║${N} Hostname : %-47s ${P}║${N}\n" "$host" printf "${P}║${N} IP Publik : %-47s ${P}║${N}\n" "$ip" printf "${P}║${N} OS : %-47s ${P}║${N}\n" "$os" printf "${P}║${N} Uptime : %-47s ${P}║${N}\n" "$up" printf "${P}║${N} RAM : %-22s Disk : %-19s ${P}║${N}\n" "$ram" "$disk" printf "${P}║${N} Domain : %-47s ${P}║${N}\n" "$dom" echo -e "${P}║${N} Xray : $xcol Nginx : $ncol ${P}║${N}" printf "${P}║${N} Akun : VMess %s • VLess %s • Trojan %s • SSH %s%-12s ${P}║${N}\n" \ "$(count_accounts vmess)" "$(count_accounts vless)" "$(count_accounts trojan)" "$(count_accounts ssh)" "" printf "${P}║${N} Auto Reboot: %-47s ${P}║${N}\n" "$(arb_teks)" echo -e "${P}╠══════════════════════════════════════════════════════════════╣${N}" echo -e "${P}║${N} ${W}MENU UTAMA${N} ${P}║${N}" echo -e "${P}║${N} [1] Buat Akun VMess ${P}║${N}" echo -e "${P}║${N} [2] Buat Akun VLess ${P}║${N}" echo -e "${P}║${N} [3] Buat Akun Trojan ${P}║${N}" echo -e "${P}║${N} [4] Buat Akun SSH ${P}║${N}" echo -e "${P}║${N} [5] Daftar Semua Akun ${P}║${N}" echo -e "${P}║${N} [6] Hapus Akun ${P}║${N}" echo -e "${P}║${N} [7] Edit Akun (Masa Aktif / Limit) ${P}║${N}" echo -e "${P}║${N} [8] Setup Notifikasi Telegram ${P}║${N}" echo -e "${P}║${N} [9] Speed Test VPS ${P}║${N}" echo -e "${P}║${N} [10] Restart Service ${P}║${N}" echo -e "${P}║${N} [11] Cek Port Aktif ${P}║${N}" echo -e "${P}║${N} [12] Auto Reboot ${P}║${N}" echo -e "${P}║${N} [13] Pasang / Pasang Ulang Script ${P}║${N}" echo -e "${P}║${N} [0] Keluar ${P}║${N}" echo -e "${P}╠══════════════════════════════════════════════════════════════╣${N}" echo -e "${P}║${N}${DM} © 2026 Near Cloud — All Rights Reserved ${N}${P}║${N}" echo -e "${P}║${N}${DM} Script terkunci • Minta akses ke pemilik lewat DM ${N}${P}║${N}" echo -e "${P}╚══════════════════════════════════════════════════════════════╝${N}" } pause_enter() { echo "" read -rp "Tekan Enter untuk kembali ke menu..." _ < /dev/tty || true } menu_buat() { local proto=$1 nm hr echo "" read -rp "Nama akun : " nm < /dev/tty || nm="" read -rp "Masa aktif (hari) : " hr < /dev/tty || hr="" if [[ -z $nm || -z $hr ]]; then err "Nama/masa aktif kosong — dibatalkan." return fi add_client "$proto" "$nm" "$hr" || true } menu_buat_ssh() { local nm pw hr echo "" read -rp "Nama akun : " nm < /dev/tty || nm="" read -rp "Password SSH : " pw < /dev/tty || pw="" read -rp "Masa aktif (hari) : " hr < /dev/tty || hr="" if [[ -z $nm || -z $pw || -z $hr ]]; then err "Ada isian kosong — dibatalkan." return fi add_ssh_user "$nm" "$pw" "$hr" || true } menu_utama() { local pilih while true; do clear 2>/dev/null || true menu_header printf "Pilih nomor [0-13]: " IFS= read -r pilih < /dev/tty || pilih="0" case $pilih in 1) menu_buat vmess ;; 2) menu_buat vless ;; 3) menu_buat trojan ;; 4) menu_buat_ssh ;; 5) echo ""; list_accounts ;; 6) proto=$(pilih_protokol); if [[ -n $proto ]]; then menu_hapus "$proto"; fi ;; 7) proto=$(pilih_protokol); if [[ -n $proto ]]; then menu_edit "$proto"; fi ;; 8) setup_tg || true ;; 9) speed_test ;; 10) menu_restart ;; 11) cek_port ;; 12) menu_auto_reboot ;; 13) install_all ;; 0) echo ""; info "Sampai jumpa! — Near Cloud"; break ;; *) warn "Pilihan tidak dikenal: $pilih" ;; esac if [[ $pilih != 0 ]]; then pause_enter; fi done } # ============================================================ # CLI # ============================================================ usage() { cat <<'EOF' stclean — autoscRIPT VPN bersih Penggunaan: stclean Buka menu interaktif Near Cloud stclean add-vmess Buat akun VMess baru stclean add-vless Buat akun VLess baru stclean add-trojan Buat akun Trojan baru stclean add-ssh Buat akun SSH baru stclean list Daftar semua akun stclean tg-setup Konfigurasi ulang Telegram Contoh: stclean add-vmess budi 30 stclean add-ssh budi rahasia123 30 EOF exit 0 } install_all() { echo "" echo "═══════════════════════════════════════════════" echo " ST-CLEAN — AutoscRIPT VPN Bersih v1.1" echo " Xray (VMess/VLess/Trojan WS+TLS) + SSH" echo "═══════════════════════════════════════════════" echo "" base_packages set_timezone setup_domain if [[ -z $DOMAIN ]]; then err "Domain wajib diisi dan mengarah ke IP server ini — nginx/Xray butuh sertifikat TLS. Instalasi dibatalkan." exit 1 fi install_xray install_acme make_xray_config setup_nginx_ws "$DOMAIN" setup_ufw install_menu setup_fitur_tambahan echo "" info "✅ Instalasi selesai!" echo "" echo " Langkah berikutnya:" echo " 1. Konfigurasi Telegram bot kamu : tg-setup" echo " 2. Buat akun pertama : add-vmess 30" echo " 3. Lihat semua akun : list" echo "" } # ============================================================ # MAIN # ============================================================ case "${1:-}" in add-vmess) [[ $# -lt 3 ]] && usage; add_client vmess "$2" "$3" ;; add-vless) [[ $# -lt 3 ]] && usage; add_client vless "$2" "$3" ;; add-trojan) [[ $# -lt 3 ]] && usage; add_client trojan "$2" "$3" ;; add-ssh) [[ $# -lt 4 ]] && usage; add_ssh_user "$2" "$3" "$4" ;; list|list-akun) list_accounts ;; tg-setup) setup_tg ;; cek-kuota) cek_kuota ;; cek-kedaluwarsa) cek_kedaluwarsa ;; auto-reboot-run) auto_reboot_run ;; boot-log) boot_log ;; -h|--help|help) usage ;; "") if [[ -e /dev/tty ]]; then menu_utama; else usage; fi ;; install) install_all ;; *) usage ;; esac