Files
DragonCoreSSH-NewWEB/update.sh
2026-05-15 17:01:24 -03:00

592 lines
19 KiB
Bash

#!/bin/bash
# Update script for DragonCoreSSH / SSH Panel.
# Pulls the newest source from Git, builds the new binary, and updates the
# installed files in place.
#
# Preserved:
# - /opt/sshpanel/.env
# - /opt/sshpanel/config.json
# - /opt/sshpanel/xray_config.json
# - SSH keys, certs, logs, database, users
#
# Usage:
# sudo bash /opt/sshpanel/update.sh
# sudo bash update.sh
#
# Optional:
# sudo UPDATE_REF=main bash /opt/sshpanel/update.sh
# sudo REPO_URL=https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git bash /opt/sshpanel/update.sh
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
info() { echo -e "${GREEN}[+]${NC} $*"; }
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
error() { echo -e "${RED}[x]${NC} $*"; exit 1; }
# Config
INSTALL_DIR="${INSTALL_DIR:-/opt/sshpanel}"
SERVICE_NAME="${SERVICE_NAME:-sshpanel}"
LOG_TMPFS_SIZE="${LOG_TMPFS_SIZE:-15m}"
PANEL_LOG_MAX_BYTES="${PANEL_LOG_MAX_BYTES:-1048576}"
REPO_URL="${REPO_URL:-https://git.dr2.site/penguinehis/DragonCoreSSH-NewWEB.git}"
UPDATE_REF="${UPDATE_REF:-}"
SOURCE_CACHE_DIR="${SOURCE_CACHE_DIR:-${INSTALL_DIR}/source}"
MKDIR_BIN="$(command -v mkdir 2>/dev/null || true)"
[[ -n "$MKDIR_BIN" ]] || MKDIR_BIN="/bin/mkdir"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_DIR=""
RESTART_NEEDED=false
[[ $EUID -ne 0 ]] && error "Run as root: sudo bash $0"
# Cross-distro helpers -------------------------------------------------------
PKG_MANAGER=""
UPDATE_DEPS=()
SYSTEMCTL_BIN=""
SH_BIN="$(command -v sh 2>/dev/null || echo /bin/sh)"
MOUNT_BIN="$(command -v mount 2>/dev/null || echo /bin/mount)"
MOUNTPOINT_BIN="$(command -v mountpoint 2>/dev/null || echo /usr/bin/mountpoint)"
TOUCH_BIN="$(command -v touch 2>/dev/null || echo /usr/bin/touch)"
CHMOD_BIN="$(command -v chmod 2>/dev/null || echo /usr/bin/chmod)"
require_systemd() {
SYSTEMCTL_BIN="$(command -v systemctl 2>/dev/null || true)"
if [[ -z "$SYSTEMCTL_BIN" ]]; then
error "systemd was not found. This updater supports Linux distributions that use systemd for services."
fi
}
detect_pkg_manager() {
if command -v apt-get >/dev/null 2>&1; then
PKG_MANAGER="apt"
elif command -v dnf >/dev/null 2>&1; then
PKG_MANAGER="dnf"
elif command -v yum >/dev/null 2>&1; then
PKG_MANAGER="yum"
elif command -v zypper >/dev/null 2>&1; then
PKG_MANAGER="zypper"
elif command -v pacman >/dev/null 2>&1; then
PKG_MANAGER="pacman"
elif command -v apk >/dev/null 2>&1; then
PKG_MANAGER="apk"
else
error "No supported package manager found. Supported: apt, dnf, yum, zypper, pacman, apk."
fi
}
set_update_deps() {
case "$PKG_MANAGER" in
apt)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
;;
dnf|yum)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
;;
zypper)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
;;
pacman)
UPDATE_DEPS=(git rsync wget ca-certificates python gcc make tar gzip)
;;
apk)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
;;
esac
}
pkg_update() {
case "$PKG_MANAGER" in
apt) apt-get update -qq ;;
dnf) dnf makecache -q ;;
yum) yum makecache -q ;;
zypper) zypper --non-interactive refresh ;;
pacman) pacman -Sy --noconfirm ;;
apk) apk update ;;
esac
}
pkg_install() {
case "$PKG_MANAGER" in
apt) DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "$@" ;;
dnf) dnf install -y "$@" ;;
yum) yum install -y "$@" ;;
zypper) zypper --non-interactive install -y "$@" ;;
pacman) pacman -S --noconfirm --needed "$@" ;;
apk) apk add --no-cache "$@" ;;
esac
}
ensure_update_dependencies() {
local missing=false cmd
for cmd in git rsync wget tar gzip gcc make; do
if ! command -v "$cmd" >/dev/null 2>&1; then
missing=true
fi
done
if ! command -v python3 >/dev/null 2>&1; then
missing=true
fi
if $missing; then
warn "One or more updater dependencies are missing. Installing them with $PKG_MANAGER..."
pkg_update
pkg_install "${UPDATE_DEPS[@]}"
fi
if ! command -v python3 >/dev/null 2>&1 && command -v python >/dev/null 2>&1; then
ln -sf "$(command -v python)" /usr/local/bin/python3 2>/dev/null || true
fi
}
echo -e "\n${GREEN}==========================================${NC}"
echo -e "${GREEN} DragonCoreSSH / SSH Panel Updater ${NC}"
echo -e "${GREEN}==========================================${NC}\n"
# Helpers
need_cmd() {
command -v "$1" >/dev/null 2>&1 || error "Required command not found: $1"
}
ensure_log_tmpfs_mount() {
local log_dir="${INSTALL_DIR}/logs"
local opts="rw,nosuid,nodev,noexec,noatime,nofail,size=${LOG_TMPFS_SIZE},mode=0755"
local tmp_fstab
mkdir -p "$log_dir"
if [[ -f /etc/fstab ]]; then
cp /etc/fstab "/etc/fstab.sshpanel.bak.$(date +%s)" 2>/dev/null || true
tmp_fstab="$(mktemp)"
awk -v mp="$log_dir" '!(($1 == "tmpfs") && ($2 == mp) && ($3 == "tmpfs")) {print}' /etc/fstab > "$tmp_fstab"
printf 'tmpfs %s tmpfs %s 0 0\n' "$log_dir" "$opts" >> "$tmp_fstab"
cat "$tmp_fstab" > /etc/fstab
rm -f "$tmp_fstab"
info " Log RAM disk automount saved in /etc/fstab: $log_dir (${LOG_TMPFS_SIZE})"
else
warn " /etc/fstab not found; service startup fallback will mount $log_dir as tmpfs"
fi
"${SYSTEMCTL_BIN:-systemctl}" daemon-reload >/dev/null 2>&1 || true
if command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$log_dir"; then
mount -o "remount,size=${LOG_TMPFS_SIZE},mode=0755" "$log_dir" >/dev/null 2>&1 || true
else
mount "$log_dir" >/dev/null 2>&1 || mount -t tmpfs -o "size=${LOG_TMPFS_SIZE},mode=0755" tmpfs "$log_dir" >/dev/null 2>&1 || \
warn " Could not mount $log_dir as tmpfs now; service startup fallback will try again"
fi
touch "$log_dir/panel.log" >/dev/null 2>&1 || true
chmod 0644 "$log_dir/panel.log" >/dev/null 2>&1 || true
}
install_git_if_missing() {
if command -v git >/dev/null 2>&1; then
return 0
fi
warn "git is not installed. Trying to install it..."
pkg_update
pkg_install git ca-certificates
}
remote_default_branch() {
local branch
branch="$(git ls-remote --symref "$REPO_URL" HEAD 2>/dev/null | awk '/^ref:/ {sub("refs/heads/", "", $2); print $2; exit}')"
if [[ -n "$branch" ]]; then
printf '%s\n' "$branch"
else
printf 'main\n'
fi
}
prepare_source_from_git() {
install_git_if_missing
if [[ -z "$UPDATE_REF" ]]; then
UPDATE_REF="$(remote_default_branch)"
fi
info "[1/7] Fetching latest files from Git..."
info " Repo : $REPO_URL"
info " Ref : $UPDATE_REF"
# If update.sh is being run from a real clone of this repo, update that folder.
# This is useful for developers who run the updater from the cloned project.
if [[ -d "$SCRIPT_DIR/.git" && -f "$SCRIPT_DIR/go.mod" ]]; then
SOURCE_DIR="$SCRIPT_DIR"
info " Updating existing source folder: $SOURCE_DIR"
git -C "$SOURCE_DIR" remote set-url origin "$REPO_URL" >/dev/null 2>&1 || true
git -C "$SOURCE_DIR" fetch --prune origin
git -C "$SOURCE_DIR" checkout "$UPDATE_REF" >/dev/null 2>&1 || true
git -C "$SOURCE_DIR" reset --hard "origin/$UPDATE_REF"
git -C "$SOURCE_DIR" clean -fd
return 0
fi
# Normal installed-server path: keep a local Git cache under /opt/sshpanel/source.
mkdir -p "$(dirname "$SOURCE_CACHE_DIR")"
if [[ -d "$SOURCE_CACHE_DIR/.git" ]]; then
SOURCE_DIR="$SOURCE_CACHE_DIR"
info " Updating cached source folder: $SOURCE_DIR"
git -C "$SOURCE_DIR" remote set-url origin "$REPO_URL" >/dev/null 2>&1 || true
git -C "$SOURCE_DIR" fetch --prune origin
git -C "$SOURCE_DIR" checkout "$UPDATE_REF" >/dev/null 2>&1 || true
git -C "$SOURCE_DIR" reset --hard "origin/$UPDATE_REF"
git -C "$SOURCE_DIR" clean -fd
else
rm -rf "$SOURCE_CACHE_DIR"
info " Cloning source folder to: $SOURCE_CACHE_DIR"
git clone --depth 1 --branch "$UPDATE_REF" "$REPO_URL" "$SOURCE_CACHE_DIR" || {
warn "Clone with ref '$UPDATE_REF' failed. Trying default clone..."
rm -rf "$SOURCE_CACHE_DIR"
git clone --depth 1 "$REPO_URL" "$SOURCE_CACHE_DIR"
}
SOURCE_DIR="$SOURCE_CACHE_DIR"
fi
[[ -f "$SOURCE_DIR/go.mod" ]] || error "Downloaded source is invalid: go.mod not found in $SOURCE_DIR"
[[ -d "$SOURCE_DIR/admin" ]] || error "Downloaded source is invalid: admin folder not found in $SOURCE_DIR"
}
install_go_if_needed() {
local go_version machine goarch go_url current_go need_go
go_version="$(awk '$1 == "go" {print $2; exit}' "$SOURCE_DIR/go.mod" 2>/dev/null || echo "1.22.5")"
need_go=true
info "[2/7] Checking Go toolchain..."
info " Required Go: $go_version"
if command -v go >/dev/null 2>&1; then
current_go="$(go version 2>/dev/null | awk '{print $3}' | sed 's/go//')"
if [[ "$(printf '%s\n' "$go_version" "$current_go" | sort -V | head -1)" == "$go_version" ]]; then
info " Go $current_go already installed."
need_go=false
fi
fi
if $need_go; then
machine="$(uname -m)"
case "$machine" in
x86_64) goarch="amd64" ;;
aarch64) goarch="arm64" ;;
armv7l) goarch="armv6l" ;;
*) goarch="amd64" ;;
esac
go_url="https://go.dev/dl/go${go_version}.linux-${goarch}.tar.gz"
info " Downloading Go ${go_version} (${goarch})..."
need_cmd wget
wget -q --show-progress -O /tmp/go.tar.gz "$go_url"
rm -rf /usr/local/go
tar -C /usr/local -xzf /tmp/go.tar.gz
rm -f /tmp/go.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' > /etc/profile.d/go.sh
chmod +x /etc/profile.d/go.sh
fi
export PATH=$PATH:/usr/local/go/bin
go version
}
build_binary() {
info "[3/7] Building new sshpanel binary..."
cd "$SOURCE_DIR"
export GOPATH=/tmp/gopath_sshpanel
export GOCACHE=/tmp/gocache_sshpanel
go mod download
go build -ldflags="-s -w" -o /tmp/sshpanel_new .
info " Build complete."
}
stop_service() {
info "[4/7] Stopping service..."
if "$SYSTEMCTL_BIN" is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
"$SYSTEMCTL_BIN" stop "$SERVICE_NAME"
RESTART_NEEDED=true
info " $SERVICE_NAME stopped."
else
RESTART_NEEDED=false
warn " $SERVICE_NAME was not running."
fi
}
copy_optional_script() {
local name mode
name="$1"
mode="$2"
if [[ -f "$SOURCE_DIR/$name" ]]; then
cp "$SOURCE_DIR/$name" "$INSTALL_DIR/$name"
chmod "$mode" "$INSTALL_DIR/$name"
info " Updated $name"
fi
}
apply_update() {
info "[5/7] Applying update..."
mkdir -p "$INSTALL_DIR/admin" "$INSTALL_DIR/logs" "$INSTALL_DIR/certs"
ensure_log_tmpfs_mount
if [[ -f "$INSTALL_DIR/sshpanel" ]]; then
cp "$INSTALL_DIR/sshpanel" "$INSTALL_DIR/sshpanel.bak"
info " Old binary backed up to $INSTALL_DIR/sshpanel.bak"
fi
mv /tmp/sshpanel_new "$INSTALL_DIR/sshpanel"
chmod 755 "$INSTALL_DIR/sshpanel"
info " Binary updated."
rsync -a --delete "$SOURCE_DIR/admin/" "$INSTALL_DIR/admin/"
info " Admin panel updated."
copy_optional_script "update.sh" 700
copy_optional_script "install.sh" 700
copy_optional_script "change_admin_password.sh" 700
# Keep a local copy of the latest source for easier support and future updates.
if [[ "$SOURCE_DIR" != "$SOURCE_CACHE_DIR" ]]; then
rm -rf "$SOURCE_CACHE_DIR"
mkdir -p "$SOURCE_CACHE_DIR"
rsync -a --delete --exclude '.git' "$SOURCE_DIR/" "$SOURCE_CACHE_DIR/"
info " Source files copied to $SOURCE_CACHE_DIR"
fi
[[ -f "$INSTALL_DIR/banner.txt" ]] || touch "$INSTALL_DIR/banner.txt"
}
patch_configs() {
info "[6/7] Patching config files without overwriting user settings..."
local cfg xcfg
cfg="$INSTALL_DIR/config.json"
xcfg="$INSTALL_DIR/xray_config.json"
if [[ -f "$cfg" ]]; then
python3 - "$cfg" <<'PYEOF'
import json, sys
path = sys.argv[1]
try:
with open(path) as f:
d = json.load(f)
except Exception as e:
print(f"[!] Could not parse {path}: {e}")
sys.exit(0)
changed = False
if 'banner_file' not in d:
d['banner_file'] = '/opt/sshpanel/banner.txt'
changed = True
if 'local_ssh_listen' in d:
d.pop('local_ssh_listen', None)
changed = True
if changed:
with open(path, 'w') as f:
json.dump(d, f, indent=2)
f.write('\n')
PYEOF
info " config.json checked."
fi
if [[ -f "$xcfg" ]] && grep -q '"geoip:private"' "$xcfg" 2>/dev/null; then
python3 - "$xcfg" <<'PYEOF'
import json, sys
path = sys.argv[1]
try:
with open(path) as f:
d = json.load(f)
except Exception as e:
print(f"[!] Could not parse {path}: {e}")
sys.exit(0)
routing = d.get('routing', {})
rules = routing.get('rules', [])
new_rules = [r for r in rules if 'geoip:private' not in r.get('ip', [])]
if new_rules != rules:
if new_rules:
d.setdefault('routing', {})['rules'] = new_rules
else:
d.pop('routing', None)
with open(path, 'w') as f:
json.dump(d, f, indent=2)
f.write('\n')
PYEOF
info " Removed geoip:private routing rule from xray_config.json"
fi
}
dnstt_redirect_is_enabled() {
# Updates must not resurrect this service when an admin intentionally
# disabled/removed it because it can break ip6tables on some machines.
local unit="sshpanel-dnstt-redirect.service"
if "$SYSTEMCTL_BIN" is-enabled --quiet "$unit" 2>/dev/null; then
return 0
fi
return 1
}
write_sshpanel_systemd_override() {
local include_dnstt_redirect="${1:-false}"
mkdir -p /etc/systemd/system/sshpanel.service.d
{
echo "[Unit]"
if [[ "$include_dnstt_redirect" == "true" ]]; then
echo "Wants=sshpanel-dnstt-redirect.service"
echo "After=local-fs.target sshpanel-dnstt-redirect.service"
else
echo "After=local-fs.target"
fi
echo
echo "[Service]"
echo "Environment=PANEL_LOG_FILE=${INSTALL_DIR}/logs/panel.log"
echo "Environment=PANEL_LOG_MAX_BYTES=${PANEL_LOG_MAX_BYTES}"
echo "ExecStartPre="
echo "ExecStartPre=${MKDIR_BIN} -p ${INSTALL_DIR}/logs"
echo "ExecStartPre=${SH_BIN} -c '${MOUNTPOINT_BIN} -q ${INSTALL_DIR}/logs || ${MOUNT_BIN} -t tmpfs -o size=${LOG_TMPFS_SIZE},mode=0755 tmpfs ${INSTALL_DIR}/logs || true'"
echo "ExecStartPre=${SH_BIN} -c '${TOUCH_BIN} ${INSTALL_DIR}/logs/panel.log && ${CHMOD_BIN} 0644 ${INSTALL_DIR}/logs/panel.log || true'"
echo "StandardOutput=journal"
echo "StandardError=journal"
} > /etc/systemd/system/sshpanel.service.d/override.conf
}
ensure_dnstt_redirect() {
if ! dnstt_redirect_is_enabled; then
warn " sshpanel-dnstt-redirect is disabled or removed; update will not recreate or enable it."
write_sshpanel_systemd_override false
"$SYSTEMCTL_BIN" daemon-reload
return 0
fi
info " Ensuring DNSTT DNS redirect service exists..."
cat > /usr/local/sbin/sshpanel-dnstt-redirect.sh <<'EOS'
#!/bin/bash
set -euo pipefail
DNS_UPSTREAM="${DNS_UPSTREAM:-1.1.1.1}"
DNSTT_PORT="${DNSTT_PORT:-5300}"
if command -v systemctl >/dev/null 2>&1; then
systemctl disable --now systemd-resolved.service >/dev/null 2>&1 || true
fi
rm -f /etc/resolv.conf
printf 'nameserver %s\n' "$DNS_UPSTREAM" > /etc/resolv.conf
if command -v ufw >/dev/null 2>&1; then
ufw allow 53/udp >/dev/null 2>&1 || true
fi
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
firewall-cmd --permanent --add-port=53/udp >/dev/null 2>&1 || true
firewall-cmd --reload >/dev/null 2>&1 || true
fi
add_iptables_rule() {
local bin="$1" chain="$2"
"$bin" -t nat -C "$chain" -p udp --dport 53 -j REDIRECT --to-ports "$DNSTT_PORT" 2>/dev/null \
|| "$bin" -t nat -A "$chain" -p udp --dport 53 -j REDIRECT --to-ports "$DNSTT_PORT"
}
if command -v iptables >/dev/null 2>&1; then
add_iptables_rule iptables PREROUTING
fi
if command -v ip6tables >/dev/null 2>&1; then
add_iptables_rule ip6tables PREROUTING || true
fi
if ! command -v iptables >/dev/null 2>&1 && command -v nft >/dev/null 2>&1; then
nft add table inet sshpanel_nat 2>/dev/null || true
nft 'add chain inet sshpanel_nat prerouting { type nat hook prerouting priority dstnat; policy accept; }' 2>/dev/null || true
nft list chain inet sshpanel_nat prerouting 2>/dev/null | grep -q "udp dport 53 redirect to :$DNSTT_PORT" \
|| nft add rule inet sshpanel_nat prerouting udp dport 53 redirect to :"$DNSTT_PORT"
fi
EOS
chmod +x /usr/local/sbin/sshpanel-dnstt-redirect.sh
cat > /etc/systemd/system/sshpanel-dnstt-redirect.service <<'EOF2'
[Unit]
Description=SSH Panel DNSTT DNS redirect (UDP 53 to 5300)
After=network.target
Before=sshpanel.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/sshpanel-dnstt-redirect.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF2
write_sshpanel_systemd_override true
"$SYSTEMCTL_BIN" daemon-reload
"$SYSTEMCTL_BIN" enable --now sshpanel-dnstt-redirect.service || warn "DNSTT redirect service failed. Check: journalctl -u sshpanel-dnstt-redirect -e"
}
restart_service() {
info "[7/7] Restarting service..."
ensure_dnstt_redirect
if $RESTART_NEEDED; then
info " Starting $SERVICE_NAME after update..."
else
warn " $SERVICE_NAME was not running before update; starting it now."
fi
"$SYSTEMCTL_BIN" start "$SERVICE_NAME"
sleep 2
if "$SYSTEMCTL_BIN" is-active --quiet "$SERVICE_NAME"; then
info " $SERVICE_NAME is running."
else
warn " $SERVICE_NAME failed to start. Check logs:"
warn " journalctl -u $SERVICE_NAME -n 50 --no-pager"
if [[ -f "$INSTALL_DIR/sshpanel.bak" ]]; then
warn " Restore command:"
warn " cp $INSTALL_DIR/sshpanel.bak $INSTALL_DIR/sshpanel && systemctl start $SERVICE_NAME"
fi
exit 1
fi
}
# Pre-flight
info "[0/7] Pre-flight checks..."
require_systemd
detect_pkg_manager
set_update_deps
ensure_update_dependencies
[[ -d "$INSTALL_DIR" ]] || error "Install dir $INSTALL_DIR not found. Run install.sh first."
[[ -f "$INSTALL_DIR/.env" ]] || error "$INSTALL_DIR/.env not found. Run install.sh first."
need_cmd python3
need_cmd rsync
need_cmd git
need_cmd wget
info " Install dir : $INSTALL_DIR"
info " Cache dir : $SOURCE_CACHE_DIR"
info " Package manager : $PKG_MANAGER"
info " Service manager : systemd"
prepare_source_from_git
install_go_if_needed
build_binary
stop_service
apply_update
patch_configs
restart_service
echo ""
echo -e "${GREEN}==========================================${NC}"
echo -e "${GREEN} Update complete! ${NC}"
echo -e "${GREEN}==========================================${NC}"
echo ""
echo -e " Updated from: ${YELLOW}${REPO_URL}${NC}"
echo -e " Source ref : ${YELLOW}${UPDATE_REF}${NC}"
echo -e " Source cache: ${YELLOW}${SOURCE_CACHE_DIR}${NC}"
echo -e " Logs : ${YELLOW}journalctl -u ${SERVICE_NAME} -f${NC}"
echo -e " ${YELLOW}tail -f ${INSTALL_DIR}/logs/panel.log${NC}"
echo -e " Backup : ${YELLOW}${INSTALL_DIR}/sshpanel.bak${NC}"
echo ""
echo -e "${YELLOW}Updated:${NC}"
echo -e " - sshpanel binary"
echo -e " - Admin panel"
echo -e " - update.sh / install.sh / helper scripts when available"
echo ""
echo -e "${YELLOW}Preserved:${NC}"
echo -e " - .env"
echo -e " - config.json"
echo -e " - xray_config.json"
echo -e " - SSH keys, certs, logs, database and users"
echo ""