#!/bin/bash # ========================================= # 作者: jinqians # 日期: 2025年2月 # 网站:jinqians.com # 描述: 这个脚本用于安装、卸载、查看和更新 Snell 代理 # ========================================= # 定义颜色代码 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' RESET='\033[0m' #当前版本号 current_version="5.2" # 全局变量:选择的 Snell 版本 SNELL_VERSION_CHOICE="" SNELL_VERSION="" # === 新增:版本选择函数 === # 选择 Snell 版本 select_snell_version() { echo -e "${CYAN}请选择要安装的 Snell 版本:${RESET}" echo -e "${GREEN}1.${RESET} Snell v4" echo -e "${GREEN}2.${RESET} Snell v5" echo -e "${GREEN}3.${RESET} Snell v6 (Beta)" while true; do read -rp "请输入选项 [1-3]: " version_choice case "$version_choice" in 1) SNELL_VERSION_CHOICE="v4" echo -e "${GREEN}已选择 Snell v4${RESET}" break ;; 2) SNELL_VERSION_CHOICE="v5" echo -e "${GREEN}已选择 Snell v5${RESET}" break ;; 3) SNELL_VERSION_CHOICE="v6" echo -e "${GREEN}已选择 Snell v6 (Beta)${RESET}" echo -e "${YELLOW}注意:v6 为 Beta 版本,协议可能存在不兼容更新${RESET}" break ;; *) echo -e "${RED}请输入正确的选项 [1-3]${RESET}" ;; esac done } # 获取 Snell v4 最新版本 get_latest_snell_v4_version() { latest_version=$(curl -s https://manual.nssurge.com/others/snell.html | grep -oP 'snell-server-v\K4\.[0-9]+\.[0-9]+' | head -n 1) if [ -z "$latest_version" ]; then latest_version=$(curl -s https://kb.nssurge.com/surge-knowledge-base/zh/release-notes/snell | grep -oP 'snell-server-v\K4\.[0-9]+\.[0-9]+' | head -n 1) fi if [ -n "$latest_version" ]; then echo "v${latest_version}" else echo "v4.1.1" fi } # 获取 Snell v5 最新版本 get_latest_snell_v5_version() { # 先抓 beta 版 v5_beta=$(curl -s https://manual.nssurge.com/others/snell.html | grep -oP 'snell-server-v\K5\.[0-9]+\.[0-9]+b[0-9]+' | head -n 1) if [ -z "$v5_beta" ]; then v5_beta=$(curl -s https://kb.nssurge.com/surge-knowledge-base/zh/release-notes/snell | grep -oP 'snell-server-v\K5\.[0-9]+\.[0-9]+b[0-9]+' | head -n 1) fi if [ -n "$v5_beta" ]; then echo "v${v5_beta}" return fi # 再抓正式版,过滤掉带 b 的 beta 版本 v5_release=$(curl -s https://manual.nssurge.com/others/snell.html | grep -oP 'snell-server-v\K5\.[0-9]+\.[0-9]+[a-z0-9]*' | grep -v b | head -n 1) if [ -z "$v5_release" ]; then v5_release=$(curl -s https://kb.nssurge.com/surge-knowledge-base/zh/release-notes/snell | grep -oP 'snell-server-v\K5\.[0-9]+\.[0-9]+[a-z0-9]*' | grep -v b | head -n 1) fi if [ -n "$v5_release" ]; then echo "v${v5_release}" else echo "v5.0.0" fi } # 获取 Snell v6 最新版本 get_latest_snell_v6_version() { local v6_ver v6_ver=$(curl -s https://kb.nssurge.com/surge-knowledge-base/release-notes/snell | grep -oP 'snell-server-v\K6\.[0-9]+\.[0-9]+[a-z0-9]*' | head -n 1) if [ -n "$v6_ver" ]; then echo "v${v6_ver}" else echo "v6.0.0b4" fi } # 获取 Snell 最新版本(根据选择的版本) get_latest_snell_version() { if [ "$SNELL_VERSION_CHOICE" = "v6" ]; then SNELL_VERSION=$(get_latest_snell_v6_version) elif [ "$SNELL_VERSION_CHOICE" = "v5" ]; then SNELL_VERSION=$(get_latest_snell_v5_version) else SNELL_VERSION=$(get_latest_snell_v4_version) fi } # 获取 Snell 下载 URL get_snell_download_url() { local arch=$(uname -m) # v6 暂不提供 armv7l 构建 if [ "$SNELL_VERSION_CHOICE" = "v6" ] && { [ "$arch" = "armv7l" ] || [ "$arch" = "armv7" ]; }; then echo -e "${RED}Snell v6 暂不支持 armv7l 架构${RESET}" >&2 exit 1 fi case ${arch} in "x86_64"|"amd64") echo "https://dl.nssurge.com/snell/snell-server-${SNELL_VERSION}-linux-amd64.zip" ;; "i386"|"i686") echo "https://dl.nssurge.com/snell/snell-server-${SNELL_VERSION}-linux-i386.zip" ;; "aarch64"|"arm64") echo "https://dl.nssurge.com/snell/snell-server-${SNELL_VERSION}-linux-aarch64.zip" ;; "armv7l"|"armv7") echo "https://dl.nssurge.com/snell/snell-server-${SNELL_VERSION}-linux-armv7l.zip" ;; *) echo -e "${RED}不支持的架构: ${arch}${RESET}" >&2 exit 1 ;; esac } # 生成 Surge 配置格式 generate_surge_config() { local ip_addr=$1 local port=$2 local psk=$3 local version=$4 local country=$5 local installed_version=$6 if [ "$installed_version" = "v6" ]; then # v6 版本:v6 协议(已移除 QUIC 模式) echo -e "${GREEN}${country} = snell, ${ip_addr}, ${port}, psk = ${psk}, version = 6, reuse = true, tfo = true${RESET}" elif [ "$installed_version" = "v5" ]; then # v5 版本输出 v4 和 v5 两种配置 echo -e "${GREEN}${country} = snell, ${ip_addr}, ${port}, psk = ${psk}, version = 4, reuse = true, tfo = true${RESET}" echo -e "${GREEN}${country} = snell, ${ip_addr}, ${port}, psk = ${psk}, version = 5, reuse = true, tfo = true${RESET}" else # v4 版本只输出 v4 配置 echo -e "${GREEN}${country} = snell, ${ip_addr}, ${port}, psk = ${psk}, version = 4, reuse = true, tfo = true${RESET}" fi } # 检测当前安装的 Snell 版本 detect_installed_snell_version() { if command -v snell-server &> /dev/null; then local version_output=$(snell-server --v 2>&1) if echo "$version_output" | grep -q "v6"; then echo "v6" elif echo "$version_output" | grep -q "v5"; then echo "v5" else echo "v4" fi else echo "unknown" fi } # === 新增:备份和还原配置函数 === # 备份 Snell 配置 backup_snell_config() { local backup_dir="/etc/snell/backup_$(date +%Y%m%d_%H%M%S)" mkdir -p "$backup_dir" cp -a /etc/snell/users/*.conf "$backup_dir"/ 2>/dev/null echo "$backup_dir" } # 恢复 Snell 配置 restore_snell_config() { local backup_dir="$1" if [ -d "$backup_dir" ]; then cp -a "$backup_dir"/*.conf /etc/snell/users/ echo -e "${GREEN}配置已从备份恢复。${RESET}" else echo -e "${RED}未找到备份目录,无法恢复配置。${RESET}" fi } # 检查 bc 是否安装 check_bc() { if ! command -v bc &> /dev/null; then echo -e "${YELLOW}未检测到 bc,正在安装...${RESET}" # 根据系统类型安装 bc if [ -x "$(command -v apt)" ]; then wait_for_apt apt update && apt install -y bc elif [ -x "$(command -v yum)" ]; then yum install -y bc else echo -e "${RED}未支持的包管理器,无法安装 bc。请手动安装 bc。${RESET}" exit 1 fi fi } # 检查 curl 是否安装 check_curl() { if ! command -v curl &> /dev/null; then echo -e "${YELLOW}未检测到 curl,正在安装...${RESET}" if [ -x "$(command -v apt)" ]; then wait_for_apt apt update && apt install -y curl elif [ -x "$(command -v yum)" ]; then yum install -y curl else echo -e "${RED}未支持的包管理器,无法安装 curl。请手动安装 curl。${RESET}" exit 1 fi fi } # 定义系统路径 INSTALL_DIR="/usr/local/bin" SYSTEMD_DIR="/etc/systemd/system" SNELL_CONF_DIR="/etc/snell" SNELL_CONF_FILE="${SNELL_CONF_DIR}/users/snell-main.conf" SYSTEMD_SERVICE_FILE="${SYSTEMD_DIR}/snell.service" SYSTEMD_SOCKET_FILE="${SYSTEMD_DIR}/snell.socket" SYSTEMD_NETNS_FILE="${SYSTEMD_DIR}/snell-netns.service" NETNS_SETUP_SCRIPT="${INSTALL_DIR}/snell-netns-setup.sh" # 出口控制(netns + socket activation)默认参数 EGRESS_FEATURE_ENABLED="false" EGRESS_IFACE="" EGRESS_NS="snell-egress" EGRESS_HOST_IP="" EGRESS_NS_IP="" EGRESS_SUBNET="" EGRESS_GW="" # 旧的配置文件路径(用于兼容性检查) OLD_SNELL_CONF_FILE="${SNELL_CONF_DIR}/snell-server.conf" OLD_SYSTEMD_SERVICE_FILE="/lib/systemd/system/snell.service" SNELL_SERVICE_USER="snell" SNELL_SERVICE_GROUP="snell" ensure_snell_service_user() { if ! getent group "${SNELL_SERVICE_GROUP}" >/dev/null 2>&1; then groupadd --system "${SNELL_SERVICE_GROUP}" 2>/dev/null || true fi if ! getent passwd "${SNELL_SERVICE_USER}" >/dev/null 2>&1; then useradd --system --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin --gid "${SNELL_SERVICE_GROUP}" "${SNELL_SERVICE_USER}" 2>/dev/null || \ useradd -r -M -s /usr/sbin/nologin -g "${SNELL_SERVICE_GROUP}" "${SNELL_SERVICE_USER}" 2>/dev/null || true fi } ensure_snell_config_dir() { ensure_snell_service_user mkdir -p "${SNELL_CONF_DIR}/users" if getent group "${SNELL_SERVICE_GROUP}" >/dev/null 2>&1 && getent passwd "${SNELL_SERVICE_USER}" >/dev/null 2>&1; then chown -R "${SNELL_SERVICE_USER}:${SNELL_SERVICE_GROUP}" "${SNELL_CONF_DIR}" 2>/dev/null || true fi chmod 755 "${SNELL_CONF_DIR}" "${SNELL_CONF_DIR}/users" 2>/dev/null || true } migrate_legacy_main_config_if_needed() { ensure_snell_config_dir if [ -f "$SNELL_CONF_FILE" ]; then return 0 fi if [ -f "$OLD_SNELL_CONF_FILE" ]; then cp -a "$OLD_SNELL_CONF_FILE" "$SNELL_CONF_FILE" if getent group "${SNELL_SERVICE_GROUP}" >/dev/null 2>&1 && getent passwd "${SNELL_SERVICE_USER}" >/dev/null 2>&1; then chown "${SNELL_SERVICE_USER}:${SNELL_SERVICE_GROUP}" "$SNELL_CONF_FILE" 2>/dev/null || true fi chmod 644 "$SNELL_CONF_FILE" echo -e "${GREEN}已将旧配置迁移到 ${SNELL_CONF_FILE}${RESET}" return 0 fi return 1 } validate_snell_main_config() { migrate_legacy_main_config_if_needed || true if [ ! -s "$SNELL_CONF_FILE" ]; then echo -e "${RED}主配置文件不存在: ${SNELL_CONF_FILE}${RESET}" echo -e "${YELLOW}请先执行安装,或将旧配置放到该路径后再启动服务。${RESET}" return 1 fi if ! grep -Eq '^[[:space:]]*listen[[:space:]]*=' "$SNELL_CONF_FILE"; then echo -e "${RED}主配置缺少 listen 配置: ${SNELL_CONF_FILE}${RESET}" return 1 fi if ! grep -Eq '^[[:space:]]*psk[[:space:]]*=' "$SNELL_CONF_FILE"; then echo -e "${RED}主配置缺少 psk 配置: ${SNELL_CONF_FILE}${RESET}" return 1 fi return 0 } write_main_systemd_service() { ensure_snell_config_dir cat > ${SYSTEMD_SERVICE_FILE} << EOF [Unit] Description=Snell Proxy Service (Main) After=network.target [Service] Type=simple User=${SNELL_SERVICE_USER} Group=${SNELL_SERVICE_GROUP} LimitNOFILE=32768 ExecStart=${INSTALL_DIR}/snell-server -c ${SNELL_CONF_FILE} AmbientCapabilities=CAP_NET_BIND_SERVICE Restart=on-failure RestartSec=2s StandardOutput=journal StandardError=journal SyslogIdentifier=snell-server [Install] WantedBy=multi-user.target EOF } sync_existing_main_service_unit() { if [ ! -f "$SYSTEMD_SERVICE_FILE" ]; then return 0 fi if systemctl is-enabled --quiet snell.socket 2>/dev/null; then return 0 fi if grep -q "NetworkNamespacePath=" "$SYSTEMD_SERVICE_FILE"; then return 0 fi if ! grep -q "ExecStart=${INSTALL_DIR}/snell-server -c ${SNELL_CONF_FILE}" "$SYSTEMD_SERVICE_FILE"; then return 0 fi if grep -q "StandardOutput=syslog\\|StandardError=syslog\\|User=nobody\\|Group=nogroup" "$SYSTEMD_SERVICE_FILE"; then write_main_systemd_service systemctl daemon-reload 2>/dev/null || true echo -e "${GREEN}已更新 snell.service systemd 配置。${RESET}" fi } # 根据 /30 子网生成 host/ns 地址与网关 apply_egress_subnet() { local subnet="$1" local base prefix base="${subnet%/30}" prefix="${base%.*}" EGRESS_SUBNET="$subnet" EGRESS_HOST_IP="${prefix}.1/30" EGRESS_NS_IP="${prefix}.2/30" EGRESS_GW="${prefix}.1" } # 自动选择未占用的 /30 子网(默认池:172.31.0.0/16) auto_pick_egress_subnet() { local i candidate if ! command -v ip &> /dev/null; then apply_egress_subnet "172.31.0.0/30" return fi for i in $(seq 0 255); do candidate="172.31.${i}.0/30" if ip -o -4 addr show | grep -q "172\\.31\\.${i}\\."; then continue fi if ip -4 route show | grep -q "172\\.31\\.${i}\\."; then continue fi apply_egress_subnet "$candidate" return done apply_egress_subnet "172.31.0.0/30" } # 初始化默认网段 auto_pick_egress_subnet # 自动检测默认出口网卡 auto_detect_egress_iface() { local detected_iface if command -v ip &> /dev/null; then detected_iface=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}') fi if [ -n "$detected_iface" ]; then EGRESS_IFACE="$detected_iface" elif [ -z "$EGRESS_IFACE" ]; then EGRESS_IFACE="eth1" fi } # 初始化默认出口网卡 auto_detect_egress_iface # 检查并迁移旧配置 check_and_migrate_config() { local need_migration=false local old_files_exist=false # 自动修复 4.x -> 5.x 后服务指向新路径、配置仍在旧路径导致的启动失败。 if [ ! -f "$SNELL_CONF_FILE" ] && [ -f "$OLD_SNELL_CONF_FILE" ]; then migrate_legacy_main_config_if_needed if [ -f "$SYSTEMD_SERVICE_FILE" ] && ! systemctl is-enabled --quiet snell.socket 2>/dev/null; then write_main_systemd_service systemctl daemon-reload 2>/dev/null || true fi fi # 检查仍需人工处理的旧配置。若主配置已自动迁移成功,仅保留旧文件不再反复提示。 if { [ ! -f "$SNELL_CONF_FILE" ] && [ -f "$OLD_SNELL_CONF_FILE" ]; } || [ -f "$OLD_SYSTEMD_SERVICE_FILE" ]; then old_files_exist=true echo -e "\n${YELLOW}检测到旧版本的 Snell 配置文件${RESET}" echo -e "旧配置位置:" [ -f "$OLD_SNELL_CONF_FILE" ] && echo -e "- 配置文件:${OLD_SNELL_CONF_FILE}" [ -f "$OLD_SYSTEMD_SERVICE_FILE" ] && echo -e "- 服务文件:${OLD_SYSTEMD_SERVICE_FILE}" # 检查用户目录是否存在 if [ ! -d "${SNELL_CONF_DIR}/users" ]; then need_migration=true mkdir -p "${SNELL_CONF_DIR}/users" # 设置正确的目录权限 ensure_snell_service_user chown -R "${SNELL_SERVICE_USER}:${SNELL_SERVICE_GROUP}" "${SNELL_CONF_DIR}" chmod -R 755 "${SNELL_CONF_DIR}" fi fi # 如果需要迁移,询问用户 if [ "$old_files_exist" = true ]; then echo -e "\n${YELLOW}是否要迁移旧的配置文件?[y/N]${RESET}" read -r choice if [[ "$choice" == "y" || "$choice" == "Y" ]]; then echo -e "${CYAN}开始迁移配置文件...${RESET}" # 停止服务 systemctl stop snell 2>/dev/null # 迁移配置文件 if [ -f "$OLD_SNELL_CONF_FILE" ]; then cp "$OLD_SNELL_CONF_FILE" "${SNELL_CONF_FILE}" # 设置正确的文件权限 ensure_snell_service_user chown "${SNELL_SERVICE_USER}:${SNELL_SERVICE_GROUP}" "${SNELL_CONF_FILE}" chmod 644 "${SNELL_CONF_FILE}" echo -e "${GREEN}已迁移配置文件${RESET}" fi # 迁移服务文件 if [ -f "$OLD_SYSTEMD_SERVICE_FILE" ]; then write_main_systemd_service echo -e "${GREEN}已迁移服务文件${RESET}" fi # 询问是否删除旧文件 echo -e "${YELLOW}是否删除旧的配置文件?[y/N]${RESET}" read -r del_choice if [[ "$del_choice" == "y" || "$del_choice" == "Y" ]]; then [ -f "$OLD_SNELL_CONF_FILE" ] && rm -f "$OLD_SNELL_CONF_FILE" [ -f "$OLD_SYSTEMD_SERVICE_FILE" ] && rm -f "$OLD_SYSTEMD_SERVICE_FILE" echo -e "${GREEN}已删除旧的配置文件${RESET}" fi # 重新加载服务 systemctl daemon-reload if validate_snell_main_config; then systemctl start snell fi # 验证服务状态 if systemctl is-active --quiet snell; then echo -e "${GREEN}配置迁移完成,服务已成功启动${RESET}" else echo -e "${RED}警告:服务启动失败,请检查配置文件和权限${RESET}" systemctl status snell fi else echo -e "${YELLOW}跳过配置迁移${RESET}" fi fi } # 自动更新脚本 auto_update_script() { echo -e "${CYAN}正在检查脚本更新...${RESET}" # 创建临时文件 TMP_SCRIPT=$(mktemp) # 下载最新版本 if curl -sL https://raw.githubusercontent.com/jinqians/snell.sh/main/snell.sh -o "$TMP_SCRIPT"; then # 获取新版本号 new_version=$(grep "current_version=" "$TMP_SCRIPT" | cut -d'"' -f2) # 比较版本号 if [ "$new_version" != "$current_version" ]; then echo -e "${GREEN}发现新版本:${new_version}${RESET}" echo -e "${YELLOW}当前版本:${current_version}${RESET}" # 备份当前脚本 cp "$0" "${0}.backup" # 更新脚本 mv "$TMP_SCRIPT" "$0" chmod +x "$0" echo -e "${GREEN}脚本已更新到最新版本${RESET}" echo -e "${YELLOW}已备份原脚本到:${0}.backup${RESET}" # 提示用户重新运行脚本 echo -e "${CYAN}请重新运行脚本以使用新版本${RESET}" exit 0 else echo -e "${GREEN}当前已是最新版本 (${current_version})${RESET}" rm -f "$TMP_SCRIPT" fi else echo -e "${RED}检查更新失败,请检查网络连接${RESET}" rm -f "$TMP_SCRIPT" fi } # 等待其他 apt 进程完成 wait_for_apt() { while fuser /var/lib/dpkg/lock >/dev/null 2>&1; do echo -e "${YELLOW}等待其他 apt 进程完成...${RESET}" sleep 1 done } # 检查是否以 root 权限运行 check_root() { if [ "$(id -u)" != "0" ]; then echo -e "${RED}请以 root 权限运行此脚本.${RESET}" exit 1 fi } check_root # 检查 jq 是否安装 check_jq() { if ! command -v jq &> /dev/null; then echo -e "${YELLOW}未检测到 jq,正在安装...${RESET}" # 根据系统类型安装 jq if [ -x "$(command -v apt)" ]; then wait_for_apt apt update && apt install -y jq elif [ -x "$(command -v yum)" ]; then yum install -y jq else echo -e "${RED}未支持的包管理器,无法安装 jq。请手动安装 jq。${RESET}" exit 1 fi fi } check_jq # 检查 Snell 是否已安装 check_snell_installed() { if command -v snell-server &> /dev/null; then return 0 else return 1 fi } # 比较版本号 version_greater_equal() { local ver1=$1 local ver2=$2 # 移除 'v' 或 'V' 前缀,并转换为小写 ver1=$(echo "${ver1#[vV]}" | tr '[:upper:]' '[:lower:]') ver2=$(echo "${ver2#[vV]}" | tr '[:upper:]' '[:lower:]') # 处理 beta 版本号(如 5.0.0b1, 5.0.0b2) # 将 beta 版本转换为可比较的格式 ver1=$(echo "$ver1" | sed 's/b\([0-9]*\)/\.999\1/g') ver2=$(echo "$ver2" | sed 's/b\([0-9]*\)/\.999\1/g') # 将版本号分割为数组 IFS='.' read -ra VER1 <<< "$ver1" IFS='.' read -ra VER2 <<< "$ver2" # 确保数组长度相等 while [ ${#VER1[@]} -lt 4 ]; do VER1+=("0") done while [ ${#VER2[@]} -lt 4 ]; do VER2+=("0") done # 比较版本号 for i in {0..3}; do local val1=${VER1[i]:-0} local val2=${VER2[i]:-0} # 如果是数字,直接比较 if [[ "$val1" =~ ^[0-9]+$ ]] && [[ "$val2" =~ ^[0-9]+$ ]]; then if [ "$val1" -gt "$val2" ]; then return 0 elif [ "$val1" -lt "$val2" ]; then return 1 fi else # 如果是字符串(如 beta 版本),按字典序比较 if [[ "$val1" > "$val2" ]]; then return 0 elif [[ "$val1" < "$val2" ]]; then return 1 fi fi done return 0 } # 用户输入端口号,范围 1-65535 get_user_port() { while true; do read -rp "请输入要使用的端口号 (1-65535): " PORT if [[ "$PORT" =~ ^[0-9]+$ ]] && [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ]; then echo -e "${GREEN}已选择端口: $PORT${RESET}" break else echo -e "${RED}无效端口号,请输入 1 到 65535 之间的数字。${RESET}" fi done } # 获取系统DNS get_system_dns() { # 尝试从resolv.conf获取系统DNS if [ -f "/etc/resolv.conf" ]; then system_dns=$(grep -E '^nameserver' /etc/resolv.conf | awk '{print $2}' | tr '\n' ',' | sed 's/,$//') if [ ! -z "$system_dns" ]; then echo "$system_dns" return 0 fi fi # 如果无法从resolv.conf获取,尝试使用公共DNS echo "1.1.1.1,8.8.8.8" } # 获取用户输入的 DNS 服务器 get_dns() { read -rp "请输入 DNS 服务器地址 (直接回车使用系统DNS): " custom_dns if [ -z "$custom_dns" ]; then DNS=$(get_system_dns) echo -e "${GREEN}使用系统 DNS 服务器: $DNS${RESET}" else DNS=$custom_dns echo -e "${GREEN}使用自定义 DNS 服务器: $DNS${RESET}" fi } # 是否启用 IPv6 get_ipv6_choice() { IPV6_ENABLE="true" LISTEN_ADDR="::0" read -rp "是否启用 IPv6? [Y/n]: " ipv6_choice case "$ipv6_choice" in [nN]|[nN][oO]) IPV6_ENABLE="false" LISTEN_ADDR="0.0.0.0" echo -e "${GREEN}已关闭 IPv6,仅监听 IPv4${RESET}" ;; *) echo -e "${GREEN}已启用 IPv6${RESET}" ;; esac } # 是否启用 Snell v5/v6 出口控制 get_egress_feature_choice() { EGRESS_FEATURE_ENABLED="false" if [ "$SNELL_VERSION_CHOICE" != "v5" ] && [ "$SNELL_VERSION_CHOICE" != "v6" ]; then return fi echo -e "${CYAN}是否启用 Snell ${SNELL_VERSION_CHOICE} 出口控制(netns + socket activation)?${RESET}" echo -e "${GREEN}1.${RESET} 启用(新特性)" echo -e "${GREEN}2.${RESET} 不启用(推荐)" while true; do read -rp "请输入选项 [1-2]: " egress_choice case "$egress_choice" in 1) EGRESS_FEATURE_ENABLED="true" echo -e "${GREEN}已启用 Snell v5 出口控制${RESET}" break ;; 2) EGRESS_FEATURE_ENABLED="false" echo -e "${YELLOW}已选择传统模式${RESET}" break ;; *) echo -e "${RED}请输入正确的选项 [1-2]${RESET}" ;; esac done } # 获取出口控制相关参数 get_egress_settings() { if [ "$EGRESS_FEATURE_ENABLED" != "true" ]; then return fi auto_detect_egress_iface read -rp "请输入出口接口名称(默认 ${EGRESS_IFACE}): " custom_iface if [ -n "$custom_iface" ]; then EGRESS_IFACE="$custom_iface" fi read -rp "请输入 netns 名称(默认 snell-egress): " custom_ns if [ -n "$custom_ns" ]; then EGRESS_NS="$custom_ns" fi # 自动探测默认子网,并允许用户手工覆盖 auto_pick_egress_subnet read -rp "请输入 veth 子网(CIDR,默认 ${EGRESS_SUBNET}): " custom_subnet if [ -n "$custom_subnet" ]; then if [[ "$custom_subnet" =~ ^([0-9]{1,3}\.){3}0/30$ ]]; then apply_egress_subnet "$custom_subnet" else echo -e "${YELLOW}子网格式无效,继续使用自动选择:${EGRESS_SUBNET}${RESET}" fi fi echo -e "${GREEN}出口接口: ${EGRESS_IFACE}${RESET}" echo -e "${GREEN}命名空间: ${EGRESS_NS}${RESET}" echo -e "${GREEN}veth 子网: ${EGRESS_SUBNET}${RESET}" echo -e "${YELLOW}说明:${EGRESS_HOST_IP}(主命名空间) <-> ${EGRESS_NS_IP}(${EGRESS_NS})${RESET}" } # 检查出口控制依赖 check_egress_dependencies() { if [ "$EGRESS_FEATURE_ENABLED" != "true" ]; then return fi if ! command -v ip &> /dev/null; then echo -e "${YELLOW}未检测到 iproute2,正在安装...${RESET}" if [ -x "$(command -v apt)" ]; then wait_for_apt apt update && apt install -y iproute2 elif [ -x "$(command -v yum)" ]; then yum install -y iproute else echo -e "${RED}未支持的包管理器,无法安装 iproute2。${RESET}" exit 1 fi fi if ! command -v nft &> /dev/null; then echo -e "${YELLOW}未检测到 nftables,正在安装...${RESET}" if [ -x "$(command -v apt)" ]; then wait_for_apt apt update && apt install -y nftables elif [ -x "$(command -v yum)" ]; then yum install -y nftables else echo -e "${RED}未支持的包管理器,无法安装 nftables。${RESET}" exit 1 fi fi } # 写入 netns 初始化单元 write_snell_netns_service() { cat > ${NETNS_SETUP_SCRIPT} << EOF #!/bin/bash set -eux ip netns add ${EGRESS_NS} 2>/dev/null || true ip link show veth-host >/dev/null 2>&1 || ip link add veth-host type veth peer name veth-snell ip link set veth-snell netns ${EGRESS_NS} 2>/dev/null || true ip addr replace ${EGRESS_HOST_IP} dev veth-host ip link set veth-host up ip netns exec ${EGRESS_NS} ip addr replace ${EGRESS_NS_IP} dev veth-snell ip netns exec ${EGRESS_NS} ip link set lo up ip netns exec ${EGRESS_NS} ip link set veth-snell up ip netns exec ${EGRESS_NS} ip route replace default via ${EGRESS_GW} mkdir -p /etc/netns/${EGRESS_NS} cp -f /etc/resolv.conf /etc/netns/${EGRESS_NS}/resolv.conf if grep -Eq '^nameserver[[:space:]]+127\\.0\\.0\\.53$' /etc/netns/${EGRESS_NS}/resolv.conf; then printf 'nameserver 1.1.1.1\nnameserver 8.8.8.8\n' > /etc/netns/${EGRESS_NS}/resolv.conf fi sysctl -w net.ipv4.ip_forward=1 nft delete table ip snell_nat 2>/dev/null || true nft add table ip snell_nat nft add chain ip snell_nat postrouting '{ type nat hook postrouting priority 100; policy accept; }' nft add rule ip snell_nat postrouting oifname "${EGRESS_IFACE}" ip saddr ${EGRESS_SUBNET} masquerade nft add table inet snell_filter 2>/dev/null || true nft list chain inet snell_filter forward >/dev/null 2>&1 || nft add chain inet snell_filter forward '{ type filter hook forward priority -5; policy accept; }' nft add rule inet snell_filter forward iifname 'veth-host' oifname "${EGRESS_IFACE}" ip saddr ${EGRESS_SUBNET} accept 2>/dev/null || true nft add rule inet snell_filter forward iifname "${EGRESS_IFACE}" oifname 'veth-host' ct state established,related accept 2>/dev/null || true if command -v iptables >/dev/null 2>&1; then iptables -C FORWARD -i veth-host -o ${EGRESS_IFACE} -s ${EGRESS_SUBNET} -j ACCEPT 2>/dev/null || iptables -I FORWARD -i veth-host -o ${EGRESS_IFACE} -s ${EGRESS_SUBNET} -j ACCEPT iptables -C FORWARD -i ${EGRESS_IFACE} -o veth-host -d ${EGRESS_SUBNET} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null || iptables -I FORWARD -i ${EGRESS_IFACE} -o veth-host -d ${EGRESS_SUBNET} -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT fi EOF chmod +x ${NETNS_SETUP_SCRIPT} cat > ${SYSTEMD_NETNS_FILE} << EOF [Unit] Description=Prepare netns and NAT for Snell egress DefaultDependencies=no After=network-online.target Wants=network-online.target [Service] Type=oneshot RemainAfterExit=yes ExecStart=${NETNS_SETUP_SCRIPT} ExecStop=/bin/true [Install] WantedBy=multi-user.target EOF } # 写入 socket activation 单元 write_snell_socket_service_units() { local listen_port=$1 cat > ${SYSTEMD_SOCKET_FILE} << EOF [Unit] Description=Snell v5 (socket-activated) [Socket] ListenStream=0.0.0.0:${listen_port} ListenDatagram=0.0.0.0:${listen_port} FileDescriptorName=snell_inet ReusePort=no NoDelay=true [Install] WantedBy=sockets.target EOF cat > ${SYSTEMD_SERVICE_FILE} << EOF [Unit] Description=Snell Proxy Service (Main, netns) Requires=snell-netns.service After=snell-netns.service [Service] Type=simple NetworkNamespacePath=/run/netns/${EGRESS_NS} BindReadOnlyPaths=/etc/netns/${EGRESS_NS}/resolv.conf:/etc/resolv.conf User=${SNELL_SERVICE_USER} Group=${SNELL_SERVICE_GROUP} NoNewPrivileges=yes PrivateTmp=yes ProtectSystem=strict ProtectHome=yes ProtectKernelTunables=yes ProtectControlGroups=yes ProtectKernelModules=yes LimitNOFILE=32768 AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE WorkingDirectory=${INSTALL_DIR} ExecStart=${INSTALL_DIR}/snell-server -c ${SNELL_CONF_FILE} Restart=on-failure RestartSec=2s StandardOutput=journal StandardError=journal SyslogIdentifier=snell-server [Install] WantedBy=multi-user.target EOF } # 检查端口是否被占用(TCP/UDP) is_port_in_use() { local port="$1" if command -v ss &> /dev/null; then ss -H -ltn "( sport = :${port} )" 2>/dev/null | grep -q . && return 0 ss -H -lun "( sport = :${port} )" 2>/dev/null | grep -q . && return 0 return 1 fi if command -v lsof &> /dev/null; then lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 && return 0 lsof -nP -iUDP:"${port}" >/dev/null 2>&1 return $? fi return 1 } # 显示占用指定端口的进程信息 show_port_occupier() { local port="$1" if command -v ss &> /dev/null; then ss -ltnp "( sport = :${port} )" 2>/dev/null | sed 's/^/ /' ss -lunp "( sport = :${port} )" 2>/dev/null | sed 's/^/ /' return fi if command -v lsof &> /dev/null; then lsof -nP -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null | sed 's/^/ /' lsof -nP -iUDP:"${port}" 2>/dev/null | sed 's/^/ /' fi } # 按端口强制清理监听进程(优先清理 snell 相关,最后兜底清理全部监听者) force_release_port_by_pid() { local port="$1" local pids pid cmd if command -v ss &> /dev/null; then pids=$( { ss -H -ltnp "( sport = :${port} )" 2>/dev/null ss -H -lunp "( sport = :${port} )" 2>/dev/null } | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' | sort -u) elif command -v lsof &> /dev/null; then pids=$( { lsof -t -nP -iTCP:"${port}" -sTCP:LISTEN 2>/dev/null lsof -t -nP -iUDP:"${port}" 2>/dev/null } | sort -u) fi [ -z "$pids" ] && return 0 for pid in $pids; do cmd=$(ps -p "$pid" -o args= 2>/dev/null) if echo "$cmd" | grep -q "snell"; then kill -TERM "$pid" 2>/dev/null || true fi done sleep 0.2 if is_port_in_use "$port"; then for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true done fi } # 切换到 socket activation 前,确保主端口已释放 ensure_main_port_free_for_socket() { local port="$1" local i systemctl stop snell.socket 2>/dev/null systemctl stop snell 2>/dev/null systemctl disable snell 2>/dev/null systemctl reset-failed snell.socket 2>/dev/null # 兜底:避免残留 snell-server 进程继续占用端口 systemctl kill snell --signal=SIGKILL 2>/dev/null pkill -f "${INSTALL_DIR}/snell-server -c ${SNELL_CONF_FILE}" 2>/dev/null || true force_release_port_by_pid "$port" for i in {1..20}; do if ! is_port_in_use "$port"; then return 0 fi sleep 0.2 done echo -e "${RED}端口 ${port} 仍被占用,无法启动 snell.socket。${RESET}" echo -e "${YELLOW}占用详情:${RESET}" show_port_occupier "$port" return 1 } # 保存 nftables 规则 save_nftables_rules() { if ! command -v nft &> /dev/null; then return fi if [ -f "/etc/nftables.conf" ]; then nft list ruleset > /etc/nftables.conf 2>/dev/null || true systemctl enable nftables >/dev/null 2>&1 || true echo -e "${GREEN}nftables 规则已保存${RESET}" elif [ -f "/etc/sysconfig/nftables.conf" ]; then nft list ruleset > /etc/sysconfig/nftables.conf 2>/dev/null || true systemctl enable nftables >/dev/null 2>&1 || true echo -e "${GREEN}nftables 规则已保存${RESET}" else echo -e "${YELLOW}未找到 nftables 持久化配置文件,端口规则已在当前运行环境生效${RESET}" fi } # 在 nftables 中开放端口 open_nftables_port() { local PORT=$1 local chains local chain_opened=false if ! command -v nft &> /dev/null; then return fi echo -e "${CYAN}在 nftables 中开放端口 $PORT${RESET}" chains=$(nft -a list ruleset 2>/dev/null | awk ' $1 == "table" { family=$2 table=$3 gsub(/[{}]/, "", table) } $1 == "chain" { chain=$2 gsub(/[{}]/, "", chain) in_chain=1 next } in_chain && /type filter/ && /hook input/ { print family " " table " " chain } in_chain && /^[[:space:]]*}/ { in_chain=0 } ') while read -r family table chain; do [ -z "$family" ] && continue if ! nft list chain "$family" "$table" "$chain" 2>/dev/null | grep -q "tcp dport $PORT .*accept"; then nft insert rule "$family" "$table" "$chain" tcp dport "$PORT" accept 2>/dev/null || true fi if ! nft list chain "$family" "$table" "$chain" 2>/dev/null | grep -q "udp dport $PORT .*accept"; then nft insert rule "$family" "$table" "$chain" udp dport "$PORT" accept 2>/dev/null || true fi chain_opened=true done << EOF $chains EOF if [ "$chain_opened" = false ]; then nft add table inet snell_filter 2>/dev/null || true nft list chain inet snell_filter input >/dev/null 2>&1 || nft add chain inet snell_filter input '{ type filter hook input priority -5; policy accept; }' if ! nft list chain inet snell_filter input 2>/dev/null | grep -q "tcp dport $PORT .*accept"; then nft add rule inet snell_filter input tcp dport "$PORT" accept 2>/dev/null || true fi if ! nft list chain inet snell_filter input 2>/dev/null | grep -q "udp dport $PORT .*accept"; then nft add rule inet snell_filter input udp dport "$PORT" accept 2>/dev/null || true fi fi save_nftables_rules } # 开放端口 (ufw、nftables 和 iptables) open_port() { local PORT=$1 local ufw_active=false # 检查 ufw 是否已安装 if command -v ufw &> /dev/null; then echo -e "${CYAN}在 UFW 中开放端口 $PORT${RESET}" ufw allow "$PORT"/tcp ufw allow "$PORT"/udp if ufw status 2>/dev/null | grep -qw "active"; then ufw_active=true fi fi # 检查 iptables 是否已安装 if command -v iptables &> /dev/null; then echo -e "${CYAN}在 iptables 中开放端口 $PORT${RESET}" iptables -I INPUT -p tcp --dport "$PORT" -j ACCEPT iptables -I INPUT -p udp --dport "$PORT" -j ACCEPT # 创建 iptables 规则保存目录(如果不存在) if [ ! -d "/etc/iptables" ]; then mkdir -p /etc/iptables fi # 尝试保存规则,如果失败则不中断脚本 iptables-save > /etc/iptables/rules.v4 || true fi if [ "$ufw_active" = false ]; then open_nftables_port "$PORT" fi } close_nftables_port() { local PORT=$1 if ! command -v nft &> /dev/null; then return fi nft -a list ruleset 2>/dev/null | awk -v port="$PORT" ' $1 == "table" { family=$2 table=$3 gsub(/[{}]/, "", table) } $1 == "chain" { chain=$2 gsub(/[{}]/, "", chain) } ($0 ~ "tcp dport " port " .*accept" || $0 ~ "udp dport " port " .*accept") && /# handle/ { handle=$NF print family " " table " " chain " " handle } ' | while read -r family table chain handle; do [ -z "$handle" ] && continue nft delete rule "$family" "$table" "$chain" handle "$handle" 2>/dev/null || true done save_nftables_rules } close_port() { local PORT=$1 if command -v ufw &> /dev/null; then ufw delete allow "$PORT"/tcp >/dev/null 2>&1 || true ufw delete allow "$PORT"/udp >/dev/null 2>&1 || true fi if command -v iptables &> /dev/null; then iptables -D INPUT -p tcp --dport "$PORT" -j ACCEPT 2>/dev/null || true iptables -D INPUT -p udp --dport "$PORT" -j ACCEPT 2>/dev/null || true if [ -d "/etc/iptables" ]; then iptables-save > /etc/iptables/rules.v4 2>/dev/null || true fi fi close_nftables_port "$PORT" } # 启用 egress 运行时:优先 socket + service;失败回退为 netns 直启服务 start_egress_runtime() { local port="$1" if ! ensure_main_port_free_for_socket "$port"; then return 1 fi if ! systemctl enable snell.socket; then echo -e "${RED}启用 snell.socket 失败。${RESET}" return 1 fi if ! systemctl start snell.socket; then echo -e "${RED}启动 snell.socket 失败。${RESET}" return 1 fi # 关键:主动拉起 snell,确保 UDP/QUIC 可用,不依赖首次 TCP 触发 if systemctl start snell; then echo -e "${GREEN}已启用 socket + service 运行模式。${RESET}" return 0 fi echo -e "${YELLOW}socket 模式下主动拉起 snell 失败,自动回退到 netns 直启服务模式。${RESET}" systemctl stop snell.socket 2>/dev/null systemctl disable snell.socket 2>/dev/null if ! systemctl enable snell; then echo -e "${RED}回退模式:启用 snell 失败。${RESET}" return 1 fi if ! systemctl restart snell; then echo -e "${RED}回退模式:启动 snell 失败。${RESET}" return 1 fi echo -e "${GREEN}已回退为 netns 直启服务模式(无 socket 激活)。${RESET}" return 0 } # 安装 Snell install_snell() { echo -e "${CYAN}正在安装 Snell${RESET}" # 选择 Snell 版本 select_snell_version wait_for_apt apt update && apt install -y wget unzip get_latest_snell_version ARCH=$(uname -m) SNELL_URL=$(get_snell_download_url "$SNELL_VERSION_CHOICE") echo -e "${CYAN}正在下载 Snell ${SNELL_VERSION_CHOICE} (${SNELL_VERSION})...${RESET}" echo -e "${YELLOW}下载链接: ${SNELL_URL}${RESET}" # v4/v5/v6 均使用 zip 格式,统一处理 wget ${SNELL_URL} -O snell-server.zip if [ $? -ne 0 ]; then echo -e "${RED}下载 Snell ${SNELL_VERSION_CHOICE} 失败。${RESET}" exit 1 fi unzip -o snell-server.zip -d ${INSTALL_DIR} if [ $? -ne 0 ]; then echo -e "${RED}解压缩 Snell 失败。${RESET}" exit 1 fi rm snell-server.zip chmod +x ${INSTALL_DIR}/snell-server get_user_port # 获取用户输入的端口 get_dns # 获取用户输入的 DNS 服务器 get_ipv6_choice # 是否启用 IPv6 get_egress_feature_choice get_egress_settings check_egress_dependencies PSK=$(tr -dc A-Za-z0-9 ${SNELL_CONF_FILE} << EOF [snell-server] listen = ${LISTEN_ADDR}:${PORT} psk = ${PSK} ipv6 = ${IPV6_ENABLE} dns = ${DNS} EOF write_main_systemd_service if [ "$EGRESS_FEATURE_ENABLED" = "true" ]; then write_snell_netns_service write_snell_socket_service_units "$PORT" fi systemctl daemon-reload if [ $? -ne 0 ]; then echo -e "${RED}重载 Systemd 配置失败。${RESET}" exit 1 fi if [ "$EGRESS_FEATURE_ENABLED" = "true" ]; then systemctl enable snell-netns if [ $? -ne 0 ]; then echo -e "${RED}启用 snell-netns 失败。${RESET}" exit 1 fi systemctl start snell-netns if [ $? -ne 0 ]; then echo -e "${RED}启动 snell-netns 失败。${RESET}" exit 1 fi if ! start_egress_runtime "$PORT"; then exit 1 fi echo -e "${GREEN}snell.socket 已启动,snell 服务将按需拉起(首次连接触发)${RESET}" echo -e "${YELLOW}建议客户端优先使用 version = 4(v5 的 QUIC/UDP 依赖更高)。${RESET}" else systemctl stop snell.socket 2>/dev/null systemctl disable snell.socket 2>/dev/null systemctl stop snell-netns 2>/dev/null systemctl disable snell-netns 2>/dev/null systemctl enable snell if [ $? -ne 0 ]; then echo -e "${RED}开机自启动 Snell 失败。${RESET}" exit 1 fi if ! validate_snell_main_config; then exit 1 fi systemctl start snell if [ $? -ne 0 ]; then echo -e "${RED}启动 Snell 服务失败。${RESET}" exit 1 fi fi # 开放端口 open_port "$PORT" # 在安装完成后输出配置信息 echo -e "\n${GREEN}安装完成!以下是您的配置信息:${RESET}" echo -e "${CYAN}--------------------------------${RESET}" if [ "$EGRESS_FEATURE_ENABLED" = "true" ]; then echo -e "${YELLOW}出口控制: 已启用(接口 ${EGRESS_IFACE},命名空间 ${EGRESS_NS})${RESET}" echo -e "${YELLOW}Socket 激活: snell.socket${RESET}" fi echo -e "${YELLOW}监听端口: ${PORT}${RESET}" echo -e "${YELLOW}PSK 密钥: ${PSK}${RESET}" echo -e "${YELLOW}IPv6: true${RESET}" echo -e "${YELLOW}DNS 服务器: ${DNS}${RESET}" echo -e "${CYAN}--------------------------------${RESET}" # 获取并显示服务器IP地址 echo -e "\n${GREEN}服务器地址信息:${RESET}" # 获取 IPv4 地址 IPV4_ADDR=$(curl -s4 --connect-timeout 5 --max-time 10 https://api.ipify.org) if [ $? -eq 0 ] && [ ! -z "$IPV4_ADDR" ]; then IP_COUNTRY_IPV4=$(curl -s --connect-timeout 5 --max-time 10 http://ipinfo.io/${IPV4_ADDR}/country) echo -e "${GREEN}IPv4 地址: ${RESET}${IPV4_ADDR} ${GREEN}所在国家: ${RESET}${IP_COUNTRY_IPV4}" fi # 获取 IPv6 地址 IPV6_ADDR=$(curl -s6 --connect-timeout 5 --max-time 10 https://api64.ipify.org) if [ $? -eq 0 ] && [ ! -z "$IPV6_ADDR" ]; then IP_COUNTRY_IPV6=$(curl -s --connect-timeout 5 --max-time 10 https://ipapi.co/${IPV6_ADDR}/country/) echo -e "${GREEN}IPv6 地址: ${RESET}${IPV6_ADDR} ${GREEN}所在国家: ${RESET}${IP_COUNTRY_IPV6}" fi # 输出 Surge 配置格式 echo -e "\n${GREEN}Surge 配置格式:${RESET}" local installed_version=$(detect_installed_snell_version) if [ ! -z "$IPV4_ADDR" ]; then generate_surge_config "$IPV4_ADDR" "$PORT" "$PSK" "$SNELL_VERSION_CHOICE" "$IP_COUNTRY_IPV4" "$installed_version" fi if [ ! -z "$IPV6_ADDR" ]; then generate_surge_config "$IPV6_ADDR" "$PORT" "$PSK" "$SNELL_VERSION_CHOICE" "$IP_COUNTRY_IPV6" "$installed_version" fi # 创建管理脚本 echo -e "${CYAN}正在安装管理脚本...${RESET}" # 确保目标目录存在 mkdir -p /usr/local/bin # 创建管理脚本 cat > /usr/local/bin/snell << 'EOFSCRIPT' #!/bin/bash # 定义颜色代码 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' RESET='\033[0m' # 检查是否以 root 权限运行 if [ "$(id -u)" != "0" ]; then echo -e "${RED}请以 root 权限运行此脚本${RESET}" exit 1 fi # 下载并执行最新版本的脚本 echo -e "${CYAN}正在获取最新版本的管理脚本...${RESET}" TMP_SCRIPT=$(mktemp) if curl -sL https://raw.githubusercontent.com/jinqians/snell.sh/main/snell.sh -o "$TMP_SCRIPT"; then bash "$TMP_SCRIPT" rm -f "$TMP_SCRIPT" else echo -e "${RED}下载脚本失败,请检查网络连接。${RESET}" rm -f "$TMP_SCRIPT" exit 1 fi EOFSCRIPT if [ $? -eq 0 ]; then chmod +x /usr/local/bin/snell if [ $? -eq 0 ]; then echo -e "\n${GREEN}管理脚本安装成功!${RESET}" echo -e "${YELLOW}您可以在终端输入 'snell' 进入管理菜单。${RESET}" echo -e "${YELLOW}注意:需要使用 sudo snell 或以 root 身份运行。${RESET}\n" else echo -e "\n${RED}设置脚本执行权限失败。${RESET}" echo -e "${YELLOW}您可以通过直接运行原脚本来管理 Snell。${RESET}\n" fi else echo -e "\n${RED}创建管理脚本失败。${RESET}" echo -e "${YELLOW}您可以通过直接运行原脚本来管理 Snell。${RESET}\n" fi } # 已安装 Snell v5/v6 的出口控制管理 configure_v5_egress_control() { echo -e "${CYAN}=============== v5/v6 出口控制设置 ===============${RESET}" if ! command -v snell-server &> /dev/null; then echo -e "${RED}未检测到 Snell,请先安装。${RESET}" return 1 fi local installed_version installed_version=$(detect_installed_snell_version) if [ "$installed_version" != "v5" ] && [ "$installed_version" != "v6" ]; then echo -e "${YELLOW}当前安装版本为 ${installed_version},仅 Snell v5/v6 支持此设置。${RESET}" return 1 fi local main_port main_port=$(get_snell_port) if [ -z "$main_port" ]; then echo -e "${RED}未找到主配置端口,请检查 ${SNELL_CONF_FILE}${RESET}" return 1 fi local egress_enabled="false" if systemctl is-enabled snell.socket &> /dev/null || systemctl is-active snell.socket &> /dev/null; then egress_enabled="true" fi echo -e "${GREEN}当前版本: Snell v5${RESET}" echo -e "${GREEN}主端口: ${main_port}${RESET}" if [ "$egress_enabled" = "true" ]; then echo -e "${YELLOW}当前出口控制状态: 已启用${RESET}" else echo -e "${YELLOW}当前出口控制状态: 未启用${RESET}" fi echo -e "${GREEN}1.${RESET} 启用/更新 出口控制" echo -e "${GREEN}2.${RESET} 关闭 出口控制(恢复传统模式)" echo -e "${GREEN}0.${RESET} 返回" read -rp "请输入选项 [0-2]: " egress_manage_choice case "$egress_manage_choice" in 1) EGRESS_FEATURE_ENABLED="true" get_egress_settings check_egress_dependencies write_snell_netns_service write_snell_socket_service_units "$main_port" systemctl daemon-reload if ! systemctl enable snell-netns; then echo -e "${RED}启用 snell-netns 失败。${RESET}" return 1 fi if ! systemctl start snell-netns; then echo -e "${RED}启动 snell-netns 失败,请执行: systemctl status snell-netns.service${RESET}" return 1 fi if ! start_egress_runtime "$main_port"; then return 1 fi echo -e "${GREEN}已应用出口控制(接口 ${EGRESS_IFACE},命名空间 ${EGRESS_NS})。${RESET}" echo -e "${YELLOW}建议客户端优先使用 version = 4(v5 的 QUIC/UDP 依赖更高)。${RESET}" echo -e "${YELLOW}说明:snell.socket 已监听,snell.service 将在首次连接时自动启动。${RESET}" ;; 2) systemctl stop snell.socket 2>/dev/null systemctl disable snell.socket 2>/dev/null systemctl stop snell-netns 2>/dev/null systemctl disable snell-netns 2>/dev/null write_main_systemd_service rm -f ${SYSTEMD_SOCKET_FILE} rm -f ${SYSTEMD_NETNS_FILE} systemctl daemon-reload systemctl enable snell if ! validate_snell_main_config; then return 1 fi systemctl restart snell echo -e "${GREEN}已关闭出口控制,恢复传统模式。${RESET}" ;; 0) echo -e "${CYAN}已返回。${RESET}" ;; *) echo -e "${RED}请输入正确的选项 [0-2]${RESET}" ;; esac } # 只更新 Snell 二进制文件,不覆盖配置 update_snell_binary() { echo -e "${CYAN}=============== Snell 更新 ===============${RESET}" echo -e "${YELLOW}注意:这是更新操作,不是重新安装${RESET}" echo -e "${GREEN}✓ 所有现有配置将被保留${RESET}" echo -e "${GREEN}✓ 端口、密码、用户配置都不会改变${RESET}" echo -e "${GREEN}✓ 服务会自动重启${RESET}" echo -e "${CYAN}============================================${RESET}" echo -e "${CYAN}正在备份当前配置...${RESET}" local backup_dir backup_dir=$(backup_snell_config) echo -e "${GREEN}配置已备份到: $backup_dir${RESET}" echo -e "${CYAN}正在更新 Snell 二进制文件...${RESET}" # 获取最新版本信息(版本已在 check_snell_update 中确定) get_latest_snell_version ARCH=$(uname -m) SNELL_URL=$(get_snell_download_url "$SNELL_VERSION_CHOICE") echo -e "${CYAN}正在下载 Snell ${SNELL_VERSION_CHOICE} (${SNELL_VERSION})...${RESET}" # v4/v5/v6 均使用 zip 格式,统一处理 wget ${SNELL_URL} -O snell-server.zip if [ $? -ne 0 ]; then echo -e "${RED}下载 Snell ${SNELL_VERSION_CHOICE} 失败。${RESET}" restore_snell_config "$backup_dir" exit 1 fi echo -e "${CYAN}正在替换 Snell 二进制文件...${RESET}" unzip -o snell-server.zip -d ${INSTALL_DIR} if [ $? -ne 0 ]; then echo -e "${RED}解压缩 Snell 失败。${RESET}" restore_snell_config "$backup_dir" exit 1 fi rm snell-server.zip chmod +x ${INSTALL_DIR}/snell-server echo -e "${CYAN}正在重启 Snell 服务...${RESET}" if ! validate_snell_main_config; then restore_snell_config "$backup_dir" return 1 fi # 重启主服务 systemctl restart snell if [ $? -ne 0 ]; then echo -e "${RED}主服务重启失败,尝试恢复配置...${RESET}" restore_snell_config "$backup_dir" if validate_snell_main_config; then systemctl restart snell fi fi # 重启所有多用户服务 if [ -d "${SNELL_CONF_DIR}/users" ]; then for user_conf in "${SNELL_CONF_DIR}/users"/*; do if [ -f "$user_conf" ] && [[ "$user_conf" != *"snell-main.conf" ]]; then local port=$(grep -E '^listen' "$user_conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') if [ ! -z "$port" ]; then systemctl restart "snell-${port}" 2>/dev/null fi fi done fi echo -e "${CYAN}============================================${RESET}" echo -e "${GREEN}✅ Snell 更新完成!${RESET}" echo -e "${GREEN}✓ 版本已更新到: ${SNELL_VERSION_CHOICE} (${SNELL_VERSION})${RESET}" echo -e "${GREEN}✓ 所有配置已保留${RESET}" echo -e "${GREEN}✓ 服务已重启${RESET}" echo -e "${YELLOW}配置备份目录: $backup_dir${RESET}" echo -e "${CYAN}============================================${RESET}" } # 卸载 Snell uninstall_snell() { echo -e "${CYAN}正在卸载 Snell${RESET}" # 停止并删除依赖 Snell 后端的 ShadowTLS 服务,避免留下无后端的监听服务 local snell_shadowtls_services snell_shadowtls_services=$(find "${SYSTEMD_DIR}" -maxdepth 1 -name "shadowtls-snell-*.service" 2>/dev/null) if [ -n "$snell_shadowtls_services" ]; then while IFS= read -r service_file; do [ -z "$service_file" ] && continue local service_name service_name=$(basename "$service_file") local shadowtls_port shadowtls_port=$(sed -n 's/.*--listen .*:\([0-9][0-9]*\).*/\1/p' "$service_file" | head -n 1) echo -e "${YELLOW}正在停止 ShadowTLS 服务 (${service_name})${RESET}" systemctl stop "$service_name" 2>/dev/null systemctl disable "$service_name" 2>/dev/null rm -f "$service_file" if [ -n "$shadowtls_port" ]; then close_port "$shadowtls_port" fi done <<< "$snell_shadowtls_services" fi # 停止并禁用主服务 systemctl stop snell 2>/dev/null systemctl disable snell 2>/dev/null systemctl stop snell.socket 2>/dev/null systemctl disable snell.socket 2>/dev/null systemctl stop snell-netns 2>/dev/null systemctl disable snell-netns 2>/dev/null # 停止并禁用所有多用户服务 if [ -d "${SNELL_CONF_DIR}/users" ]; then for user_conf in "${SNELL_CONF_DIR}/users"/*; do if [ -f "$user_conf" ]; then local port=$(grep -E '^listen' "$user_conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') if [ ! -z "$port" ]; then echo -e "${YELLOW}正在停止用户服务 (端口: $port)${RESET}" systemctl stop "snell-${port}" 2>/dev/null systemctl disable "snell-${port}" 2>/dev/null rm -f "${SYSTEMD_DIR}/snell-${port}.service" close_port "$port" fi fi done fi # 删除服务文件 rm -f /lib/systemd/system/snell.service rm -f ${SYSTEMD_SERVICE_FILE} rm -f ${SYSTEMD_SOCKET_FILE} rm -f ${SYSTEMD_NETNS_FILE} rm -f ${NETNS_SETUP_SCRIPT} # 删除可执行文件和配置目录 rm -f /usr/local/bin/snell-server rm -rf ${SNELL_CONF_DIR} rm -f /usr/local/bin/snell # 删除管理脚本 if ! find "${SYSTEMD_DIR}" -maxdepth 1 -name "shadowtls-*.service" 2>/dev/null | grep -q .; then rm -f /usr/local/bin/shadow-tls fi # 重载 systemd 配置 systemctl daemon-reload echo -e "${GREEN}Snell 及其所有多用户配置已成功卸载${RESET}" } # 重启 Snell restart_snell() { echo -e "${YELLOW}正在重启所有 Snell 服务...${RESET}" if ! validate_snell_main_config; then echo -e "${RED}已取消重启,避免 snell-server 在缺少配置时崩溃。${RESET}" return 1 fi # 若使用 socket activation,先重启 socket 与 netns,再重启服务 if systemctl list-unit-files | grep -q '^snell.socket'; then systemctl restart snell-netns 2>/dev/null systemctl restart snell.socket 2>/dev/null fi # 重启主服务 systemctl restart snell if [ $? -eq 0 ]; then echo -e "${GREEN}主 Snell 服务已成功重启。${RESET}" else echo -e "${RED}重启主 Snell 服务失败。${RESET}" fi # 重启所有多用户服务 if [ -d "${SNELL_CONF_DIR}/users" ]; then for user_conf in "${SNELL_CONF_DIR}/users"/*; do if [ -f "$user_conf" ] && [[ "$user_conf" != *"snell-main.conf" ]]; then local port=$(grep -E '^listen' "$user_conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') if [ ! -z "$port" ]; then echo -e "${YELLOW}正在重启用户服务 (端口: $port)${RESET}" systemctl restart "snell-${port}" 2>/dev/null if [ $? -eq 0 ]; then echo -e "${GREEN}用户服务 (端口: $port) 已成功重启。${RESET}" else echo -e "${RED}重启用户服务 (端口: $port) 失败。${RESET}" fi fi fi done fi } # 检查服务状态并显示 check_and_show_status() { echo -e "\n${CYAN}=============== 服务状态检查 ===============${RESET}" # 检查 Snell 状态 if command -v snell-server &> /dev/null; then # 初始化计数器和资源使用变量 local user_count=0 local running_count=0 local total_snell_memory=0 local total_snell_cpu=0 # 检查主服务状态 local main_available=false if systemctl is-active snell &> /dev/null; then main_available=true elif systemctl is-active snell.socket &> /dev/null; then # socket activation 场景下,服务可能按需拉起 main_available=true fi if [ "$main_available" = "true" ]; then user_count=$((user_count + 1)) running_count=$((running_count + 1)) # 获取主服务资源使用情况 local main_pid=$(systemctl show -p MainPID snell | cut -d'=' -f2) if [ ! -z "$main_pid" ] && [ "$main_pid" != "0" ]; then local mem=$(ps -o rss= -p $main_pid 2>/dev/null) local cpu=$(ps -o %cpu= -p $main_pid 2>/dev/null) if [ ! -z "$mem" ]; then total_snell_memory=$((total_snell_memory + mem)) fi if [ ! -z "$cpu" ]; then total_snell_cpu=$(echo "$total_snell_cpu + $cpu" | bc -l) fi fi else user_count=$((user_count + 1)) fi # 检查多用户状态 if [ -d "${SNELL_CONF_DIR}/users" ]; then for user_conf in "${SNELL_CONF_DIR}/users"/*; do if [ -f "$user_conf" ] && [[ "$user_conf" != *"snell-main.conf" ]]; then local port=$(grep -E '^listen' "$user_conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') if [ ! -z "$port" ]; then user_count=$((user_count + 1)) if systemctl is-active --quiet "snell-${port}"; then running_count=$((running_count + 1)) # 获取用户服务资源使用情况 local user_pid=$(systemctl show -p MainPID "snell-${port}" | cut -d'=' -f2) if [ ! -z "$user_pid" ] && [ "$user_pid" != "0" ]; then local mem=$(ps -o rss= -p $user_pid 2>/dev/null) local cpu=$(ps -o %cpu= -p $user_pid 2>/dev/null) if [ ! -z "$mem" ]; then total_snell_memory=$((total_snell_memory + mem)) fi if [ ! -z "$cpu" ]; then total_snell_cpu=$(echo "$total_snell_cpu + $cpu" | bc -l) fi fi fi fi fi done fi # 显示 Snell 状态 local total_snell_memory_mb=$(echo "scale=2; $total_snell_memory/1024" | bc) printf "${GREEN}Snell 已安装${RESET} ${YELLOW}CPU:%.2f%%${RESET} ${YELLOW}内存:%.2f MB${RESET} ${GREEN}运行中:${running_count}/${user_count}${RESET}\n" "$total_snell_cpu" "$total_snell_memory_mb" else echo -e "${YELLOW}Snell 未安装${RESET}" fi # 检查 ShadowTLS 状态 if [ -f "/usr/local/bin/shadow-tls" ]; then # 初始化 ShadowTLS 服务计数器和资源使用 local stls_total=0 local stls_running=0 local total_stls_memory=0 local total_stls_cpu=0 declare -A processed_ports # 检查 Snell 的 ShadowTLS 服务 local snell_services=$(find /etc/systemd/system -name "shadowtls-snell-*.service" 2>/dev/null | sort -u) if [ ! -z "$snell_services" ]; then while IFS= read -r service_file; do local port=$(basename "$service_file" | sed 's/shadowtls-snell-\([0-9]*\)\.service/\1/') # 检查是否已处理过该端口 if [ -z "${processed_ports[$port]}" ]; then processed_ports[$port]=1 stls_total=$((stls_total + 1)) if systemctl is-active "shadowtls-snell-${port}" &> /dev/null; then stls_running=$((stls_running + 1)) # 获取 ShadowTLS 服务资源使用情况 local stls_pid=$(systemctl show -p MainPID "shadowtls-snell-${port}" | cut -d'=' -f2) if [ ! -z "$stls_pid" ] && [ "$stls_pid" != "0" ]; then local mem=$(ps -o rss= -p $stls_pid 2>/dev/null) local cpu=$(ps -o %cpu= -p $stls_pid 2>/dev/null) if [ ! -z "$mem" ]; then total_stls_memory=$((total_stls_memory + mem)) fi if [ ! -z "$cpu" ]; then total_stls_cpu=$(echo "$total_stls_cpu + $cpu" | bc -l) fi fi fi fi done <<< "$snell_services" fi # 显示 ShadowTLS 状态 if [ $stls_total -gt 0 ]; then local total_stls_memory_mb=$(echo "scale=2; $total_stls_memory/1024" | bc) printf "${GREEN}ShadowTLS 已安装${RESET} ${YELLOW}CPU:%.2f%%${RESET} ${YELLOW}内存:%.2f MB${RESET} ${GREEN}运行中:${stls_running}/${stls_total}${RESET}\n" "$total_stls_cpu" "$total_stls_memory_mb" else echo -e "${YELLOW}ShadowTLS 未安装${RESET}" fi else echo -e "${YELLOW}ShadowTLS 未安装${RESET}" fi echo -e "${CYAN}============================================${RESET}\n" } # 查看配置 view_snell_config() { echo -e "${GREEN}Snell 配置信息:${RESET}" echo -e "${CYAN}================================${RESET}" # 检测当前安装的 Snell 版本 local installed_version=$(detect_installed_snell_version) if [ "$installed_version" != "unknown" ]; then echo -e "${YELLOW}当前安装版本: Snell ${installed_version}${RESET}" fi # 获取 IPv4 地址 IPV4_ADDR=$(curl -s4 --connect-timeout 5 --max-time 10 https://api.ipify.org) if [ $? -eq 0 ] && [ ! -z "$IPV4_ADDR" ]; then IP_COUNTRY_IPV4=$(curl -s --connect-timeout 5 --max-time 10 http://ipinfo.io/${IPV4_ADDR}/country) echo -e "${GREEN}IPv4 地址: ${RESET}${IPV4_ADDR} ${GREEN}所在国家: ${RESET}${IP_COUNTRY_IPV4}" fi # 获取 IPv6 地址 IPV6_ADDR=$(curl -s6 --connect-timeout 5 --max-time 10 https://api64.ipify.org) if [ $? -eq 0 ] && [ ! -z "$IPV6_ADDR" ]; then IP_COUNTRY_IPV6=$(curl -s --connect-timeout 5 --max-time 10 https://ipapi.co/${IPV6_ADDR}/country/) echo -e "${GREEN}IPv6 地址: ${RESET}${IPV6_ADDR} ${GREEN}所在国家: ${RESET}${IP_COUNTRY_IPV6}" fi # 检查是否获取到 IP 地址 if [ -z "$IPV4_ADDR" ] && [ -z "$IPV6_ADDR" ]; then echo -e "${RED}无法获取到公网 IP 地址,请检查网络连接。${RESET}" return fi echo -e "\n${YELLOW}=== 用户配置列表 ===${RESET}" # 显示主用户配置 local main_conf="${SNELL_CONF_DIR}/users/snell-main.conf" if [ -f "$main_conf" ]; then echo -e "\n${GREEN}主用户配置:${RESET}" local main_port=$(grep -E '^listen' "$main_conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') local main_psk=$(grep -E '^psk' "$main_conf" | awk -F'=' '{print $2}' | tr -d ' ') local main_ipv6=$(grep -E '^ipv6' "$main_conf" | awk -F'=' '{print $2}' | tr -d ' ') local main_dns=$(grep -E '^dns' "$main_conf" | awk -F'=' '{print $2}' | tr -d ' ') echo -e "${YELLOW}端口: ${main_port}${RESET}" echo -e "${YELLOW}PSK: ${main_psk}${RESET}" echo -e "${YELLOW}IPv6: ${main_ipv6}${RESET}" echo -e "${YELLOW}DNS: ${main_dns}${RESET}" echo -e "\n${GREEN}Surge 配置格式:${RESET}" if [ ! -z "$IPV4_ADDR" ]; then generate_surge_config "$IPV4_ADDR" "$main_port" "$main_psk" "$installed_version" "$IP_COUNTRY_IPV4" "$installed_version" fi if [ ! -z "$IPV6_ADDR" ]; then generate_surge_config "$IPV6_ADDR" "$main_port" "$main_psk" "$installed_version" "$IP_COUNTRY_IPV6" "$installed_version" fi fi # 显示其他用户配置 if [ -d "${SNELL_CONF_DIR}/users" ]; then for user_conf in "${SNELL_CONF_DIR}/users"/*; do if [ -f "$user_conf" ] && [[ "$user_conf" != *"snell-main.conf" ]]; then local user_port=$(grep -E '^listen' "$user_conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') local user_psk=$(grep -E '^psk' "$user_conf" | awk -F'=' '{print $2}' | tr -d ' ') local user_ipv6=$(grep -E '^ipv6' "$user_conf" | awk -F'=' '{print $2}' | tr -d ' ') local user_dns=$(grep -E '^dns' "$user_conf" | awk -F'=' '{print $2}' | tr -d ' ') echo -e "\n${GREEN}用户配置 (端口: ${user_port}):${RESET}" echo -e "${YELLOW}PSK: ${user_psk}${RESET}" echo -e "${YELLOW}IPv6: ${user_ipv6}${RESET}" echo -e "${YELLOW}DNS: ${user_dns}${RESET}" echo -e "\n${GREEN}Surge 配置格式:${RESET}" if [ ! -z "$IPV4_ADDR" ]; then generate_surge_config "$IPV4_ADDR" "$user_port" "$user_psk" "$installed_version" "$IP_COUNTRY_IPV4" "$installed_version" fi if [ ! -z "$IPV6_ADDR" ]; then generate_surge_config "$IPV6_ADDR" "$user_port" "$user_psk" "$installed_version" "$IP_COUNTRY_IPV6" "$installed_version" fi fi done fi # 如果 ShadowTLS 已安装,显示组合配置 local snell_version=$(detect_installed_snell_version) local snell_services=$(find /etc/systemd/system -name "shadowtls-snell-*.service" 2>/dev/null | sort -u) if [ ! -z "$snell_services" ]; then echo -e "\n${YELLOW}=== ShadowTLS 组合配置 ===${RESET}" declare -A processed_ports while IFS= read -r service_file; do local exec_line=$(grep "ExecStart=" "$service_file") local stls_port=$(echo "$exec_line" | grep -oP '(?<=--listen ::0:)\d+') local stls_password=$(echo "$exec_line" | grep -oP '(?<=--password )[^ ]+') local stls_domain=$(echo "$exec_line" | grep -oP '(?<=--tls )[^ ]+') local snell_port=$(echo "$exec_line" | grep -oP '(?<=--server 127.0.0.1:)\d+') # 查找 psk local psk="" if [ -f "${SNELL_CONF_DIR}/users/snell-${snell_port}.conf" ]; then psk=$(grep -E '^psk' "${SNELL_CONF_DIR}/users/snell-${snell_port}.conf" | awk -F'=' '{print $2}' | tr -d ' ') elif [ -f "${SNELL_CONF_DIR}/users/snell-main.conf" ] && [ "$snell_port" = "$(get_snell_port)" ]; then psk=$(grep -E '^psk' "${SNELL_CONF_DIR}/users/snell-main.conf" | awk -F'=' '{print $2}' | tr -d ' ') fi # 避免重复 if [ -z "$snell_port" ] || [ -z "$psk" ] || [ -n "${processed_ports[$snell_port]}" ]; then continue fi processed_ports[$snell_port]=1 if [ "$snell_port" = "$(get_snell_port)" ]; then echo -e "\n${GREEN}主用户 ShadowTLS 配置:${RESET}" else echo -e "\n${GREEN}用户 ShadowTLS 配置 (端口: ${snell_port}):${RESET}" fi echo -e " - Snell 端口:${snell_port}" echo -e " - PSK:${psk}" echo -e " - ShadowTLS 监听端口:${stls_port}" echo -e " - ShadowTLS 密码:${stls_password}" echo -e " - ShadowTLS SNI:${stls_domain}" echo -e " - 版本:3" echo -e "\n${GREEN}Surge 配置格式:${RESET}" if [ ! -z "$IPV4_ADDR" ]; then if [ "$snell_version" = "v6" ]; then echo -e "${GREEN}${IP_COUNTRY_IPV4} = snell, ${IPV4_ADDR}, ${stls_port}, psk = ${psk}, version = 6, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" elif [ "$snell_version" = "v5" ]; then echo -e "${GREEN}${IP_COUNTRY_IPV4} = snell, ${IPV4_ADDR}, ${stls_port}, psk = ${psk}, version = 4, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" echo -e "${GREEN}${IP_COUNTRY_IPV4} = snell, ${IPV4_ADDR}, ${stls_port}, psk = ${psk}, version = 5, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" else echo -e "${GREEN}${IP_COUNTRY_IPV4} = snell, ${IPV4_ADDR}, ${stls_port}, psk = ${psk}, version = 4, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" fi fi if [ ! -z "$IPV6_ADDR" ]; then if [ "$snell_version" = "v6" ]; then echo -e "${GREEN}${IP_COUNTRY_IPV6} = snell, ${IPV6_ADDR}, ${stls_port}, psk = ${psk}, version = 6, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" elif [ "$snell_version" = "v5" ]; then echo -e "${GREEN}${IP_COUNTRY_IPV6} = snell, ${IPV6_ADDR}, ${stls_port}, psk = ${psk}, version = 4, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" echo -e "${GREEN}${IP_COUNTRY_IPV6} = snell, ${IPV6_ADDR}, ${stls_port}, psk = ${psk}, version = 5, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" else echo -e "${GREEN}${IP_COUNTRY_IPV6} = snell, ${IPV6_ADDR}, ${stls_port}, psk = ${psk}, version = 4, reuse = true, tfo = true, shadow-tls-password = ${stls_password}, shadow-tls-sni = ${stls_domain}, shadow-tls-version = 3${RESET}" fi fi done <<< "$snell_services" fi echo -e "\n${YELLOW}注意:${RESET}" echo -e "1. Snell 仅支持 Surge 客户端" echo -e "2. 请将配置中的服务器地址替换为实际可用的地址" read -p "按任意键返回主菜单..." } # 获取当前安装的 Snell 版本 get_current_snell_version() { local current_installed_version=$(detect_installed_snell_version) if [ "$current_installed_version" = "v6" ] || [ "$current_installed_version" = "v5" ]; then CURRENT_VERSION=$(snell-server --v 2>&1 | grep -oP 'v[0-9]+\.[0-9]+\.[0-9]+[a-z0-9]*') if [ -z "$CURRENT_VERSION" ]; then if [ "$current_installed_version" = "v6" ]; then CURRENT_VERSION="v6.0.0b4" else CURRENT_VERSION="v5.0.1" fi fi else CURRENT_VERSION=$(snell-server --v 2>&1 | grep -oP 'v[0-9]+\.[0-9]+\.[0-9]+') if [ -z "$CURRENT_VERSION" ]; then echo -e "${RED}无法获取当前 Snell 版本。${RESET}" exit 1 fi fi } # 检查 Snell 更新 check_snell_update() { echo -e "\n${CYAN}=============== 检查 Snell 更新 ===============${RESET}" # 检测当前安装的 Snell 版本 local current_installed_version=$(detect_installed_snell_version) if [ "$current_installed_version" = "unknown" ]; then echo -e "${RED}无法检测当前 Snell 版本${RESET}" return 1 fi echo -e "${YELLOW}当前安装版本: Snell ${current_installed_version}${RESET}" # 根据当前版本确定更新策略 if [ "$current_installed_version" = "v4" ]; then # v4 用户:可升级到 v5 或 v6,或继续检查 v4 更新 echo -e "\n${CYAN}检测到您当前使用的是 Snell v4,请选择目标版本:${RESET}" echo -e "${YELLOW}注意:v5/v6 为新版本,升级前请确认客户端支持${RESET}" echo -e "${GREEN}1.${RESET} 升级到 Snell v5" echo -e "${GREEN}2.${RESET} 升级到 Snell v6 (Beta)" echo -e "${GREEN}3.${RESET} 继续使用 Snell v4(检查 v4 更新)" echo -e "${GREEN}4.${RESET} 取消更新" while true; do read -rp "请选择 [1-4]: " upgrade_choice case "$upgrade_choice" in 1) SNELL_VERSION_CHOICE="v5" echo -e "${GREEN}已选择升级到 Snell v5${RESET}" break ;; 2) SNELL_VERSION_CHOICE="v6" echo -e "${GREEN}已选择升级到 Snell v6 (Beta)${RESET}" echo -e "${YELLOW}注意:v6 Beta 版协议可能存在不兼容更新,且已移除 QUIC 代理模式${RESET}" break ;; 3) SNELL_VERSION_CHOICE="v4" echo -e "${GREEN}已选择继续使用 Snell v4${RESET}" break ;; 4) echo -e "${CYAN}已取消更新${RESET}" return 0 ;; *) echo -e "${RED}请输入正确的选项 [1-4]${RESET}" ;; esac done elif [ "$current_installed_version" = "v5" ]; then # v5 用户:可升级到 v6 或继续检查 v5 更新 echo -e "\n${CYAN}检测到您当前使用的是 Snell v5,请选择目标版本:${RESET}" echo -e "${GREEN}1.${RESET} 升级到 Snell v6 (Beta)" echo -e "${GREEN}2.${RESET} 继续使用 Snell v5(检查 v5 更新)" echo -e "${GREEN}3.${RESET} 取消更新" while true; do read -rp "请选择 [1-3]: " upgrade_choice case "$upgrade_choice" in 1) SNELL_VERSION_CHOICE="v6" echo -e "${GREEN}已选择升级到 Snell v6 (Beta)${RESET}" echo -e "${YELLOW}注意:v6 Beta 版协议可能存在不兼容更新,且已移除 QUIC 代理模式${RESET}" break ;; 2) SNELL_VERSION_CHOICE="v5" echo -e "${GREEN}已选择继续使用 Snell v5${RESET}" break ;; 3) echo -e "${CYAN}已取消更新${RESET}" return 0 ;; *) echo -e "${RED}请输入正确的选项 [1-3]${RESET}" ;; esac done else # v6 用户:直接检查 v6 更新 SNELL_VERSION_CHOICE="v6" echo -e "${GREEN}当前为 Snell v6,将检查 v6 更新${RESET}" fi # 获取最新版本信息 get_latest_snell_version get_current_snell_version echo -e "${YELLOW}当前 Snell 版本: ${CURRENT_VERSION}${RESET}" echo -e "${YELLOW}最新 Snell 版本: ${SNELL_VERSION}${RESET}" # 检查是否需要更新 if ! version_greater_equal "$CURRENT_VERSION" "$SNELL_VERSION"; then echo -e "\n${CYAN}发现新版本,更新说明:${RESET}" echo -e "${GREEN}✓ 这是更新操作,不是重新安装${RESET}" echo -e "${GREEN}✓ 所有现有配置将被保留(端口、密码、用户配置)${RESET}" echo -e "${GREEN}✓ 服务会自动重启${RESET}" echo -e "${GREEN}✓ 配置文件会自动备份${RESET}" echo -e "${CYAN}是否更新 Snell? [y/N]${RESET}" read -r choice if [[ "$choice" == "y" || "$choice" == "Y" ]]; then update_snell_binary else echo -e "${CYAN}已取消更新。${RESET}" fi else echo -e "${GREEN}当前已是最新版本 (${CURRENT_VERSION})。${RESET}" fi } # 获取最新 GitHub 版本 get_latest_github_version() { local api_url="https://api.github.com/repos/jinqians/snell.sh/releases/latest" local response response=$(curl -s "$api_url") if [ $? -ne 0 ] || [ -z "$response" ]; then echo -e "${RED}无法获取 GitHub 上的最新版本信息。${RESET}" return 1 fi GITHUB_VERSION=$(echo "$response" | grep -o '"tag_name": "[^"]*"' | cut -d'"' -f4) if [ -z "$GITHUB_VERSION" ]; then echo -e "${RED}解析 GitHub 版本信息失败。${RESET}" return 1 fi } # 更新脚本 update_script() { echo -e "${CYAN}正在检查脚本更新...${RESET}" # 创建临时文件 TMP_SCRIPT=$(mktemp) # 下载最新版本 if curl -sL https://raw.githubusercontent.com/jinqians/snell.sh/main/snell.sh -o "$TMP_SCRIPT"; then # 获取新版本号 new_version=$(grep "current_version=" "$TMP_SCRIPT" | cut -d'"' -f2) if [ -z "$new_version" ]; then echo -e "${RED}无法获取新版本信息${RESET}" rm -f "$TMP_SCRIPT" return 1 fi echo -e "${YELLOW}当前版本:${current_version}${RESET}" echo -e "${YELLOW}最新版本:${new_version}${RESET}" # 比较版本号 if [ "$new_version" != "$current_version" ]; then echo -e "${CYAN}是否更新到新版本?[y/N]${RESET}" read -r choice if [[ "$choice" == "y" || "$choice" == "Y" ]]; then # 获取当前脚本的完整路径 SCRIPT_PATH=$(readlink -f "$0") # 备份当前脚本 cp "$SCRIPT_PATH" "${SCRIPT_PATH}.backup" # 更新脚本 mv "$TMP_SCRIPT" "$SCRIPT_PATH" chmod +x "$SCRIPT_PATH" echo -e "${GREEN}脚本已更新到最新版本${RESET}" echo -e "${YELLOW}已备份原脚本到:${SCRIPT_PATH}.backup${RESET}" echo -e "${CYAN}请重新运行脚本以使用新版本${RESET}" exit 0 else echo -e "${YELLOW}已取消更新${RESET}" rm -f "$TMP_SCRIPT" fi else echo -e "${GREEN}当前已是最新版本${RESET}" rm -f "$TMP_SCRIPT" fi else echo -e "${RED}下载新版本失败,请检查网络连接${RESET}" rm -f "$TMP_SCRIPT" fi } # 检查是否安装的函数 check_installation() { local service=$1 if systemctl list-unit-files | grep -q "^$service.service"; then echo -e "${GREEN}已安装${RESET}" else echo -e "${RED}未安装${RESET}" fi } # 获取 ShadowTLS 配置 get_shadowtls_config() { # 获取主 Snell 端口 local main_port=$(get_snell_port) if [ -z "$main_port" ]; then return 1 fi # 检查对应端口的 ShadowTLS 服务 local service_name="shadowtls-snell-${main_port}" if ! systemctl is-active --quiet "$service_name"; then return 1 fi local service_file="/etc/systemd/system/${service_name}.service" if [ ! -f "$service_file" ]; then return 1 fi # 从服务文件中读取配置行 local exec_line=$(grep "ExecStart=" "$service_file") if [ -z "$exec_line" ]; then return 1 fi # 提取配置信息 local tls_domain=$(echo "$exec_line" | grep -o -- "--tls [^ ]*" | cut -d' ' -f2) local password=$(echo "$exec_line" | grep -o -- "--password [^ ]*" | cut -d' ' -f2) local listen_part=$(echo "$exec_line" | grep -o -- "--listen [^ ]*" | cut -d' ' -f2) local listen_port=$(echo "$listen_part" | grep -o '[0-9]*$') if [ -z "$tls_domain" ] || [ -z "$password" ] || [ -z "$listen_port" ]; then return 1 fi echo "${password}|${tls_domain}|${listen_port}" return 0 } # 检查是否以 root 权限运行 check_root() { if [ "$(id -u)" != "0" ]; then echo -e "${RED}请以 root 权限运行此脚本${RESET}" exit 1 fi } # 初始检查 initial_check() { check_root check_curl check_bc check_and_migrate_config sync_existing_main_service_unit check_and_show_status } # 运行初始检查 initial_check # 多用户管理 setup_multi_user() { echo -e "${CYAN}正在执行多用户管理脚本...${RESET}" bash <(curl -sL https://raw.githubusercontent.com/jinqians/snell.sh/main/multi-user.sh) # 多用户管理脚本执行完毕后会自动返回这里 echo -e "${GREEN}多用户管理操作完成${RESET}" sleep 1 # 给用户一点时间看到提示 } # 主菜单 show_menu() { clear echo -e "${CYAN}============================================${RESET}" echo -e "${CYAN} Snell 管理脚本 v${current_version} (支持v4/v5/v6)${RESET}" echo -e "${CYAN}============================================${RESET}" echo -e "${GREEN}作者: jinqian${RESET}" echo -e "${GREEN}网站:https://jinqians.com${RESET}" echo -e "${CYAN}============================================${RESET}" # 显示服务状态 check_and_show_status echo -e "${YELLOW}=== 基础功能 ===${RESET}" echo -e "${GREEN}1.${RESET} 安装 Snell" echo -e "${GREEN}2.${RESET} 卸载 Snell" echo -e "${GREEN}3.${RESET} 查看配置" echo -e "${GREEN}4.${RESET} 重启服务" echo -e "\n${YELLOW}=== 增强功能 ===${RESET}" echo -e "${GREEN}5.${RESET} ShadowTLS 管理" echo -e "${GREEN}6.${RESET} BBR 管理" echo -e "${GREEN}7.${RESET} 多用户管理" echo -e "\n${YELLOW}=== 系统功能 ===${RESET}" echo -e "${GREEN}8.${RESET} 更新Snell" echo -e "${GREEN}9.${RESET} 更新脚本" echo -e "${GREEN}10.${RESET} 查看服务状态" echo -e "${GREEN}11.${RESET} Snell v5/v6 出口控制设置" echo -e "${GREEN}0.${RESET} 退出脚本" echo -e "${CYAN}============================================${RESET}" if ! read -rp "请输入选项 [0-11]: " num; then echo echo -e "${YELLOW}未读取到输入,已退出 Snell 菜单。${RESET}" exit 0 fi } #开启bbr setup_bbr() { echo -e "${CYAN}正在获取并执行 BBR 管理脚本...${RESET}" # 直接从远程执行BBR脚本 bash <(curl -sL https://raw.githubusercontent.com/jinqians/snell.sh/main/bbr.sh) # BBR 脚本执行完毕后会自动返回这里 echo -e "${GREEN}BBR 管理操作完成${RESET}" sleep 1 # 给用户一点时间看到提示 } # ShadowTLS管理 setup_shadowtls() { echo -e "${CYAN}正在执行 ShadowTLS 管理脚本...${RESET}" bash <(curl -sL https://raw.githubusercontent.com/jinqians/snell.sh/main/shadowtls.sh) # ShadowTLS 脚本执行完毕后会自动返回这里 echo -e "${GREEN}ShadowTLS 管理操作完成${RESET}" sleep 1 # 给用户一点时间看到提示 } # 获取 Snell 端口 get_snell_port() { if [ -f "${SNELL_CONF_DIR}/users/snell-main.conf" ]; then grep -E '^listen' "${SNELL_CONF_DIR}/users/snell-main.conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p' fi } # 获取所有 Snell 用户配置 get_all_snell_users() { # 检查用户配置目录是否存在 if [ ! -d "${SNELL_CONF_DIR}/users" ]; then return 1 fi # 首先获取主用户配置 local main_port="" local main_psk="" if [ -f "${SNELL_CONF_DIR}/users/snell-main.conf" ]; then main_port=$(grep -E '^listen' "${SNELL_CONF_DIR}/users/snell-main.conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') main_psk=$(grep -E '^psk' "${SNELL_CONF_DIR}/users/snell-main.conf" | awk -F'=' '{print $2}' | tr -d ' ') if [ ! -z "$main_port" ] && [ ! -z "$main_psk" ]; then echo "${main_port}|${main_psk}" fi fi # 获取其他用户配置 for user_conf in "${SNELL_CONF_DIR}/users"/snell-*.conf; do if [ -f "$user_conf" ] && [[ "$user_conf" != *"snell-main.conf" ]]; then local port=$(grep -E '^listen' "$user_conf" | sed -n 's/^[[:space:]]*listen[[:space:]]*=.*:\([0-9][0-9]*\).*/\1/p') local psk=$(grep -E '^psk' "$user_conf" | awk -F'=' '{print $2}' | tr -d ' ') if [ ! -z "$port" ] && [ ! -z "$psk" ]; then echo "${port}|${psk}" fi fi done } # 主循环 while true; do show_menu case "$num" in 1) install_snell ;; 2) uninstall_snell ;; 3) view_snell_config ;; 4) restart_snell ;; 5) setup_shadowtls ;; 6) setup_bbr ;; 7) setup_multi_user ;; 8) check_snell_update ;; 9) update_script ;; 10) check_and_show_status read -p "按任意键继续..." || exit 0 ;; 11) configure_v5_egress_control read -p "按任意键继续..." || exit 0 ;; 0) echo -e "${GREEN}感谢使用,再见!${RESET}" exit 0 ;; *) echo -e "${RED}请输入正确的选项 [0-11]${RESET}" ;; esac echo -e "\n${CYAN}按任意键返回主菜单...${RESET}" read -n 1 -s -r || exit 0 done