#!/usr/bin/env bash set -Eeuo pipefail PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin red='\033[0;31m' green='\033[0;32m' yellow='\033[0;33m' plain='\033[0m' service_name='ssr-shell' config_dir='/etc/shadowsocks-rust' config_file="${config_dir}/server.json" service_file="/etc/systemd/system/${service_name}.service" method='2022-blake3-aes-256-gcm' node_name='HKIXhuoshan-akcdn' info() { echo -e "[${green}Info${plain}] $*" } warn() { echo -e "[${yellow}Warn${plain}] $*" } fail() { echo -e "[${red}Error${plain}] $*" >&2 exit 1 } require_root() { [ "${EUID}" -eq 0 ] || fail "Please run as root." } command_exists() { command -v "$1" >/dev/null 2>&1 } detect_package_manager() { if command_exists apt-get; then echo apt elif command_exists dnf; then echo dnf elif command_exists yum; then echo yum else echo unknown fi } install_base_dependencies() { local pm pm=$(detect_package_manager) case "${pm}" in apt) export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y ca-certificates curl tar xz-utils openssl iproute2 ;; dnf) dnf install -y ca-certificates curl tar xz openssl iproute ;; yum) yum install -y ca-certificates curl tar xz openssl iproute ;; *) fail "Unsupported package manager. Please install curl, tar, xz, openssl manually." ;; esac } try_install_from_package_manager() { local pm pm=$(detect_package_manager) case "${pm}" in apt) export DEBIAN_FRONTEND=noninteractive apt-get install -y shadowsocks-rust || true ;; dnf) dnf install -y shadowsocks-rust || true ;; yum) yum install -y shadowsocks-rust || true ;; esac } detect_release_target() { local arch arch=$(uname -m) case "${arch}" in x86_64|amd64) echo x86_64-unknown-linux-gnu ;; aarch64|arm64) echo aarch64-unknown-linux-gnu ;; armv7l|armv7) echo armv7-unknown-linux-gnueabihf ;; *) fail "Unsupported CPU architecture: ${arch}" ;; esac } install_from_github_release() { local target api url tmp archive ssserver_path ssservice_path target=$(detect_release_target) api='https://api.github.com/repos/shadowsocks/shadowsocks-rust/releases/latest' tmp=$(mktemp -d) archive="${tmp}/shadowsocks-rust.tar" info "Downloading official shadowsocks-rust release for ${target}..." url=$(curl -fsSL "${api}" \ | grep -E '"browser_download_url":' \ | grep "${target}" \ | grep -E 'tar\.(xz|gz)"' \ | head -n 1 \ | sed -E 's/.*"([^"]+)".*/\1/') [ -n "${url}" ] || fail "Cannot find a shadowsocks-rust release asset for ${target}." curl -fL "${url}" -o "${archive}" tar -xf "${archive}" -C "${tmp}" ssserver_path=$(find "${tmp}" -type f -name ssserver | head -n 1) ssservice_path=$(find "${tmp}" -type f -name ssservice | head -n 1) [ -n "${ssserver_path}" ] || fail "ssserver binary not found in release archive." install -m 0755 "${ssserver_path}" /usr/local/bin/ssserver if [ -n "${ssservice_path}" ]; then install -m 0755 "${ssservice_path}" /usr/local/bin/ssservice fi rm -rf "${tmp}" } install_shadowsocks_rust() { if command_exists ssserver; then info "ssserver already installed: $(command -v ssserver)" return 0 fi try_install_from_package_manager if command_exists ssserver; then info "ssserver installed: $(command -v ssserver)" return 0 fi install_from_github_release command_exists ssserver || fail "Failed to install ssserver." } is_valid_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ] } random_port() { echo $((10000 + RANDOM % 50000)) } public_ip() { local ip ip=$(curl -4fsS --max-time 3 https://api.ipify.org 2>/dev/null || true) if [ -z "${ip}" ] && command_exists ip; then ip=$(ip -4 addr show scope global | awk '/inet / { sub(/\/.*/, "", $2); print $2; exit }') fi echo "${ip}" } prompt_value() { local prompt default value prompt="$1" default="$2" read -r -p "${prompt} (Default: ${default}): " value if [ -z "${value}" ]; then value="${default}" fi echo "${value}" } prompt_port() { local default_port value default_port=$(random_port) while true; do value=$(prompt_value "Please enter Shadowsocks server port" "${default_port}") if is_valid_port "${value}"; then echo "${value}" return 0 fi warn "Invalid port: ${value}" done } generate_key() { if command_exists ssservice; then if ssservice genkey -m "${method}" 2>/dev/null; then return 0 fi fi openssl rand -base64 32 } write_config() { local port password port="$1" password="$2" mkdir -p "${config_dir}" chmod 0755 "${config_dir}" cat > "${config_file}" < "${service_file}" </dev/null 2>&1 || true firewall-cmd --permanent --add-port="${port}/udp" >/dev/null 2>&1 || true firewall-cmd --reload >/dev/null 2>&1 || true info "firewalld opened ${port}/tcp and ${port}/udp." elif command_exists ufw && ufw status 2>/dev/null | grep -qi active; then ufw allow "${port}/tcp" >/dev/null 2>&1 || true ufw allow "${port}/udp" >/dev/null 2>&1 || true info "ufw opened ${port}/tcp and ${port}/udp." else warn "No active firewalld/ufw detected. Open ${port}/tcp and ${port}/udp manually if needed." fi } start_service() { systemctl daemon-reload systemctl enable --now "${service_name}" systemctl --no-pager --full status "${service_name}" || true } print_node() { local server port password name server="$1" port="$2" password="$3" name="$4" echo echo "Your Mihomo ss node:" printf ' - {name: %s, type: ss, server: %s, port: %s, cipher: "%s", password: "%s", udp: true}\n' "${name}" "${server}" "${port}" "${method}" "${password}" } install_action() { local port password default_server client_server name require_root install_base_dependencies install_shadowsocks_rust port=$(prompt_port) password=$(generate_key) default_server=$(public_ip) [ -n "${default_server}" ] || default_server='your.server.ip' client_server=$(prompt_value "Please enter server IP/domain for Mihomo node" "${default_server}") name=$(prompt_value "Please enter Mihomo node name" "${node_name}") write_config "${port}" "${password}" write_service open_firewall "${port}" start_service echo info "Config file: ${config_file}" info "Service name: ${service_name}" info "Cipher: ${method}" info "Port: ${port}" print_node "${client_server}" "${port}" "${password}" "${name}" } uninstall_action() { require_root systemctl disable --now "${service_name}" >/dev/null 2>&1 || true rm -f "${service_file}" rm -rf "${config_dir}" systemctl daemon-reload info "Uninstalled ${service_name}." } status_action() { systemctl --no-pager --full status "${service_name}" } usage() { echo "Usage: $(basename "$0") [install|uninstall|status]" } action="${1:-install}" case "${action}" in install) install_action ;; uninstall) uninstall_action ;; status) status_action ;; *) usage exit 1 ;; esac