#!/usr/bin/env bash set -e while [[ $# -gt 0 ]]; do key="$1" case $key in install|update|uninstall|up|down|restart|status|logs|core-update|install-script|uninstall-script|edit) COMMAND="$1" shift # past argument ;; --name) if [[ "$COMMAND" == "install" || "$COMMAND" == "install-script" ]]; then APP_NAME="$2" shift # past argument else echo "Error: --name parameter is only allowed with 'install' or 'install-script' commands." exit 1 fi shift # past value ;; *) shift # past unknown argument ;; esac done # Determine the local address without contacting the internet: ask the routing # table which source address would be used, then fall back to the first # configured global address. An external echo service would abort the script # under `set -e` on any host that cannot reach it. NODE_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}') if [ -z "$NODE_IP" ]; then NODE_IP=$(hostname -I 2>/dev/null | awk '{print $1}') fi if [ -z "$NODE_IP" ] && command -v ip >/dev/null 2>&1; then NODE_IP=$(ip -4 -o addr show scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1) fi if [[ "$COMMAND" == "install" || "$COMMAND" == "install-script" ]] && [ -z "$APP_NAME" ]; then APP_NAME="marzban-node" fi # Set script name if APP_NAME is not set if [ -z "$APP_NAME" ]; then SCRIPT_NAME=$(basename "$0") APP_NAME="${SCRIPT_NAME%.*}" fi INSTALL_DIR="/opt" if [ -d "$INSTALL_DIR/$APP_NAME" ]; then APP_DIR="$INSTALL_DIR/$APP_NAME" elif [ -d "$INSTALL_DIR/Marzban-node" ]; then APP_DIR="$INSTALL_DIR/Marzban-node" else APP_DIR="$INSTALL_DIR/$APP_NAME" fi DATA_DIR="/var/lib/$APP_NAME" DATA_MAIN_DIR="/var/lib/$APP_NAME" COMPOSE_FILE="$APP_DIR/docker-compose.yml" LAST_XRAY_CORES=5 CERT_FILE="$DATA_DIR/cert.pem" FETCH_REPO="Gozargah/Marzban-scripts" SCRIPT_URL="https://git.nimbaha.org/marzban/github/$FETCH_REPO/raw/master/marzban-node.sh" colorized_echo() { local color=$1 local text=$2 local style=${3:-0} # Default style is normal case $color in "red") printf "\e[${style};91m${text}\e[0m\n" ;; "green") printf "\e[${style};92m${text}\e[0m\n" ;; "yellow") printf "\e[${style};93m${text}\e[0m\n" ;; "blue") printf "\e[${style};94m${text}\e[0m\n" ;; "magenta") printf "\e[${style};95m${text}\e[0m\n" ;; "cyan") printf "\e[${style};96m${text}\e[0m\n" ;; *) echo "${text}" ;; esac } check_running_as_root() { if [ "$(id -u)" != "0" ]; then colorized_echo red "This command must be run as root." exit 1 fi } detect_os() { # Detect the operating system if [ -f /etc/lsb-release ]; then OS=$(lsb_release -si) elif [ -f /etc/os-release ]; then OS=$(awk -F= '/^NAME/{print $2}' /etc/os-release | tr -d '"') elif [ -f /etc/redhat-release ]; then OS=$(cat /etc/redhat-release | awk '{print $1}') elif [ -f /etc/arch-release ]; then OS="Arch" else colorized_echo red "Unsupported operating system" exit 1 fi } detect_and_update_package_manager() { colorized_echo blue "Updating package manager" if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then PKG_MANAGER="apt-get" $PKG_MANAGER update -qq >/dev/null 2>&1 || true elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then PKG_MANAGER="yum" $PKG_MANAGER update -y -q >/dev/null 2>&1 || true $PKG_MANAGER install -y -q epel-release >/dev/null 2>&1 || true elif [[ "$OS" == "Fedora"* ]]; then PKG_MANAGER="dnf" $PKG_MANAGER update -q -y >/dev/null 2>&1 || true elif [[ "$OS" == "Arch"* ]]; then PKG_MANAGER="pacman" $PKG_MANAGER -Sy --noconfirm --quiet >/dev/null 2>&1 || true elif [[ "$OS" == "openSUSE"* ]]; then PKG_MANAGER="zypper" $PKG_MANAGER refresh --quiet >/dev/null 2>&1 || true else colorized_echo red "Unsupported operating system" exit 1 fi } detect_compose() { # Check if docker compose command exists if docker compose >/dev/null 2>&1; then COMPOSE='docker compose' elif docker-compose >/dev/null 2>&1; then COMPOSE='docker-compose' else colorized_echo red "docker compose not found" exit 1 fi } _install_package_impl () { if [ -z "$PKG_MANAGER" ]; then detect_and_update_package_manager fi PACKAGE=$1 colorized_echo blue "Installing $PACKAGE" if [[ "$OS" == "Ubuntu"* ]] || [[ "$OS" == "Debian"* ]]; then $PKG_MANAGER -y -qq install "$PACKAGE" >/dev/null 2>&1 elif [[ "$OS" == "CentOS"* ]] || [[ "$OS" == "AlmaLinux"* ]]; then $PKG_MANAGER install -y -q "$PACKAGE" >/dev/null 2>&1 elif [[ "$OS" == "Fedora"* ]]; then $PKG_MANAGER install -y -q "$PACKAGE" >/dev/null 2>&1 elif [[ "$OS" == "Arch"* ]]; then $PKG_MANAGER -S --noconfirm --quiet "$PACKAGE" >/dev/null 2>&1 elif [[ "$OS" == "openSUSE"* ]]; then PKG_MANAGER="zypper" $PKG_MANAGER --quiet install -y "$PACKAGE" >/dev/null 2>&1 else colorized_echo red "Unsupported operating system" exit 1 fi } install_docker() { # Install Docker and Docker Compose using the official installation script colorized_echo blue "Installing Docker" # Overridden by the offline installer appended at the end of this file. colorized_echo red "get.docker.com is not used; see install_docker below." return 1 colorized_echo green "Docker installed successfully" } install_marzban_node_script() { colorized_echo blue "Installing marzban script" TARGET_PATH="/usr/local/bin/$APP_NAME" curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 $SCRIPT_URL -o $TARGET_PATH sed -i "s/^APP_NAME=.*/APP_NAME=\"$APP_NAME\"/" $TARGET_PATH chmod 755 $TARGET_PATH colorized_echo green "Marzban-node script installed successfully at $TARGET_PATH" } # Get a list of occupied ports get_occupied_ports() { if command -v ss &>/dev/null; then OCCUPIED_PORTS=$(ss -tuln | awk '{print $5}' | grep -Eo '[0-9]+$' | sort | uniq) elif command -v netstat &>/dev/null; then OCCUPIED_PORTS=$(netstat -tuln | awk '{print $4}' | grep -Eo '[0-9]+$' | sort | uniq) else colorized_echo yellow "Neither ss nor netstat found. Attempting to install net-tools." detect_os install_package net-tools if command -v netstat &>/dev/null; then OCCUPIED_PORTS=$(netstat -tuln | awk '{print $4}' | grep -Eo '[0-9]+$' | sort | uniq) else colorized_echo red "Failed to install net-tools. Please install it manually." exit 1 fi fi } # Function to check if a port is occupied is_port_occupied() { if echo "$OCCUPIED_PORTS" | grep -q -w "$1"; then return 0 else return 1 fi } install_marzban_node() { # Fetch releases mkdir -p "$DATA_DIR" mkdir -p "$APP_DIR" mkdir -p "$DATA_MAIN_DIR" # Проверка на существование файла перед его очисткой if [ -f "$CERT_FILE" ]; then >"$CERT_FILE" fi # Function to print information to the user print_info() { echo -e "\033[1;34m$1\033[0m" } # Prompt the user to input the certificate echo -e "Please paste the content of the Client Certificate, press ENTER on a new line when finished: " while IFS= read -r line; do if [[ -z $line ]]; then break fi echo "$line" >>"$CERT_FILE" done print_info "Certificate saved to $CERT_FILE" # Prompt the user to choose REST or another protocol read -p "Do you want to use REST protocol? (Y/n): " -r use_rest # Default to "Y" if the user just presses ENTER if [[ -z "$use_rest" || "$use_rest" =~ ^[Yy]$ ]]; then USE_REST=true else USE_REST=false fi get_occupied_ports # Prompt the user to enter ports with occupation check while true; do read -p "Enter the SERVICE_PORT (default 62050): " -r SERVICE_PORT if [[ -z "$SERVICE_PORT" ]]; then SERVICE_PORT=62050 fi if [[ "$SERVICE_PORT" -ge 1 && "$SERVICE_PORT" -le 65535 ]]; then if is_port_occupied "$SERVICE_PORT"; then colorized_echo red "Port $SERVICE_PORT is already in use. Please enter another port." else break fi else colorized_echo red "Invalid port. Please enter a port between 1 and 65535." fi done while true; do read -p "Enter the XRAY_API_PORT (default 62051): " -r XRAY_API_PORT if [[ -z "$XRAY_API_PORT" ]]; then XRAY_API_PORT=62051 fi if [[ "$XRAY_API_PORT" -ge 1 && "$XRAY_API_PORT" -le 65535 ]]; then if is_port_occupied "$XRAY_API_PORT"; then colorized_echo red "Port $XRAY_API_PORT is already in use. Please enter another port." elif [[ "$XRAY_API_PORT" -eq "$SERVICE_PORT" ]]; then colorized_echo red "Port $XRAY_API_PORT cannot be the same as SERVICE_PORT. Please enter another port." else break fi else colorized_echo red "Invalid port. Please enter a port between 1 and 65535." fi done colorized_echo blue "Generating compose file" # Write content to the file cat > "$COMPOSE_FILE" <> "$COMPOSE_FILE" <> "$COMPOSE_FILE" </dev/null 2>&1; then colorized_echo yellow "Image $image removed" fi done fi } uninstall_marzban_node_data_files() { if [ -d "$DATA_DIR" ]; then colorized_echo yellow "Removing directory: $DATA_DIR" rm -r "$DATA_DIR" fi } up_marzban_node() { $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" up -d --remove-orphans } down_marzban_node() { $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" down } show_marzban_node_logs() { $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs } follow_marzban_node_logs() { $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" logs -f } update_marzban_node_script() { colorized_echo blue "Updating marzban-node script" curl -sSL $SCRIPT_URL | install -m 755 /dev/stdin /usr/local/bin/$APP_NAME colorized_echo green "marzban-node script updated successfully" } update_marzban_node() { $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" pull } is_marzban_node_installed() { if [ -d $APP_DIR ]; then return 0 else return 1 fi } is_marzban_node_up() { if [ -z "$($COMPOSE -f $COMPOSE_FILE ps -q -a)" ]; then return 1 else return 0 fi } install_command() { check_running_as_root # Check if marzban is already installed if is_marzban_node_installed; then colorized_echo red "Marzban-node is already installed at $APP_DIR" read -p "Do you want to override the previous installation? (y/n) " if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo red "Aborted installation" exit 1 fi fi detect_os if ! command -v jq >/dev/null 2>&1; then install_package jq fi if ! command -v curl >/dev/null 2>&1; then install_package curl fi if ! command -v docker >/dev/null 2>&1; then install_docker fi detect_compose install_marzban_node_script install_marzban_node up_marzban_node follow_marzban_node_logs echo "Use your IP: $NODE_IP and defaults ports: $SERVICE_PORT and $XRAY_API_PORT to setup your Marzban Main Panel" } uninstall_command() { check_running_as_root # Check if marzban is installed if ! is_marzban_node_installed; then colorized_echo red "Marzban-node not installed!" exit 1 fi read -p "Do you really want to uninstall Marzban-node? (y/n) " if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo red "Aborted" exit 1 fi detect_compose if is_marzban_node_up; then down_marzban_node fi uninstall_marzban_node_script uninstall_marzban_node uninstall_marzban_node_docker_images read -p "Do you want to remove Marzban-node data files too ($DATA_DIR)? (y/n) " if [[ ! $REPLY =~ ^[Yy]$ ]]; then colorized_echo green "Marzban-node uninstalled successfully" else uninstall_marzban_node_data_files colorized_echo green "Marzban-node uninstalled successfully" fi } up_command() { help() { colorized_echo red "Usage: marzban-node up [options]" echo "" echo "OPTIONS:" echo " -h, --help display this help message" echo " -n, --no-logs do not follow logs after starting" } local no_logs=false while [[ "$#" -gt 0 ]]; do case "$1" in -n|--no-logs) no_logs=true ;; -h|--help) help exit 0 ;; *) echo "Error: Invalid option: $1" >&2 help exit 0 ;; esac shift done # Check if marzban-node is installed if ! is_marzban_node_installed; then colorized_echo red "Marzban-node's not installed!" exit 1 fi detect_compose if is_marzban_node_up; then colorized_echo red "Marzban-node's already up" exit 1 fi up_marzban_node if [ "$no_logs" = false ]; then follow_marzban_node_logs fi } down_command() { # Check if marzban-node is installed if ! is_marzban_node_installed; then colorized_echo red "Marzban-node not installed!" exit 1 fi detect_compose if ! is_marzban_node_up; then colorized_echo red "Marzban-node already down" exit 1 fi down_marzban_node } restart_command() { help() { colorized_echo red "Usage: marzban-node restart [options]" echo echo "OPTIONS:" echo " -h, --help display this help message" echo " -n, --no-logs do not follow logs after starting" } local no_logs=false while [[ "$#" -gt 0 ]]; do case "$1" in -n|--no-logs) no_logs=true ;; -h|--help) help exit 0 ;; *) echo "Error: Invalid option: $1" >&2 help exit 0 ;; esac shift done # Check if marzban-node is installed if ! is_marzban_node_installed; then colorized_echo red "Marzban-node not installed!" exit 1 fi detect_compose down_marzban_node up_marzban_node } status_command() { # Check if marzban-node is installed if ! is_marzban_node_installed; then echo -n "Status: " colorized_echo red "Not Installed" exit 1 fi detect_compose if ! is_marzban_node_up; then echo -n "Status: " colorized_echo blue "Down" exit 1 fi echo -n "Status: " colorized_echo green "Up" json=$($COMPOSE -f $COMPOSE_FILE ps -a --format=json) services=$(echo "$json" | jq -r 'if type == "array" then .[] else . end | .Service') states=$(echo "$json" | jq -r 'if type == "array" then .[] else . end | .State') # Print out the service names and statuses for i in $(seq 0 $(expr $(echo $services | wc -w) - 1)); do service=$(echo $services | cut -d' ' -f $(expr $i + 1)) state=$(echo $states | cut -d' ' -f $(expr $i + 1)) echo -n "- $service: " if [ "$state" == "running" ]; then colorized_echo green $state else colorized_echo red $state fi done } logs_command() { help() { colorized_echo red "Usage: marzban-node logs [options]" echo "" echo "OPTIONS:" echo " -h, --help display this help message" echo " -n, --no-follow do not show follow logs" } local no_follow=false while [[ "$#" -gt 0 ]]; do case "$1" in -n|--no-follow) no_follow=true ;; -h|--help) help exit 0 ;; *) echo "Error: Invalid option: $1" >&2 help exit 0 ;; esac shift done # Check if marzban is installed if ! is_marzban_node_installed; then colorized_echo red "Marzban-node's not installed!" exit 1 fi detect_compose if ! is_marzban_node_up; then colorized_echo red "Marzban-node is not up." exit 1 fi if [ "$no_follow" = true ]; then show_marzban_node_logs else follow_marzban_node_logs fi } update_command() { check_running_as_root # Check if marzban is installed if ! is_marzban_node_installed; then colorized_echo red "Marzban-node not installed!" exit 1 fi detect_compose update_marzban_node_script colorized_echo blue "Pulling latest version" update_marzban_node colorized_echo blue "Restarting Marzban-node services" down_marzban_node up_marzban_node colorized_echo blue "Marzban-node updated successfully" } identify_the_operating_system_and_architecture() { if [[ "$(uname)" == 'Linux' ]]; then case "$(uname -m)" in 'i386' | 'i686') ARCH='32' ;; 'amd64' | 'x86_64') ARCH='64' ;; 'armv5tel') ARCH='arm32-v5' ;; 'armv6l') ARCH='arm32-v6' grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' ;; 'armv7' | 'armv7l') ARCH='arm32-v7a' grep Features /proc/cpuinfo | grep -qw 'vfp' || ARCH='arm32-v5' ;; 'armv8' | 'aarch64') ARCH='arm64-v8a' ;; 'mips') ARCH='mips32' ;; 'mipsle') ARCH='mips32le' ;; 'mips64') ARCH='mips64' lscpu | grep -q "Little Endian" && ARCH='mips64le' ;; 'mips64le') ARCH='mips64le' ;; 'ppc64') ARCH='ppc64' ;; 'ppc64le') ARCH='ppc64le' ;; 'riscv64') ARCH='riscv64' ;; 's390x') ARCH='s390x' ;; *) echo "error: The architecture is not supported." exit 1 ;; esac else echo "error: This operating system is not supported." exit 1 fi } # Function to update the Xray core get_xray_core() { identify_the_operating_system_and_architecture clear validate_version() { local version="$1" local response=$(curl -s "https://git.nimbaha.org/marzban/api/xray-tags/$version") if echo "$response" | grep -q '"message": "Not Found"'; then echo "invalid" else echo "valid" fi } print_menu() { clear echo -e "\033[1;32m==============================\033[0m" echo -e "\033[1;32m Xray-core Installer \033[0m" echo -e "\033[1;32m==============================\033[0m" current_version=$(get_current_xray_core_version) echo -e "\033[1;33m>>>> Current Xray-core version: \033[1;1m$current_version\033[0m" echo -e "\033[1;32m==============================\033[0m" echo -e "\033[1;33mAvailable Xray-core versions:\033[0m" for ((i=0; i<${#versions[@]}; i++)); do echo -e "\033[1;34m$((i + 1)):\033[0m ${versions[i]}" done echo -e "\033[1;32m==============================\033[0m" echo -e "\033[1;35mM:\033[0m Enter a version manually" echo -e "\033[1;31mQ:\033[0m Quit" echo -e "\033[1;32m==============================\033[0m" } latest_releases=$(curl -s "https://git.nimbaha.org/marzban/api/repos/XTLS/Xray-core/releases?per_page=$LAST_XRAY_CORES") versions=($(echo "$latest_releases" | grep -oP '"tag_name": "\K(.*?)(?=")')) while true; do print_menu read -p "Choose a version to install (1-${#versions[@]}), or press M to enter manually, Q to quit: " choice if [[ "$choice" =~ ^[1-9][0-9]*$ ]] && [ "$choice" -le "${#versions[@]}" ]; then choice=$((choice - 1)) selected_version=${versions[choice]} break elif [ "$choice" == "M" ] || [ "$choice" == "m" ]; then while true; do read -p "Enter the version manually (e.g., v1.2.3): " custom_version if [ "$(validate_version "$custom_version")" == "valid" ]; then selected_version="$custom_version" break 2 else echo -e "\033[1;31mInvalid version or version does not exist. Please try again.\033[0m" fi done elif [ "$choice" == "Q" ] || [ "$choice" == "q" ]; then echo -e "\033[1;31mExiting.\033[0m" exit 0 else echo -e "\033[1;31mInvalid choice. Please try again.\033[0m" sleep 2 fi done echo -e "\033[1;32mSelected version $selected_version for installation.\033[0m" if ! dpkg -s unzip >/dev/null 2>&1; then echo -e "\033[1;33mInstalling required packages...\033[0m" detect_os install_package unzip fi mkdir -p $DATA_MAIN_DIR/xray-core cd $DATA_MAIN_DIR/xray-core xray_filename="Xray-linux-$ARCH.zip" xray_download_url="https://git.nimbaha.org/marzban/github/XTLS/Xray-core/releases/download/${selected_version}/${xray_filename}" echo -e "\033[1;33mDownloading Xray-core version ${selected_version} in the background...\033[0m" curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "${xray_download_url}" -o "${xray_filename}" & wait echo -e "\033[1;33mExtracting Xray-core in the background...\033[0m" unzip -o "${xray_filename}" >/dev/null 2>&1 & wait rm "${xray_filename}" } get_current_xray_core_version() { XRAY_BINARY="$DATA_MAIN_DIR/xray-core/xray" if [ -f "$XRAY_BINARY" ]; then version_output=$("$XRAY_BINARY" -version 2>/dev/null) if [ $? -eq 0 ]; then version=$(echo "$version_output" | head -n1 | awk '{print $2}') echo "$version" return fi fi # If local binary is not found or failed, check in the Docker container CONTAINER_NAME="$APP_NAME" if docker ps --format '{{.Names}}' | grep -q "^$CONTAINER_NAME$"; then version_output=$(docker exec "$CONTAINER_NAME" xray -version 2>/dev/null) if [ $? -eq 0 ]; then # Extract the version number from the first line version=$(echo "$version_output" | head -n1 | awk '{print $2}') echo "$version (in container)" return fi fi echo "Not installed" } install_yq() { if command -v yq &>/dev/null; then colorized_echo green "yq is already installed." return fi identify_the_operating_system_and_architecture local base_url="https://git.nimbaha.org/marzban/github/mikefarah/yq/releases/latest/download" local yq_binary="" case "$ARCH" in '64' | 'x86_64') yq_binary="yq_linux_amd64" ;; 'arm32-v7a' | 'arm32-v6' | 'arm32-v5' | 'armv7l') yq_binary="yq_linux_arm" ;; 'arm64-v8a' | 'aarch64') yq_binary="yq_linux_arm64" ;; '32' | 'i386' | 'i686') yq_binary="yq_linux_386" ;; *) colorized_echo red "Unsupported architecture: $ARCH" exit 1 ;; esac local yq_url="${base_url}/${yq_binary}" colorized_echo blue "Downloading yq from ${yq_url}..." if ! command -v curl &>/dev/null && ! command -v wget &>/dev/null; then colorized_echo yellow "Neither curl nor wget is installed. Attempting to install curl." install_package curl || { colorized_echo red "Failed to install curl. Please install curl or wget manually." exit 1 } fi if command -v curl &>/dev/null; then if curl -L "$yq_url" -o /usr/local/bin/yq; then chmod +x /usr/local/bin/yq colorized_echo green "yq installed successfully!" else colorized_echo red "Failed to download yq using curl. Please check your internet connection." exit 1 fi elif command -v wget &>/dev/null; then if wget -O /usr/local/bin/yq "$yq_url"; then chmod +x /usr/local/bin/yq colorized_echo green "yq installed successfully!" else colorized_echo red "Failed to download yq using wget. Please check your internet connection." exit 1 fi fi if ! echo "$PATH" | grep -q "/usr/local/bin"; then export PATH="/usr/local/bin:$PATH" fi hash -r if command -v yq &>/dev/null; then colorized_echo green "yq is ready to use." elif [ -x "/usr/local/bin/yq" ]; then colorized_echo yellow "yq is installed at /usr/local/bin/yq but not found in PATH." colorized_echo yellow "You can add /usr/local/bin to your PATH environment variable." else colorized_echo red "yq installation failed. Please try again or install manually." exit 1 fi } update_core_command() { check_running_as_root get_xray_core if ! command -v yq &>/dev/null; then echo "yq is not installed. Installing yq..." install_yq fi if ! grep -q 'XRAY_EXECUTABLE_PATH: "/var/lib/marzban-node/xray-core/xray"' "$COMPOSE_FILE"; then yq eval '.services."marzban-node".environment.XRAY_EXECUTABLE_PATH = "/var/lib/marzban-node/xray-core/xray"' -i "$COMPOSE_FILE" fi if ! yq eval ".services.\"marzban-node\".volumes[] | select(. == \"${DATA_MAIN_DIR}:/var/lib/marzban-node\")" "$COMPOSE_FILE" &>/dev/null; then yq eval ".services.\"marzban-node\".volumes += \"${DATA_MAIN_DIR}:/var/lib/marzban-node\"" -i "$COMPOSE_FILE" fi # Restart Marzban-node colorized_echo red "Restarting Marzban-node..." $APP_NAME restart -n colorized_echo blue "Installation of XRAY-CORE version $selected_version completed." } check_editor() { if [ -z "$EDITOR" ]; then if command -v nano >/dev/null 2>&1; then EDITOR="nano" elif command -v vi >/dev/null 2>&1; then EDITOR="vi" else detect_os install_package nano EDITOR="nano" fi fi } edit_command() { detect_os check_editor if [ -f "$COMPOSE_FILE" ]; then $EDITOR "$COMPOSE_FILE" else colorized_echo red "Compose file not found at $COMPOSE_FILE" exit 1 fi } usage() { colorized_echo blue "================================" colorized_echo magenta " $APP_NAME Node CLI Help" colorized_echo blue "================================" colorized_echo cyan "Usage:" echo " $APP_NAME [command]" echo colorized_echo cyan "Commands:" colorized_echo yellow " up $(tput sgr0)– Start services" colorized_echo yellow " down $(tput sgr0)– Stop services" colorized_echo yellow " restart $(tput sgr0)– Restart services" colorized_echo yellow " status $(tput sgr0)– Show status" colorized_echo yellow " logs $(tput sgr0)– Show logs" colorized_echo yellow " install $(tput sgr0)– Install/reinstall Marzban-node" colorized_echo yellow " update $(tput sgr0)– Update to latest version" colorized_echo yellow " uninstall $(tput sgr0)– Uninstall Marzban-node" colorized_echo yellow " install-script $(tput sgr0)– Install Marzban-node script" colorized_echo yellow " uninstall-script $(tput sgr0)– Uninstall Marzban-node script" colorized_echo yellow " edit $(tput sgr0)– Edit docker-compose.yml (via nano or vi)" colorized_echo yellow " core-update $(tput sgr0)– Update/Change Xray core" echo colorized_echo cyan "Node Information:" colorized_echo magenta " Cert file path: $CERT_FILE" colorized_echo magenta " Node IP: $NODE_IP" echo current_version=$(get_current_xray_core_version) colorized_echo cyan "Current Xray-core version: " 1 # 1 for bold colorized_echo magenta "$current_version" 1 echo DEFAULT_SERVICE_PORT="62050" DEFAULT_XRAY_API_PORT="62051" if [ -f "$COMPOSE_FILE" ]; then SERVICE_PORT=$(awk -F': ' '/SERVICE_PORT:/ {gsub(/"/, "", $2); print $2}' "$COMPOSE_FILE") XRAY_API_PORT=$(awk -F': ' '/XRAY_API_PORT:/ {gsub(/"/, "", $2); print $2}' "$COMPOSE_FILE") fi SERVICE_PORT=${SERVICE_PORT:-$DEFAULT_SERVICE_PORT} XRAY_API_PORT=${XRAY_API_PORT:-$DEFAULT_XRAY_API_PORT} colorized_echo cyan "Ports:" colorized_echo magenta " Service port: $SERVICE_PORT" colorized_echo magenta " API port: $XRAY_API_PORT" colorized_echo blue "=================================" echo } # >> offline support — added by patch-marzban.py; overrides the # >> upstream docker/compose/package helpers defined above. # --------------------------------------------------------------------------- # Offline install support # # Upstream pipes the get.docker.com installer into sh (which wires # the host to download.docker.com) and pulls the image from Docker Hub. Both are # unreachable from an Iranian server, so everything is served from the mirror # instead and the image is loaded from a local docker-archive. # --------------------------------------------------------------------------- MARZBAN_MIRROR="https://git.nimbaha.org/marzban" # x86_64/aarch64 naming used by the docker static tarball and the compose plugin _marzban_host_arch() { case "$(uname -m)" in x86_64|amd64) echo x86_64 ;; aarch64|arm64) echo aarch64 ;; *) return 1 ;; esac } # amd64/arm64 naming used by the docker image archives _marzban_image_arch() { case "$(uname -m)" in x86_64|amd64) echo amd64 ;; aarch64|arm64) echo arm64 ;; *) return 1 ;; esac } _marzban_fetch() { # if command -v curl >/dev/null 2>&1; then curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 20 "$1" -o "$2" else wget -q --tries=3 --timeout=20 -O "$2" "$1" fi } ensure_docker_service_running() { if ! command -v systemctl >/dev/null 2>&1; then return 0; fi if [ ! -d /run/systemd/system ]; then return 0; fi if systemctl is-active --quiet docker; then return 0; fi colorized_echo blue "Starting Docker service" systemctl enable --now docker >/dev/null 2>&1 || true return 0 } # The function above returns silently when systemd is absent (containers) or the # unit refuses to start, leaving no socket at all. Make sure a daemon exists # before anything tries to talk to it. ensure_docker_daemon() { docker info >/dev/null 2>&1 && return 0 ensure_docker_service_running local i for i in $(seq 1 15); do docker info >/dev/null 2>&1 && return 0 sleep 1 done if command -v dockerd >/dev/null 2>&1 \ && { ! command -v pgrep >/dev/null 2>&1 || ! pgrep -x dockerd >/dev/null 2>&1; }; then colorized_echo yellow " The Docker service did not come up; starting dockerd directly" nohup dockerd -H unix:///var/run/docker.sock >/var/log/dockerd.log 2>&1 & fi for i in $(seq 1 30); do docker info >/dev/null 2>&1 && return 0 sleep 1 done return 1 } _install_compose_plugin() { local arch dest=/usr/local/lib/docker/cli-plugins/docker-compose arch=$(_marzban_host_arch) || return 1 mkdir -p /usr/local/lib/docker/cli-plugins || return 1 if [ -x "$dest" ]; then return 0; fi colorized_echo blue "Installing Docker Compose v2 plugin (offline mirror)" if _marzban_fetch "$MARZBAN_MIRROR/bin/compose/docker-compose-linux-$arch" "$dest.tmp"; then chmod 755 "$dest.tmp" && mv -f "$dest.tmp" "$dest" return $? fi rm -f "$dest.tmp" return 1 } # Replaces the get.docker.com installer. install_docker() { if command -v docker >/dev/null 2>&1; then ensure_docker_service_running ensure_docker_daemon >/dev/null 2>&1 || true _install_compose_plugin >/dev/null 2>&1 || true return 0 fi local arch tmp arch=$(_marzban_host_arch) || { colorized_echo red "Unsupported architecture: $(uname -m)"; return 1; } tmp=$(mktemp -d) || return 1 colorized_echo blue "Installing Docker from the bundled static build ($arch)" if ! _marzban_fetch "$MARZBAN_MIRROR/bin/docker-static/docker-29.9.3-$arch.tgz" "$tmp/docker.tgz"; then rm -rf "$tmp" colorized_echo red "Could not download the Docker bundle from the mirror" return 1 fi if ! tar -xzf "$tmp/docker.tgz" -C "$tmp"; then rm -rf "$tmp" colorized_echo red "Could not extract the Docker bundle" return 1 fi if ! install -m 755 "$tmp"/docker/* /usr/local/bin/; then rm -rf "$tmp" colorized_echo red "Could not install the Docker binaries" return 1 fi rm -rf "$tmp" # Static builds ship no unit file; dockerd starts containerd itself. cat > /etc/systemd/system/docker.service <<'DOCKERUNIT' [Unit] Description=Docker Application Container Engine (offline static build) After=network-online.target Wants=network-online.target [Service] Type=notify ExecStart=/usr/local/bin/dockerd -H fd:// ExecReload=/bin/kill -s HUP $MAINPID LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity TasksMax=infinity TimeoutStartSec=0 Delegate=yes KillMode=process Restart=on-failure StartLimitBurst=3 StartLimitInterval=60s [Install] WantedBy=multi-user.target DOCKERUNIT systemctl daemon-reload >/dev/null 2>&1 || true ensure_docker_service_running ensure_docker_daemon >/dev/null 2>&1 || true _install_compose_plugin || colorized_echo yellow "Compose plugin deferred; retrying at detect_compose" colorized_echo green "Docker installed successfully" } # Replaces the upstream check: if no compose is present, install the plugin # from the mirror instead of giving up. detect_compose() { if docker compose version >/dev/null 2>&1; then COMPOSE='docker compose' return 0 fi if docker-compose --version >/dev/null 2>&1; then COMPOSE='docker-compose' return 0 fi if _install_compose_plugin && docker compose version >/dev/null 2>&1; then COMPOSE='docker compose' return 0 fi colorized_echo red "docker compose not found" exit 1 } # Load the image from the bundled docker-archive. crane saves it under the # registry mirror's name, so it is re-tagged to the reference compose expects. _load_marzban_image() { local arch tag="gozargah/marzban-node:latest" src tmp if docker image inspect "$tag" >/dev/null 2>&1; then return 0 fi arch=$(_marzban_image_arch) || return 1 if ! ensure_docker_daemon; then colorized_echo red "The Docker daemon is not running and could not be started" (systemctl status docker --no-pager -l 2>/dev/null || true) | tail -8 | sed 's/^/ /' if [ -s /var/log/dockerd.log ]; then tail -8 /var/log/dockerd.log | sed 's/^/ /' fi return 1 fi tmp=$(mktemp) || return 1 colorized_echo blue "Loading the marzban-node image from the mirror ($arch)" if ! _marzban_fetch "$MARZBAN_MIRROR/images/marzban-node-$arch.tar" "$tmp"; then rm -f "$tmp" colorized_echo red "Could not download the marzban-node image from the mirror" return 1 fi if ! load_out=$(docker load -i "$tmp" 2>&1); then rm -f "$tmp" colorized_echo red "Could not load the marzban-node image archive" printf '%s\n' "$load_out" | tail -6 | sed 's/^/ /' return 1 fi rm -f "$tmp" for src in "hub.rat.dev/gozargah/marzban-node:latest" \ "dockerproxy.net/gozargah/marzban-node:latest" \ "docker.1ms.run/gozargah/marzban-node:latest" \ "gozargah/marzban-node:latest"; do if docker tag "$src" "$tag" 2>/dev/null; then break fi done docker image inspect "$tag" >/dev/null 2>&1 } _marzban_install_jq_static() { command -v jq >/dev/null 2>&1 && return 0 local arch case "$(uname -m)" in x86_64|amd64) arch=amd64 ;; aarch64|arm64) arch=arm64 ;; *) return 1 ;; esac _marzban_fetch "$MARZBAN_MIRROR/bin/jq/jq-linux-$arch" /usr/local/bin/jq || return 1 chmod 755 /usr/local/bin/jq hash -r command -v jq >/dev/null 2>&1 } up_marzban_node() { _load_marzban_image || colorized_echo yellow "Bundled image unavailable; using whatever is already loaded." $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" up -d --remove-orphans } update_marzban_node() { _load_marzban_image || $COMPOSE -f $COMPOSE_FILE -p "$APP_NAME" pull } # >> temporary Iranian package mirrors # --------------------------------------------------------------------------- # Temporary Iranian package mirrors # # Nothing below runs unless a package install has already failed, i.e. the host # cannot reach its configured repositories. The original configuration is # copied aside before a single byte is rewritten and an EXIT trap puts it back, # so the host is left exactly as it was found even if the installer dies or is # interrupted half way. # # The caller supplies the wrapper (see the end of this file's integration in # each patcher): it must call _pg_ir_enable only after its own normal install # path has failed once. # --------------------------------------------------------------------------- IR_SRC_BACKUP=/var/backups/ir-mirror-sources _pg_ir_mirror_active=0 # Self-contained so this block can be inlined into scripts that do not define # an is_redhat_family_os helper. _pg_ir_is_redhat() { case "${OS:-}" in CentOS*|AlmaLinux*|Rocky*|Red\ Hat*|Oracle*|Fedora*) return 0 ;; esac return 1 } # Probe for each base URL read from stdin; print the first that answers. _pg_ir_pick() { local suffix="$1" base while IFS= read -r base; do [ -n "$base" ] || continue if curl -fsSL --connect-timeout 6 --max-time 12 -o /dev/null -r 0-0 "$base$suffix" 2>/dev/null; then printf '%s\n' "$base" return 0 fi done return 1 } _pg_ir_backup() { mkdir -p "$IR_SRC_BACKUP" || return 1 rm -rf "$IR_SRC_BACKUP"/* 2>/dev/null if [ -f /etc/apt/sources.list ]; then cp -a /etc/apt/sources.list "$IR_SRC_BACKUP/sources.list" || return 1 fi if [ -d /etc/apt/sources.list.d ]; then mkdir -p "$IR_SRC_BACKUP/sources.list.d" cp -a /etc/apt/sources.list.d/. "$IR_SRC_BACKUP/sources.list.d/" 2>/dev/null || true fi if [ -d /etc/yum.repos.d ]; then mkdir -p "$IR_SRC_BACKUP/yum.repos.d" cp -a /etc/yum.repos.d/. "$IR_SRC_BACKUP/yum.repos.d/" 2>/dev/null || true fi return 0 } _pg_ir_restore() { [ "$_pg_ir_mirror_active" = 1 ] || return 0 _pg_ir_mirror_active=0 trap - EXIT INT TERM if [ -d "$IR_SRC_BACKUP/sources.list.d" ]; then rm -rf /etc/apt/sources.list.d mkdir -p /etc/apt/sources.list.d cp -a "$IR_SRC_BACKUP/sources.list.d/." /etc/apt/sources.list.d/ 2>/dev/null || true fi if [ -f "$IR_SRC_BACKUP/sources.list" ]; then cp -a "$IR_SRC_BACKUP/sources.list" /etc/apt/sources.list 2>/dev/null || true fi if [ -d "$IR_SRC_BACKUP/yum.repos.d" ]; then rm -rf /etc/yum.repos.d mkdir -p /etc/yum.repos.d cp -a "$IR_SRC_BACKUP/yum.repos.d/." /etc/yum.repos.d/ 2>/dev/null || true fi # Deliberately no `apt-get update` here: the original repositories are # unreachable by definition, so refreshing would just hang the exit. colorized_echo green "Package repositories restored to their original configuration." } _pg_ir_enable() { [ "$_pg_ir_mirror_active" = 1 ] && return 0 [ -n "${OS:-}" ] || detect_os local codename base sec rv arch comps codename=$(sed -n 's/^VERSION_CODENAME=//p' /etc/os-release 2>/dev/null | tr -d '"') if [ -z "$codename" ]; then colorized_echo yellow "No release codename found; cannot select an Iranian mirror." return 1 fi if [ "$OS" = "Ubuntu" ] || [ "${OS#Debian}" != "$OS" ]; then if [ "$OS" = "Ubuntu" ]; then base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND' https://mirror.arvancloud.ir/ubuntu https://repo.iut.ac.ir/ubuntu https://mirror.iranserver.com/ubuntu CAND ) else base=$(_pg_ir_pick "/dists/$codename/Release" <<'CAND' https://mirror.arvancloud.ir/debian https://repo.iut.ac.ir/debian CAND ) sec=$(_pg_ir_pick "/dists/$codename-security/Release" <<'CAND' https://mirror.arvancloud.ir/debian-security CAND ) fi if [ -z "$base" ]; then colorized_echo yellow "No reachable Iranian mirror for $OS $codename." return 1 fi _pg_ir_backup || return 1 mkdir -p /etc/apt/sources.list.d # Third-party lists may point abroad as well; park them for the duration. find /etc/apt/sources.list.d -maxdepth 1 -name '*.list' -exec rm -f {} + 2>/dev/null || true if [ "$OS" = "Ubuntu" ]; then cat > /etc/apt/sources.list < /etc/apt/sources.list <> /etc/apt/sources.list fi fi _pg_ir_mirror_active=1 trap '_pg_ir_restore' EXIT INT TERM colorized_echo yellow "Package repositories temporarily switched to $base; they will be restored on exit." apt-get update -qq >/dev/null 2>&1 || true return 0 fi if _pg_ir_is_redhat; then rv=$(rpm -E %{rhel} 2>/dev/null) case "$(uname -m)" in x86_64|amd64) arch=x86_64 ;; aarch64|arm64) arch=aarch64 ;; *) return 1 ;; esac [ -n "$rv" ] || return 1 base=$(_pg_ir_pick "/almalinux/$rv/BaseOS/$arch/os/repodata/repomd.xml" <<'CAND' https://mirror.iranserver.com/almalinux https://mirror.arvancloud.ir/almalinux CAND ) if [ -z "$base" ]; then colorized_echo yellow "No reachable Iranian mirror for $OS $rv." return 1 fi _pg_ir_backup || return 1 mkdir -p /etc/yum.repos.d rm -f /etc/yum.repos.d/*.repo cat > /etc/yum.repos.d/ir-temp-mirror.repo </dev/null 2>&1 || true return 0 fi colorized_echo yellow "No Iranian mirror recipe for $OS." return 1 } install_package() { local package="$1" if [ "$package" = "jq" ] && _marzban_install_jq_static; then return 0 fi if _install_package_impl "$package"; then return 0 fi colorized_echo yellow "Package repositories are unreachable; retrying through an Iranian mirror." _pg_ir_enable || return 1 _install_package_impl "$package" } # << offline support case "$COMMAND" in install) install_command ;; update) update_command ;; uninstall) uninstall_command ;; up) up_command ;; down) down_command ;; restart) restart_command ;; status) status_command ;; logs) logs_command ;; core-update) update_core_command ;; install-script) install_marzban_node_script ;; uninstall-script) uninstall_marzban_node_script ;; edit) edit_command ;; *) usage ;; esac