mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-23 00:17:16 +08:00
1368 lines
42 KiB
Bash
Executable File
1368 lines
42 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
###############################################################################
|
|
# ss-setup -- Interactive TUI for shadowsocks-libev server/client setup
|
|
#
|
|
# Requires: whiptail or dialog, openssl (optional), curl (for plugin install)
|
|
# Usage: sudo ss-setup (full server setup + service management)
|
|
# ss-setup (config generation only, no service install)
|
|
###############################################################################
|
|
|
|
readonly SS_SETUP_VERSION="1.0.0"
|
|
readonly CONFIG_DIR="/etc/shadowsocks-libev"
|
|
readonly SYSTEMD_UNIT_DIR="/etc/systemd/system"
|
|
readonly SYSTEMD_TEMPLATE="shadowsocks-libev-server@.service"
|
|
|
|
# AEAD ciphers supported by shadowsocks-libev (from src/aead.c)
|
|
readonly AEAD_CIPHERS=(
|
|
"chacha20-ietf-poly1305"
|
|
"aes-256-gcm"
|
|
"aes-192-gcm"
|
|
"aes-128-gcm"
|
|
"xchacha20-ietf-poly1305"
|
|
)
|
|
|
|
# Known SIP003 plugins and their GitHub repos
|
|
# Using a function instead of associative array for bash 3.x compatibility
|
|
plugin_repo() {
|
|
case "$1" in
|
|
simple-obfs) echo "shadowsocks/simple-obfs" ;;
|
|
v2ray-plugin) echo "shadowsocks/v2ray-plugin" ;;
|
|
xray-plugin) echo "teddysun/xray-plugin" ;;
|
|
kcptun) echo "xtaci/kcptun" ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
readonly KNOWN_PLUGINS=("simple-obfs" "v2ray-plugin" "xray-plugin" "kcptun")
|
|
|
|
# Globals set by the setup flows
|
|
TUI_BACKEND=""
|
|
TMPDIR_CLEANUP=""
|
|
|
|
# Server config globals
|
|
CFG_SERVER="0.0.0.0"
|
|
CFG_SERVER_PORT="8388"
|
|
CFG_METHOD="chacha20-ietf-poly1305"
|
|
CFG_PASSWORD=""
|
|
CFG_TIMEOUT="300"
|
|
CFG_MODE="tcp_and_udp"
|
|
CFG_FAST_OPEN="false"
|
|
CFG_PLUGIN=""
|
|
CFG_PLUGIN_OPTS=""
|
|
CFG_INSTANCE_NAME="config"
|
|
|
|
# Client config globals
|
|
CFG_CLIENT_SERVER=""
|
|
CFG_CLIENT_SERVER_PORT="8388"
|
|
CFG_CLIENT_LOCAL_PORT="1080"
|
|
CFG_CLIENT_METHOD="chacha20-ietf-poly1305"
|
|
CFG_CLIENT_PASSWORD=""
|
|
CFG_CLIENT_PLUGIN=""
|
|
CFG_CLIENT_PLUGIN_OPTS=""
|
|
CFG_CLIENT_OUTPUT=""
|
|
|
|
###############################################################################
|
|
# Cleanup
|
|
###############################################################################
|
|
|
|
cleanup() {
|
|
if [[ -n "${TMPDIR_CLEANUP}" && -d "${TMPDIR_CLEANUP}" ]]; then
|
|
rm -rf "${TMPDIR_CLEANUP}"
|
|
fi
|
|
}
|
|
# Only set trap when executed directly (not when sourced for testing)
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
trap cleanup EXIT
|
|
fi
|
|
|
|
###############################################################################
|
|
# TUI Backend Detection & Wrappers
|
|
###############################################################################
|
|
|
|
detect_tui_backend() {
|
|
if command -v whiptail >/dev/null 2>&1; then
|
|
TUI_BACKEND="whiptail"
|
|
elif command -v dialog >/dev/null 2>&1; then
|
|
TUI_BACKEND="dialog"
|
|
else
|
|
echo "Error: neither whiptail nor dialog found." >&2
|
|
echo "Install one of them:" >&2
|
|
echo " Debian/Ubuntu: sudo apt install whiptail" >&2
|
|
echo " RHEL/Fedora: sudo dnf install newt" >&2
|
|
echo " Arch: sudo pacman -S libnewt" >&2
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# tui_msgbox title message
|
|
tui_msgbox() {
|
|
local title="$1" msg="$2"
|
|
$TUI_BACKEND --title "$title" --msgbox "$msg" 20 70
|
|
}
|
|
|
|
# tui_yesno title message → exit code 0=yes 1=no
|
|
tui_yesno() {
|
|
local title="$1" msg="$2"
|
|
if $TUI_BACKEND --title "$title" --yesno "$msg" 12 70; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# tui_inputbox title message default → prints value
|
|
tui_inputbox() {
|
|
local title="$1" msg="$2" default="${3:-}"
|
|
local result
|
|
result=$($TUI_BACKEND --title "$title" --inputbox "$msg" 10 70 "$default" 3>&1 1>&2 2>&3) || return $?
|
|
echo "$result"
|
|
}
|
|
|
|
# tui_passwordbox title message → prints value
|
|
tui_passwordbox() {
|
|
local title="$1" msg="$2"
|
|
local result
|
|
result=$($TUI_BACKEND --title "$title" --passwordbox "$msg" 10 70 3>&1 1>&2 2>&3) || return $?
|
|
echo "$result"
|
|
}
|
|
|
|
# tui_menu title message tag1 item1 tag2 item2 ... → prints selected tag
|
|
tui_menu() {
|
|
local title="$1" msg="$2"
|
|
shift 2
|
|
local items=("$@")
|
|
local count=$(( ${#items[@]} / 2 ))
|
|
local result
|
|
result=$($TUI_BACKEND --title "$title" --menu "$msg" 20 70 "$count" "${items[@]}" 3>&1 1>&2 2>&3) || return $?
|
|
echo "$result"
|
|
}
|
|
|
|
# tui_radiolist title message tag1 item1 status1 ... → prints selected tag
|
|
tui_radiolist() {
|
|
local title="$1" msg="$2"
|
|
shift 2
|
|
local items=("$@")
|
|
local count=$(( ${#items[@]} / 3 ))
|
|
local result
|
|
result=$($TUI_BACKEND --title "$title" --radiolist "$msg" 20 70 "$count" "${items[@]}" 3>&1 1>&2 2>&3) || return $?
|
|
echo "$result"
|
|
}
|
|
|
|
###############################################################################
|
|
# Utility Functions
|
|
###############################################################################
|
|
|
|
check_root() {
|
|
if [[ $EUID -ne 0 ]]; then
|
|
tui_msgbox "Notice" "Not running as root.\n\nYou can still generate config files, but service installation and plugin management will be skipped.\n\nRe-run with sudo for full functionality."
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
is_root() {
|
|
[[ $EUID -eq 0 ]]
|
|
}
|
|
|
|
validate_port() {
|
|
local port="$1"
|
|
if [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )); then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
validate_instance_name() {
|
|
local name="$1"
|
|
if [[ "$name" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
generate_password() {
|
|
local len="${1:-32}"
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
openssl rand -base64 "$len" | tr -d '\n'
|
|
else
|
|
head -c "$len" /dev/urandom | base64 | tr -d '\n'
|
|
fi
|
|
}
|
|
|
|
generate_random_port() {
|
|
local port
|
|
if [[ -r /dev/urandom ]]; then
|
|
port=$(( ($(od -An -tu2 -N2 /dev/urandom | tr -d ' ') % 55001) + 10000 ))
|
|
else
|
|
port=$(( (RANDOM % 55001) + 10000 ))
|
|
fi
|
|
echo "$port"
|
|
}
|
|
|
|
json_escape() {
|
|
local str="$1"
|
|
str="${str//\\/\\\\}"
|
|
str="${str//\"/\\\"}"
|
|
str="${str//$'\n'/\\n}"
|
|
str="${str//$'\r'/\\r}"
|
|
str="${str//$'\t'/\\t}"
|
|
echo -n "$str"
|
|
}
|
|
|
|
urlencode() {
|
|
local str="$1"
|
|
local encoded=""
|
|
local i c
|
|
for (( i = 0; i < ${#str}; i++ )); do
|
|
c="${str:$i:1}"
|
|
case "$c" in
|
|
[a-zA-Z0-9.~_-]) encoded+="$c" ;;
|
|
*) encoded+=$(printf '%%%02X' "'$c") ;;
|
|
esac
|
|
done
|
|
echo -n "$encoded"
|
|
}
|
|
|
|
# Generate SIP002 ss:// URI
|
|
# ss://BASE64URL(method:password)@host:port[/?plugin=URLENCODE(plugin;opts)]
|
|
generate_ss_uri() {
|
|
local method="$1" password="$2" server="$3" port="$4"
|
|
local plugin="${5:-}" plugin_opts="${6:-}"
|
|
|
|
local userinfo
|
|
userinfo=$(echo -n "${method}:${password}" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
|
|
|
|
local uri="ss://${userinfo}@${server}:${port}"
|
|
|
|
if [[ -n "$plugin" ]]; then
|
|
local plugin_str="$plugin"
|
|
if [[ -n "$plugin_opts" ]]; then
|
|
plugin_str="${plugin};${plugin_opts}"
|
|
fi
|
|
uri="${uri}/?plugin=$(urlencode "$plugin_str")"
|
|
fi
|
|
|
|
echo "$uri"
|
|
}
|
|
|
|
# Write JSON config from CFG_* globals (server mode)
|
|
# Produces valid JSON with no trailing commas; booleans unquoted
|
|
write_json_config() {
|
|
local outfile="$1"
|
|
local fast_open_val="false"
|
|
[[ "$CFG_FAST_OPEN" == "true" ]] && fast_open_val="true"
|
|
|
|
{
|
|
printf '{\n'
|
|
printf ' "server": "%s",\n' "$(json_escape "$CFG_SERVER")"
|
|
printf ' "server_port": %s,\n' "$CFG_SERVER_PORT"
|
|
printf ' "password": "%s",\n' "$(json_escape "$CFG_PASSWORD")"
|
|
printf ' "timeout": %s,\n' "$CFG_TIMEOUT"
|
|
printf ' "method": "%s",\n' "$(json_escape "$CFG_METHOD")"
|
|
printf ' "mode": "%s",\n' "$(json_escape "$CFG_MODE")"
|
|
if [[ -n "$CFG_PLUGIN" ]]; then
|
|
printf ' "fast_open": %s,\n' "$fast_open_val"
|
|
printf ' "plugin": "%s"' "$(json_escape "$CFG_PLUGIN")"
|
|
if [[ -n "$CFG_PLUGIN_OPTS" ]]; then
|
|
printf ',\n'
|
|
printf ' "plugin_opts": "%s"' "$(json_escape "$CFG_PLUGIN_OPTS")"
|
|
fi
|
|
else
|
|
printf ' "fast_open": %s' "$fast_open_val"
|
|
fi
|
|
printf '\n}\n'
|
|
} > "$outfile"
|
|
}
|
|
|
|
# Write JSON config for client mode
|
|
write_client_json_config() {
|
|
local outfile="$1"
|
|
|
|
{
|
|
printf '{\n'
|
|
printf ' "server": "%s",\n' "$(json_escape "$CFG_CLIENT_SERVER")"
|
|
printf ' "server_port": %s,\n' "$CFG_CLIENT_SERVER_PORT"
|
|
printf ' "local_address": "127.0.0.1",\n'
|
|
printf ' "local_port": %s,\n' "$CFG_CLIENT_LOCAL_PORT"
|
|
printf ' "password": "%s",\n' "$(json_escape "$CFG_CLIENT_PASSWORD")"
|
|
printf ' "timeout": 300,\n'
|
|
printf ' "method": "%s",\n' "$(json_escape "$CFG_CLIENT_METHOD")"
|
|
if [[ -n "$CFG_CLIENT_PLUGIN" ]]; then
|
|
printf ' "mode": "tcp_and_udp",\n'
|
|
printf ' "plugin": "%s"' "$(json_escape "$CFG_CLIENT_PLUGIN")"
|
|
if [[ -n "$CFG_CLIENT_PLUGIN_OPTS" ]]; then
|
|
printf ',\n'
|
|
printf ' "plugin_opts": "%s"' "$(json_escape "$CFG_CLIENT_PLUGIN_OPTS")"
|
|
fi
|
|
else
|
|
printf ' "mode": "tcp_and_udp"'
|
|
fi
|
|
printf '\n}\n'
|
|
} > "$outfile"
|
|
}
|
|
|
|
# Parse an existing config file and populate CFG_* globals
|
|
parse_existing_config() {
|
|
local file="$1"
|
|
local val
|
|
|
|
val=$(grep -o '"server"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | head -1 | sed 's/.*:.*"\(.*\)"/\1/') && [[ -n "$val" ]] && CFG_SERVER="$val"
|
|
val=$(grep -o '"server_port"[[:space:]]*:[[:space:]]*[0-9]*' "$file" | head -1 | sed 's/.*:[[:space:]]*//') && [[ -n "$val" ]] && CFG_SERVER_PORT="$val"
|
|
val=$(grep -o '"password"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | head -1 | sed 's/.*:.*"\(.*\)"/\1/') && [[ -n "$val" ]] && CFG_PASSWORD="$val"
|
|
val=$(grep -o '"method"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | head -1 | sed 's/.*:.*"\(.*\)"/\1/') && [[ -n "$val" ]] && CFG_METHOD="$val"
|
|
val=$(grep -o '"timeout"[[:space:]]*:[[:space:]]*[0-9]*' "$file" | head -1 | sed 's/.*:[[:space:]]*//') && [[ -n "$val" ]] && CFG_TIMEOUT="$val"
|
|
val=$(grep -o '"mode"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | head -1 | sed 's/.*:.*"\(.*\)"/\1/') && [[ -n "$val" ]] && CFG_MODE="$val"
|
|
val=$(grep -o '"fast_open"[[:space:]]*:[[:space:]]*[a-z]*' "$file" | head -1 | sed 's/.*:[[:space:]]*//') && [[ -n "$val" ]] && CFG_FAST_OPEN="$val"
|
|
val=$(grep -o '"plugin"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | head -1 | sed 's/.*:.*"\(.*\)"/\1/') && [[ -n "$val" ]] && CFG_PLUGIN="$val"
|
|
val=$(grep -o '"plugin_opts"[[:space:]]*:[[:space:]]*"[^"]*"' "$file" | head -1 | sed 's/.*:.*"\(.*\)"/\1/') && [[ -n "$val" ]] && CFG_PLUGIN_OPTS="$val"
|
|
|
|
return 0
|
|
}
|
|
|
|
detect_arch() {
|
|
local arch
|
|
arch=$(uname -m)
|
|
case "$arch" in
|
|
x86_64|amd64) echo "amd64" ;;
|
|
aarch64|arm64) echo "arm64" ;;
|
|
armv7*|armhf) echo "armv7" ;;
|
|
i686|i386) echo "386" ;;
|
|
*) echo "$arch" ;;
|
|
esac
|
|
}
|
|
|
|
detect_os() {
|
|
local os
|
|
os=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
case "$os" in
|
|
linux*) echo "linux" ;;
|
|
darwin*) echo "darwin" ;;
|
|
*) echo "$os" ;;
|
|
esac
|
|
}
|
|
|
|
detect_distro() {
|
|
if [[ -f /etc/os-release ]]; then
|
|
# shellcheck disable=SC1091
|
|
. /etc/os-release
|
|
echo "${ID:-unknown}"
|
|
elif command -v lsb_release >/dev/null 2>&1; then
|
|
lsb_release -si | tr '[:upper:]' '[:lower:]'
|
|
else
|
|
echo "unknown"
|
|
fi
|
|
}
|
|
|
|
has_systemd() {
|
|
command -v systemctl >/dev/null 2>&1 && [[ -d /run/systemd/system ]]
|
|
}
|
|
|
|
systemd_version() {
|
|
local ver
|
|
ver=$(systemctl --version 2>/dev/null | head -1 | grep -oE '[0-9]+' | head -1) || ver="0"
|
|
echo "$ver"
|
|
}
|
|
|
|
check_port_in_use() {
|
|
local port="$1"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tlnp 2>/dev/null | grep -q ":${port} " && return 0
|
|
elif command -v netstat >/dev/null 2>&1; then
|
|
netstat -tlnp 2>/dev/null | grep -q ":${port} " && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
find_installed_plugins() {
|
|
local plugins=()
|
|
local p
|
|
for p in "${KNOWN_PLUGINS[@]}"; do
|
|
if command -v "$p" >/dev/null 2>&1; then
|
|
plugins+=("$p")
|
|
fi
|
|
done
|
|
# Also check obfs-local (simple-obfs installs as obfs-local/obfs-server)
|
|
if command -v obfs-local >/dev/null 2>&1; then
|
|
local found=0
|
|
for p in "${plugins[@]}"; do
|
|
[[ "$p" == "simple-obfs" ]] && found=1
|
|
done
|
|
[[ $found -eq 0 ]] && plugins+=("obfs-local")
|
|
fi
|
|
echo "${plugins[*]}"
|
|
}
|
|
|
|
###############################################################################
|
|
# Server Setup Flow
|
|
###############################################################################
|
|
|
|
server_ask_listen_address() {
|
|
local addr
|
|
while true; do
|
|
addr=$(tui_inputbox "Listen Address" "Enter the server listen address:" "$CFG_SERVER") || return 1
|
|
if [[ -n "$addr" ]]; then
|
|
CFG_SERVER="$addr"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Address cannot be empty."
|
|
done
|
|
}
|
|
|
|
server_ask_port() {
|
|
local choice
|
|
choice=$(tui_menu "Server Port" "Choose how to set the server port:" \
|
|
"manual" "Enter port manually (default: ${CFG_SERVER_PORT})" \
|
|
"random" "Generate a random high port") || return 1
|
|
|
|
if [[ "$choice" == "random" ]]; then
|
|
CFG_SERVER_PORT=$(generate_random_port)
|
|
tui_msgbox "Random Port" "Selected port: ${CFG_SERVER_PORT}"
|
|
return 0
|
|
fi
|
|
|
|
local port
|
|
while true; do
|
|
port=$(tui_inputbox "Server Port" "Enter the server port (1-65535):" "$CFG_SERVER_PORT") || return 1
|
|
if validate_port "$port"; then
|
|
if check_port_in_use "$port"; then
|
|
if ! tui_yesno "Port In Use" "Port ${port} appears to be in use.\n\nContinue anyway?"; then
|
|
continue
|
|
fi
|
|
fi
|
|
CFG_SERVER_PORT="$port"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Invalid port number. Must be 1-65535."
|
|
done
|
|
}
|
|
|
|
server_ask_method() {
|
|
local items=()
|
|
local cipher
|
|
for cipher in "${AEAD_CIPHERS[@]}"; do
|
|
if [[ "$cipher" == "$CFG_METHOD" ]]; then
|
|
items+=("$cipher" "" "ON")
|
|
else
|
|
items+=("$cipher" "" "OFF")
|
|
fi
|
|
done
|
|
|
|
local method
|
|
method=$(tui_radiolist "Encryption Method" "Select the AEAD cipher to use:" "${items[@]}") || return 1
|
|
CFG_METHOD="$method"
|
|
}
|
|
|
|
server_ask_password() {
|
|
if tui_yesno "Password" "Auto-generate a secure random password?\n\n(Select No to enter manually)"; then
|
|
CFG_PASSWORD=$(generate_password 32)
|
|
tui_msgbox "Generated Password" "Password (save this!):\n\n${CFG_PASSWORD}"
|
|
return 0
|
|
fi
|
|
|
|
local pass1 pass2
|
|
while true; do
|
|
pass1=$(tui_passwordbox "Password" "Enter the password:") || return 1
|
|
if [[ -z "$pass1" ]]; then
|
|
tui_msgbox "Error" "Password cannot be empty."
|
|
continue
|
|
fi
|
|
pass2=$(tui_passwordbox "Confirm Password" "Confirm the password:") || return 1
|
|
if [[ "$pass1" == "$pass2" ]]; then
|
|
CFG_PASSWORD="$pass1"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Passwords do not match. Try again."
|
|
done
|
|
}
|
|
|
|
server_ask_timeout() {
|
|
local timeout
|
|
while true; do
|
|
timeout=$(tui_inputbox "Timeout" "Connection timeout in seconds:" "$CFG_TIMEOUT") || return 1
|
|
if [[ "$timeout" =~ ^[0-9]+$ ]] && (( timeout > 0 )); then
|
|
CFG_TIMEOUT="$timeout"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Timeout must be a positive integer."
|
|
done
|
|
}
|
|
|
|
server_ask_mode() {
|
|
local items=()
|
|
local modes=("tcp_only" "tcp_and_udp" "udp_only")
|
|
local m
|
|
for m in "${modes[@]}"; do
|
|
if [[ "$m" == "$CFG_MODE" ]]; then
|
|
items+=("$m" "" "ON")
|
|
else
|
|
items+=("$m" "" "OFF")
|
|
fi
|
|
done
|
|
|
|
local mode
|
|
mode=$(tui_radiolist "Network Mode" "Select the network mode:" "${items[@]}") || return 1
|
|
CFG_MODE="$mode"
|
|
}
|
|
|
|
server_ask_fast_open() {
|
|
# Check if TCP Fast Open is available (Linux only)
|
|
if [[ -f /proc/sys/net/ipv4/tcp_fastopen ]]; then
|
|
local tfo_val
|
|
tfo_val=$(cat /proc/sys/net/ipv4/tcp_fastopen 2>/dev/null) || tfo_val="0"
|
|
if (( tfo_val >= 2 )); then
|
|
CFG_FAST_OPEN="true"
|
|
fi
|
|
if tui_yesno "TCP Fast Open" "Enable TCP Fast Open?\n\n(Current kernel setting: ${tfo_val})\nRequires kernel support and sysctl net.ipv4.tcp_fastopen >= 2 for server."; then
|
|
CFG_FAST_OPEN="true"
|
|
else
|
|
CFG_FAST_OPEN="false"
|
|
fi
|
|
else
|
|
CFG_FAST_OPEN="false"
|
|
tui_msgbox "TCP Fast Open" "TCP Fast Open is not available on this system.\nSetting to disabled."
|
|
fi
|
|
}
|
|
|
|
server_ask_plugin() {
|
|
local installed
|
|
installed=$(find_installed_plugins)
|
|
|
|
local items=("none" "No plugin")
|
|
local p
|
|
for p in $installed; do
|
|
items+=("$p" "$(command -v "$p" 2>/dev/null || echo "$p")")
|
|
done
|
|
items+=("custom" "Enter custom plugin path")
|
|
|
|
local choice
|
|
choice=$(tui_menu "SIP003 Plugin" "Select a plugin (optional):" "${items[@]}") || return 1
|
|
|
|
if [[ "$choice" == "none" ]]; then
|
|
CFG_PLUGIN=""
|
|
CFG_PLUGIN_OPTS=""
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$choice" == "custom" ]]; then
|
|
local plugin_path
|
|
plugin_path=$(tui_inputbox "Custom Plugin" "Enter the plugin binary name or full path:" "") || return 1
|
|
if [[ -z "$plugin_path" ]]; then
|
|
CFG_PLUGIN=""
|
|
CFG_PLUGIN_OPTS=""
|
|
return 0
|
|
fi
|
|
CFG_PLUGIN="$plugin_path"
|
|
else
|
|
CFG_PLUGIN="$choice"
|
|
fi
|
|
|
|
local opts
|
|
opts=$(tui_inputbox "Plugin Options" "Enter plugin options (or leave empty):" "$CFG_PLUGIN_OPTS") || return 1
|
|
CFG_PLUGIN_OPTS="$opts"
|
|
}
|
|
|
|
server_ask_instance_name() {
|
|
local name
|
|
while true; do
|
|
name=$(tui_inputbox "Instance Name" "Enter a name for this config instance.\nThe config will be saved as ${CONFIG_DIR}/<name>.json" "$CFG_INSTANCE_NAME") || return 1
|
|
if ! validate_instance_name "$name"; then
|
|
tui_msgbox "Error" "Invalid name. Use only letters, numbers, hyphens, and underscores."
|
|
continue
|
|
fi
|
|
if [[ -f "${CONFIG_DIR}/${name}.json" ]]; then
|
|
local action
|
|
action=$(tui_menu "Config Exists" "Config '${name}.json' already exists:" \
|
|
"overwrite" "Overwrite the existing config" \
|
|
"edit" "Load and edit the existing config" \
|
|
"rename" "Choose a different name") || return 1
|
|
case "$action" in
|
|
overwrite)
|
|
CFG_INSTANCE_NAME="$name"
|
|
return 0
|
|
;;
|
|
edit)
|
|
parse_existing_config "${CONFIG_DIR}/${name}.json"
|
|
CFG_INSTANCE_NAME="$name"
|
|
return 2 # signal to restart setup with loaded values
|
|
;;
|
|
rename)
|
|
continue
|
|
;;
|
|
esac
|
|
else
|
|
CFG_INSTANCE_NAME="$name"
|
|
return 0
|
|
fi
|
|
done
|
|
}
|
|
|
|
server_generate_config() {
|
|
local config_file="${CONFIG_DIR}/${CFG_INSTANCE_NAME}.json"
|
|
|
|
if is_root; then
|
|
mkdir -p "$CONFIG_DIR"
|
|
write_json_config "$config_file"
|
|
chmod 640 "$config_file"
|
|
else
|
|
# Non-root: write to current directory
|
|
config_file="${CFG_INSTANCE_NAME}.json"
|
|
write_json_config "$config_file"
|
|
fi
|
|
|
|
echo "$config_file"
|
|
}
|
|
|
|
server_install_systemd_template() {
|
|
if ! has_systemd; then
|
|
return 1
|
|
fi
|
|
|
|
local template_path="${SYSTEMD_UNIT_DIR}/${SYSTEMD_TEMPLATE}"
|
|
|
|
# Only install if missing
|
|
if [[ -f "$template_path" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local ss_server_path
|
|
ss_server_path=$(command -v ss-server 2>/dev/null) || ss_server_path="/usr/local/bin/ss-server"
|
|
|
|
local sd_ver
|
|
sd_ver=$(systemd_version)
|
|
|
|
if (( sd_ver >= 232 )); then
|
|
cat > "$template_path" <<UNIT
|
|
[Unit]
|
|
Description=Shadowsocks-Libev Custom Server Service for %I
|
|
Documentation=man:ss-server(1)
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
|
DynamicUser=true
|
|
LimitNOFILE=32768
|
|
ExecStart=${ss_server_path} -c /etc/shadowsocks-libev/%i.json
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
else
|
|
cat > "$template_path" <<UNIT
|
|
[Unit]
|
|
Description=Shadowsocks-Libev Custom Server Service for %I
|
|
Documentation=man:ss-server(1)
|
|
After=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
|
User=nobody
|
|
Group=nogroup
|
|
LimitNOFILE=32768
|
|
ExecStart=${ss_server_path} -c /etc/shadowsocks-libev/%i.json
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
UNIT
|
|
fi
|
|
|
|
systemctl daemon-reload
|
|
}
|
|
|
|
server_install_service() {
|
|
local name="$1"
|
|
local unit="shadowsocks-libev-server@${name}.service"
|
|
|
|
server_install_systemd_template || {
|
|
tui_msgbox "No systemd" "systemd not detected. Skipping service installation.\n\nStart manually:\n ss-server -c ${CONFIG_DIR}/${name}.json"
|
|
return 1
|
|
}
|
|
|
|
systemctl enable "$unit" 2>/dev/null
|
|
systemctl restart "$unit" 2>/dev/null
|
|
|
|
if systemctl is-active --quiet "$unit"; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
server_show_summary() {
|
|
local config_file="$1"
|
|
local uri
|
|
uri=$(generate_ss_uri "$CFG_METHOD" "$CFG_PASSWORD" "$CFG_SERVER" "$CFG_SERVER_PORT" "$CFG_PLUGIN" "$CFG_PLUGIN_OPTS")
|
|
|
|
local service_status="(not installed)"
|
|
if is_root && has_systemd; then
|
|
local unit="shadowsocks-libev-server@${CFG_INSTANCE_NAME}.service"
|
|
if systemctl is-active --quiet "$unit" 2>/dev/null; then
|
|
service_status="active (running)"
|
|
else
|
|
service_status="inactive or failed"
|
|
fi
|
|
fi
|
|
|
|
local summary=""
|
|
summary+="Config file: ${config_file}\n"
|
|
summary+="\n"
|
|
summary+="Server: ${CFG_SERVER}:${CFG_SERVER_PORT}\n"
|
|
summary+="Method: ${CFG_METHOD}\n"
|
|
summary+="Password: ${CFG_PASSWORD}\n"
|
|
summary+="Mode: ${CFG_MODE}\n"
|
|
summary+="Fast Open: ${CFG_FAST_OPEN}\n"
|
|
if [[ -n "$CFG_PLUGIN" ]]; then
|
|
summary+="Plugin: ${CFG_PLUGIN}\n"
|
|
[[ -n "$CFG_PLUGIN_OPTS" ]] && summary+="Opts: ${CFG_PLUGIN_OPTS}\n"
|
|
fi
|
|
summary+="Service: ${service_status}\n"
|
|
summary+="\n"
|
|
summary+="ss:// URI (for client import):\n"
|
|
summary+="${uri}\n"
|
|
|
|
tui_msgbox "Server Setup Complete" "$summary"
|
|
}
|
|
|
|
server_setup() {
|
|
while true; do
|
|
server_ask_instance_name
|
|
local rc=$?
|
|
if [[ $rc -eq 1 ]]; then
|
|
return # user cancelled
|
|
fi
|
|
# rc=2 means config was loaded for editing, restart the flow
|
|
# rc=0 means proceed
|
|
|
|
server_ask_listen_address || return
|
|
server_ask_port || return
|
|
server_ask_method || return
|
|
server_ask_password || return
|
|
server_ask_timeout || return
|
|
server_ask_mode || return
|
|
server_ask_fast_open
|
|
server_ask_plugin || return
|
|
|
|
# Confirm before writing
|
|
local confirm_msg=""
|
|
confirm_msg+="Server: ${CFG_SERVER}:${CFG_SERVER_PORT}\n"
|
|
confirm_msg+="Method: ${CFG_METHOD}\n"
|
|
confirm_msg+="Mode: ${CFG_MODE}\n"
|
|
confirm_msg+="Fast Open: ${CFG_FAST_OPEN}\n"
|
|
confirm_msg+="Instance: ${CFG_INSTANCE_NAME}\n"
|
|
[[ -n "$CFG_PLUGIN" ]] && confirm_msg+="Plugin: ${CFG_PLUGIN}\n"
|
|
|
|
if ! tui_yesno "Confirm" "Review your settings:\n\n${confirm_msg}\nProceed?"; then
|
|
if tui_yesno "Restart" "Start over with new settings?"; then
|
|
continue
|
|
fi
|
|
return
|
|
fi
|
|
|
|
break
|
|
done
|
|
|
|
# Generate config
|
|
local config_file
|
|
config_file=$(server_generate_config)
|
|
|
|
# Install systemd service if root
|
|
if is_root && has_systemd; then
|
|
if tui_yesno "Systemd Service" "Install and start a systemd service for this instance?"; then
|
|
if server_install_service "$CFG_INSTANCE_NAME"; then
|
|
tui_msgbox "Service Started" "Service shadowsocks-libev-server@${CFG_INSTANCE_NAME} is now running."
|
|
else
|
|
tui_msgbox "Service Error" "Service failed to start.\n\nCheck: journalctl -u shadowsocks-libev-server@${CFG_INSTANCE_NAME} -n 20"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Show summary
|
|
server_show_summary "$config_file"
|
|
}
|
|
|
|
###############################################################################
|
|
# Client Config Flow
|
|
###############################################################################
|
|
|
|
client_ask_server_address() {
|
|
local addr
|
|
while true; do
|
|
addr=$(tui_inputbox "Server Address" "Enter the remote ss-server address (IP or hostname):" "$CFG_CLIENT_SERVER") || return 1
|
|
if [[ -n "$addr" ]]; then
|
|
CFG_CLIENT_SERVER="$addr"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Server address cannot be empty."
|
|
done
|
|
}
|
|
|
|
client_ask_server_port() {
|
|
local port
|
|
while true; do
|
|
port=$(tui_inputbox "Server Port" "Enter the remote ss-server port:" "$CFG_CLIENT_SERVER_PORT") || return 1
|
|
if validate_port "$port"; then
|
|
CFG_CLIENT_SERVER_PORT="$port"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Invalid port number. Must be 1-65535."
|
|
done
|
|
}
|
|
|
|
client_ask_local_port() {
|
|
local port
|
|
while true; do
|
|
port=$(tui_inputbox "Local Port" "Enter the local SOCKS5 listen port:" "$CFG_CLIENT_LOCAL_PORT") || return 1
|
|
if validate_port "$port"; then
|
|
CFG_CLIENT_LOCAL_PORT="$port"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Invalid port number. Must be 1-65535."
|
|
done
|
|
}
|
|
|
|
client_ask_method() {
|
|
local items=()
|
|
local cipher
|
|
for cipher in "${AEAD_CIPHERS[@]}"; do
|
|
if [[ "$cipher" == "$CFG_CLIENT_METHOD" ]]; then
|
|
items+=("$cipher" "" "ON")
|
|
else
|
|
items+=("$cipher" "" "OFF")
|
|
fi
|
|
done
|
|
|
|
local method
|
|
method=$(tui_radiolist "Encryption Method" "Select the cipher (must match server):" "${items[@]}") || return 1
|
|
CFG_CLIENT_METHOD="$method"
|
|
}
|
|
|
|
client_ask_password() {
|
|
local pass
|
|
while true; do
|
|
pass=$(tui_passwordbox "Password" "Enter the password (must match server):") || return 1
|
|
if [[ -n "$pass" ]]; then
|
|
CFG_CLIENT_PASSWORD="$pass"
|
|
return 0
|
|
fi
|
|
tui_msgbox "Error" "Password cannot be empty."
|
|
done
|
|
}
|
|
|
|
client_ask_plugin() {
|
|
local installed
|
|
installed=$(find_installed_plugins)
|
|
|
|
local items=("none" "No plugin")
|
|
local p
|
|
for p in $installed; do
|
|
items+=("$p" "$(command -v "$p" 2>/dev/null || echo "$p")")
|
|
done
|
|
items+=("custom" "Enter custom plugin path")
|
|
|
|
local choice
|
|
choice=$(tui_menu "SIP003 Plugin" "Select a plugin (must match server):" "${items[@]}") || return 1
|
|
|
|
if [[ "$choice" == "none" ]]; then
|
|
CFG_CLIENT_PLUGIN=""
|
|
CFG_CLIENT_PLUGIN_OPTS=""
|
|
return 0
|
|
fi
|
|
|
|
if [[ "$choice" == "custom" ]]; then
|
|
local plugin_path
|
|
plugin_path=$(tui_inputbox "Custom Plugin" "Enter the plugin binary name or full path:" "") || return 1
|
|
if [[ -z "$plugin_path" ]]; then
|
|
CFG_CLIENT_PLUGIN=""
|
|
CFG_CLIENT_PLUGIN_OPTS=""
|
|
return 0
|
|
fi
|
|
CFG_CLIENT_PLUGIN="$plugin_path"
|
|
else
|
|
CFG_CLIENT_PLUGIN="$choice"
|
|
fi
|
|
|
|
local opts
|
|
opts=$(tui_inputbox "Plugin Options" "Enter plugin options (or leave empty):" "$CFG_CLIENT_PLUGIN_OPTS") || return 1
|
|
CFG_CLIENT_PLUGIN_OPTS="$opts"
|
|
}
|
|
|
|
client_ask_output_path() {
|
|
local default_path
|
|
if is_root; then
|
|
default_path="${CONFIG_DIR}/client.json"
|
|
else
|
|
default_path="${HOME}/ss-client.json"
|
|
fi
|
|
[[ -n "$CFG_CLIENT_OUTPUT" ]] && default_path="$CFG_CLIENT_OUTPUT"
|
|
|
|
local path
|
|
path=$(tui_inputbox "Output Path" "Where to save the client config:" "$default_path") || return 1
|
|
CFG_CLIENT_OUTPUT="$path"
|
|
}
|
|
|
|
client_generate_config() {
|
|
local outdir
|
|
outdir=$(dirname "$CFG_CLIENT_OUTPUT")
|
|
if [[ ! -d "$outdir" ]]; then
|
|
mkdir -p "$outdir" 2>/dev/null || {
|
|
tui_msgbox "Error" "Cannot create directory: ${outdir}"
|
|
return 1
|
|
}
|
|
fi
|
|
write_client_json_config "$CFG_CLIENT_OUTPUT"
|
|
chmod 640 "$CFG_CLIENT_OUTPUT" 2>/dev/null || true
|
|
}
|
|
|
|
client_show_summary() {
|
|
local uri
|
|
uri=$(generate_ss_uri "$CFG_CLIENT_METHOD" "$CFG_CLIENT_PASSWORD" "$CFG_CLIENT_SERVER" "$CFG_CLIENT_SERVER_PORT" "$CFG_CLIENT_PLUGIN" "$CFG_CLIENT_PLUGIN_OPTS")
|
|
|
|
local summary=""
|
|
summary+="Config file: ${CFG_CLIENT_OUTPUT}\n"
|
|
summary+="\n"
|
|
summary+="Server: ${CFG_CLIENT_SERVER}:${CFG_CLIENT_SERVER_PORT}\n"
|
|
summary+="Local: 127.0.0.1:${CFG_CLIENT_LOCAL_PORT}\n"
|
|
summary+="Method: ${CFG_CLIENT_METHOD}\n"
|
|
if [[ -n "$CFG_CLIENT_PLUGIN" ]]; then
|
|
summary+="Plugin: ${CFG_CLIENT_PLUGIN}\n"
|
|
[[ -n "$CFG_CLIENT_PLUGIN_OPTS" ]] && summary+="Opts: ${CFG_CLIENT_PLUGIN_OPTS}\n"
|
|
fi
|
|
summary+="\n"
|
|
summary+="ss:// URI:\n${uri}\n"
|
|
summary+="\n"
|
|
summary+="Usage:\n ss-local -c ${CFG_CLIENT_OUTPUT}\n"
|
|
|
|
tui_msgbox "Client Config Complete" "$summary"
|
|
}
|
|
|
|
client_setup() {
|
|
client_ask_server_address || return
|
|
client_ask_server_port || return
|
|
client_ask_local_port || return
|
|
client_ask_method || return
|
|
client_ask_password || return
|
|
client_ask_plugin || return
|
|
client_ask_output_path || return
|
|
|
|
# Confirm
|
|
local confirm_msg=""
|
|
confirm_msg+="Server: ${CFG_CLIENT_SERVER}:${CFG_CLIENT_SERVER_PORT}\n"
|
|
confirm_msg+="Local: 127.0.0.1:${CFG_CLIENT_LOCAL_PORT}\n"
|
|
confirm_msg+="Method: ${CFG_CLIENT_METHOD}\n"
|
|
confirm_msg+="Output: ${CFG_CLIENT_OUTPUT}\n"
|
|
[[ -n "$CFG_CLIENT_PLUGIN" ]] && confirm_msg+="Plugin: ${CFG_CLIENT_PLUGIN}\n"
|
|
|
|
if ! tui_yesno "Confirm" "Review your settings:\n\n${confirm_msg}\nProceed?"; then
|
|
return
|
|
fi
|
|
|
|
client_generate_config || return
|
|
client_show_summary
|
|
}
|
|
|
|
###############################################################################
|
|
# Plugin Installation Flow
|
|
###############################################################################
|
|
|
|
plugin_download_github_release() {
|
|
local repo="$1" binary_name="$2"
|
|
local os arch api_url assets_json download_url asset_name
|
|
|
|
os=$(detect_os)
|
|
arch=$(detect_arch)
|
|
|
|
if ! command -v curl >/dev/null 2>&1; then
|
|
tui_msgbox "Error" "curl is required for downloading plugins."
|
|
return 1
|
|
fi
|
|
|
|
api_url="https://api.github.com/repos/${repo}/releases/latest"
|
|
|
|
tui_msgbox "Downloading" "Fetching latest release info from:\n${repo}\n\nPlease wait..."
|
|
|
|
assets_json=$(curl -sS --connect-timeout 15 --max-time 30 "$api_url") || {
|
|
tui_msgbox "Error" "Failed to fetch release info from GitHub.\n\nCheck your network connection."
|
|
return 1
|
|
}
|
|
|
|
# Find matching asset URL -- look for os and arch in filename
|
|
download_url=""
|
|
# Try common naming patterns
|
|
local patterns=()
|
|
patterns+=("${os}-${arch}")
|
|
patterns+=("${os}_${arch}")
|
|
# Map arch names for different projects
|
|
case "$arch" in
|
|
amd64) patterns+=("${os}-amd64" "${os}_amd64" "${os}-x86_64" "${os}_x86_64" "linux-64") ;;
|
|
arm64) patterns+=("${os}-arm64" "${os}_arm64" "${os}-aarch64" "${os}_aarch64") ;;
|
|
esac
|
|
|
|
local p
|
|
for p in "${patterns[@]}"; do
|
|
download_url=$(echo "$assets_json" | grep -o '"browser_download_url"[[:space:]]*:[[:space:]]*"[^"]*'"$p"'[^"]*"' | head -1 | sed 's/.*"\(http[^"]*\)".*/\1/')
|
|
[[ -n "$download_url" ]] && break
|
|
done
|
|
|
|
if [[ -z "$download_url" ]]; then
|
|
tui_msgbox "Error" "Could not find a matching release for ${os}-${arch}.\n\nYou may need to build from source or download manually."
|
|
return 1
|
|
fi
|
|
|
|
asset_name=$(basename "$download_url")
|
|
|
|
TMPDIR_CLEANUP=$(mktemp -d)
|
|
local tmpdir="$TMPDIR_CLEANUP"
|
|
|
|
tui_msgbox "Downloading" "Downloading:\n${asset_name}\n\nThis may take a moment..."
|
|
|
|
if ! curl -sSL --connect-timeout 15 --max-time 120 -o "${tmpdir}/${asset_name}" "$download_url"; then
|
|
tui_msgbox "Error" "Download failed."
|
|
return 1
|
|
fi
|
|
|
|
# Extract based on file type
|
|
local extract_dir="${tmpdir}/extract"
|
|
mkdir -p "$extract_dir"
|
|
|
|
case "$asset_name" in
|
|
*.tar.gz|*.tgz)
|
|
tar xzf "${tmpdir}/${asset_name}" -C "$extract_dir" || {
|
|
tui_msgbox "Error" "Failed to extract archive."
|
|
return 1
|
|
}
|
|
;;
|
|
*.zip)
|
|
unzip -q "${tmpdir}/${asset_name}" -d "$extract_dir" || {
|
|
tui_msgbox "Error" "Failed to extract archive."
|
|
return 1
|
|
}
|
|
;;
|
|
*)
|
|
# Assume it's the binary itself
|
|
cp "${tmpdir}/${asset_name}" "${extract_dir}/${binary_name}"
|
|
chmod +x "${extract_dir}/${binary_name}"
|
|
;;
|
|
esac
|
|
|
|
# Find the binary
|
|
local found_binary=""
|
|
found_binary=$(find "$extract_dir" -name "$binary_name" -type f 2>/dev/null | head -1)
|
|
|
|
# Some plugins have different names in the archive
|
|
if [[ -z "$found_binary" ]]; then
|
|
# Try finding any executable
|
|
found_binary=$(find "$extract_dir" -type f -executable 2>/dev/null | head -1)
|
|
fi
|
|
if [[ -z "$found_binary" ]]; then
|
|
found_binary=$(find "$extract_dir" -type f -name "${binary_name}*" 2>/dev/null | head -1)
|
|
fi
|
|
|
|
if [[ -z "$found_binary" ]]; then
|
|
tui_msgbox "Error" "Could not find '${binary_name}' in the downloaded archive.\n\nContents of archive:\n$(ls -la "$extract_dir" 2>/dev/null)"
|
|
return 1
|
|
fi
|
|
|
|
chmod +x "$found_binary"
|
|
cp "$found_binary" "/usr/local/bin/${binary_name}"
|
|
|
|
if command -v "$binary_name" >/dev/null 2>&1; then
|
|
tui_msgbox "Success" "${binary_name} installed to /usr/local/bin/${binary_name}"
|
|
return 0
|
|
else
|
|
tui_msgbox "Warning" "Installed to /usr/local/bin/${binary_name} but it was not found in PATH."
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
plugin_install_simple_obfs() {
|
|
local distro
|
|
distro=$(detect_distro)
|
|
|
|
local method
|
|
method=$(tui_menu "Install simple-obfs" "Choose installation method:" \
|
|
"package" "Install via package manager (if available)" \
|
|
"source" "Build from source (requires git, build tools)") || return 1
|
|
|
|
if [[ "$method" == "package" ]]; then
|
|
case "$distro" in
|
|
ubuntu|debian)
|
|
apt-get update && apt-get install -y simple-obfs && {
|
|
tui_msgbox "Success" "simple-obfs installed via apt."
|
|
return 0
|
|
}
|
|
tui_msgbox "Notice" "Package not available. Trying source build..."
|
|
;;
|
|
*)
|
|
tui_msgbox "Notice" "No package available for ${distro}. Building from source..."
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Build from source
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
tui_msgbox "Error" "git is required to build from source."
|
|
return 1
|
|
fi
|
|
if ! command -v make >/dev/null 2>&1; then
|
|
tui_msgbox "Error" "make is required to build from source."
|
|
return 1
|
|
fi
|
|
|
|
TMPDIR_CLEANUP=$(mktemp -d)
|
|
local tmpdir="$TMPDIR_CLEANUP"
|
|
|
|
tui_msgbox "Building" "Cloning and building simple-obfs...\nThis may take a few minutes."
|
|
|
|
(
|
|
cd "$tmpdir"
|
|
git clone https://github.com/shadowsocks/simple-obfs.git
|
|
cd simple-obfs
|
|
git submodule update --init --recursive
|
|
./autogen.sh
|
|
./configure
|
|
make
|
|
make install
|
|
) || {
|
|
tui_msgbox "Error" "Build failed. Check that build dependencies are installed:\n autoconf, automake, libtool, libev-dev"
|
|
return 1
|
|
}
|
|
|
|
if command -v obfs-local >/dev/null 2>&1; then
|
|
tui_msgbox "Success" "simple-obfs built and installed.\nBinaries: obfs-local, obfs-server"
|
|
else
|
|
tui_msgbox "Warning" "Build completed but obfs-local not found in PATH."
|
|
fi
|
|
}
|
|
|
|
plugin_install_custom() {
|
|
local path
|
|
path=$(tui_inputbox "Custom Plugin" "Enter the full path to the plugin binary:" "") || return 1
|
|
|
|
if [[ -z "$path" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -f "$path" ]]; then
|
|
tui_msgbox "Error" "File not found: ${path}"
|
|
return 1
|
|
fi
|
|
|
|
if [[ ! -x "$path" ]]; then
|
|
chmod +x "$path"
|
|
fi
|
|
|
|
local name
|
|
name=$(basename "$path")
|
|
|
|
if tui_yesno "Symlink" "Create a symlink in /usr/local/bin/${name}?"; then
|
|
ln -sf "$path" "/usr/local/bin/${name}"
|
|
tui_msgbox "Done" "Symlinked ${path} → /usr/local/bin/${name}"
|
|
else
|
|
tui_msgbox "Done" "Plugin at ${path} is ready to use.\nSpecify the full path when configuring."
|
|
fi
|
|
}
|
|
|
|
plugin_install() {
|
|
if ! is_root; then
|
|
tui_msgbox "Root Required" "Plugin installation requires root privileges.\n\nRe-run with sudo."
|
|
return
|
|
fi
|
|
|
|
local items=()
|
|
local p
|
|
for p in "${KNOWN_PLUGINS[@]}"; do
|
|
local status="not installed"
|
|
if command -v "$p" >/dev/null 2>&1; then
|
|
status="installed"
|
|
fi
|
|
items+=("$p" "$(plugin_repo "$p") (${status})")
|
|
done
|
|
items+=("custom" "Install a custom binary")
|
|
|
|
local choice
|
|
choice=$(tui_menu "Install Plugin" "Select a plugin to install:" "${items[@]}") || return
|
|
|
|
case "$choice" in
|
|
simple-obfs)
|
|
plugin_install_simple_obfs
|
|
;;
|
|
v2ray-plugin)
|
|
plugin_download_github_release "$(plugin_repo v2ray-plugin)" "v2ray-plugin"
|
|
;;
|
|
xray-plugin)
|
|
plugin_download_github_release "$(plugin_repo xray-plugin)" "xray-plugin"
|
|
;;
|
|
kcptun)
|
|
plugin_download_github_release "$(plugin_repo kcptun)" "kcptun-client"
|
|
;;
|
|
custom)
|
|
plugin_install_custom
|
|
;;
|
|
esac
|
|
}
|
|
|
|
###############################################################################
|
|
# Service Management
|
|
###############################################################################
|
|
|
|
service_list_instances() {
|
|
local instances=()
|
|
|
|
# Scan config files
|
|
if [[ -d "$CONFIG_DIR" ]]; then
|
|
local f name
|
|
for f in "${CONFIG_DIR}"/*.json; do
|
|
[[ -f "$f" ]] || continue
|
|
name=$(basename "$f" .json)
|
|
local status="no service"
|
|
local unit="shadowsocks-libev-server@${name}.service"
|
|
if has_systemd; then
|
|
if systemctl is-active --quiet "$unit" 2>/dev/null; then
|
|
status="running"
|
|
elif systemctl is-enabled --quiet "$unit" 2>/dev/null; then
|
|
status="stopped (enabled)"
|
|
fi
|
|
fi
|
|
instances+=("$name" "${status}")
|
|
done
|
|
fi
|
|
|
|
if [[ ${#instances[@]} -eq 0 ]]; then
|
|
tui_msgbox "No Instances" "No config files found in ${CONFIG_DIR}/"
|
|
return 1
|
|
fi
|
|
|
|
local choice
|
|
choice=$(tui_menu "Instances" "Select an instance:" "${instances[@]}") || return 1
|
|
echo "$choice"
|
|
}
|
|
|
|
service_action_menu() {
|
|
local name="$1"
|
|
local unit="shadowsocks-libev-server@${name}.service"
|
|
|
|
while true; do
|
|
local action
|
|
action=$(tui_menu "Manage: ${name}" "Select an action:" \
|
|
"status" "View service status" \
|
|
"start" "Start the service" \
|
|
"stop" "Stop the service" \
|
|
"restart" "Restart the service" \
|
|
"enable" "Enable on boot" \
|
|
"disable" "Disable on boot" \
|
|
"logs" "View recent logs" \
|
|
"back" "Return to instance list") || return
|
|
|
|
case "$action" in
|
|
status)
|
|
local st
|
|
st=$(systemctl status "$unit" 2>&1 || true)
|
|
tui_msgbox "Status: ${name}" "$st"
|
|
;;
|
|
start)
|
|
systemctl start "$unit" 2>&1 && \
|
|
tui_msgbox "Started" "Service ${unit} started." || \
|
|
tui_msgbox "Error" "Failed to start ${unit}."
|
|
;;
|
|
stop)
|
|
systemctl stop "$unit" 2>&1 && \
|
|
tui_msgbox "Stopped" "Service ${unit} stopped." || \
|
|
tui_msgbox "Error" "Failed to stop ${unit}."
|
|
;;
|
|
restart)
|
|
systemctl restart "$unit" 2>&1 && \
|
|
tui_msgbox "Restarted" "Service ${unit} restarted." || \
|
|
tui_msgbox "Error" "Failed to restart ${unit}."
|
|
;;
|
|
enable)
|
|
systemctl enable "$unit" 2>&1 && \
|
|
tui_msgbox "Enabled" "Service ${unit} enabled on boot." || \
|
|
tui_msgbox "Error" "Failed to enable ${unit}."
|
|
;;
|
|
disable)
|
|
systemctl disable "$unit" 2>&1 && \
|
|
tui_msgbox "Disabled" "Service ${unit} disabled." || \
|
|
tui_msgbox "Error" "Failed to disable ${unit}."
|
|
;;
|
|
logs)
|
|
local logs
|
|
logs=$(journalctl -u "$unit" -n 50 --no-pager 2>&1 || echo "(no logs available)")
|
|
tui_msgbox "Logs: ${name}" "$logs"
|
|
;;
|
|
back)
|
|
return
|
|
;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
service_manage() {
|
|
if ! is_root; then
|
|
tui_msgbox "Root Required" "Service management requires root privileges.\n\nRe-run with sudo."
|
|
return
|
|
fi
|
|
|
|
if ! has_systemd; then
|
|
tui_msgbox "No systemd" "systemd was not detected on this system.\n\nService management is not available."
|
|
return
|
|
fi
|
|
|
|
while true; do
|
|
local instance
|
|
instance=$(service_list_instances) || return
|
|
service_action_menu "$instance"
|
|
done
|
|
}
|
|
|
|
###############################################################################
|
|
# Main Menu
|
|
###############################################################################
|
|
|
|
main_menu() {
|
|
while true; do
|
|
local choice
|
|
choice=$(tui_menu "ss-setup v${SS_SETUP_VERSION}" "shadowsocks-libev setup tool" \
|
|
"server" "Setup ss-server (generate config + service)" \
|
|
"client" "Generate ss-local client config" \
|
|
"plugin" "Install a SIP003 plugin" \
|
|
"service" "Manage running services" \
|
|
"exit" "Exit") || break
|
|
|
|
case "$choice" in
|
|
server) server_setup ;;
|
|
client) client_setup ;;
|
|
plugin) plugin_install ;;
|
|
service) service_manage ;;
|
|
exit) break ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
###############################################################################
|
|
# Entry Point
|
|
###############################################################################
|
|
|
|
main() {
|
|
# Non-interactive check
|
|
if [[ ! -t 0 ]]; then
|
|
echo "Error: ss-setup requires an interactive terminal." >&2
|
|
echo "Usage: ss-setup" >&2
|
|
exit 1
|
|
fi
|
|
|
|
detect_tui_backend
|
|
check_root || true # warn but continue
|
|
main_menu
|
|
|
|
echo "Goodbye."
|
|
}
|
|
|
|
# Only run main when executed directly (not when sourced for testing)
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
main "$@"
|
|
fi
|