#!/bin/sh # 此文件由 tools/build.sh 从 src/snell-docker.sh 和 src/lib 生成:请修改 src/ 下的文件后重新生成。 # ========================================= # 作者: jinqians (Docker版) # 创建日期: 2026年3月5日 # 描述: 使用 Docker 在 Alpine | Debian | CentOS…… Linux 上安装和管理 Snell 代理 # 避免宿主机兼容性问题,支持 Snell v4/v5/v6 # ========================================= # --- 共用部分(src/lib,发布时由 tools/build.sh 合进来;都是 POSIX sh)--- # 颜色、root 检查、Snell 官方版本与下载地址、公网地址与国家、防火墙 # ── lib/common.sh ───────────────────────────────────────────────────────────── # 所有脚本共用:颜色、root 检查、系统与包管理器、脚本的发布地址。 # 只用 POSIX sh 的写法:Alpine / Docker 版脚本在 ash、dash 下也要能用。 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' CYAN='\033[0;36m' RESET='\033[0m' # 脚本的发布地址。短域名(*.jinqians.com)由 Cloudflare 重定向到仓库里的文件: # snell / snell-centos / snell-alpine → 根目录,menu / snell-docker / install → scripts/。 # 菜单里调用的子脚本从 SNELL_RAW_BASE 下的 scripts/、docker/ 取。 # 脚本自身的更新与管理命令走短域名,运行才会被统计到。 # 可以用同名环境变量换成镜像地址(测试时指向本地的文件)。 SNELL_RAW_BASE="${SNELL_RAW_BASE:-https://raw.githubusercontent.com/jinqians/snell.sh/main}" SNELL_SCRIPT_URL="${SNELL_SCRIPT_URL:-https://snell.jinqians.com}" # snell.sh(Debian / Ubuntu / CentOS / RHEL) SNELL_ALPINE_SCRIPT_URL="${SNELL_ALPINE_SCRIPT_URL:-https://snell-alpine.jinqians.com}" # snell-alpine.sh SNELL_DOCKER_SCRIPT_URL="${SNELL_DOCKER_SCRIPT_URL:-https://snell-docker.jinqians.com}" # snell-docker.sh SNELL_MENU_SCRIPT_URL="${SNELL_MENU_SCRIPT_URL:-https://menu.jinqians.com}" # menu.sh # 检查是否以 root 权限运行 check_root() { if [ "$(id -u)" != "0" ]; then printf '%b\n' "${RED}请以 root 权限运行此脚本${RESET}" exit 1 fi } # 识别系统:OS_FAMILY = debian | rhel | alpine | unknown,PKG = apt | dnf | yum | apk OS_FAMILY="" PKG="" detect_os() { [ -n "$OS_FAMILY" ] && return 0 OS_FAMILY="unknown" if [ -f /etc/os-release ]; then # 在子 shell 里读,os-release 的 NAME / VERSION 等变量不会覆盖脚本自己的 case " $(. /etc/os-release; echo "$ID $ID_LIKE" | tr '[:upper:]' '[:lower:]') " in *" debian "*|*" ubuntu "*) OS_FAMILY="debian" ;; *" rhel "*|*" centos "*|*" fedora "*|*" rocky "*|*" almalinux "*) OS_FAMILY="rhel" ;; *" alpine "*) OS_FAMILY="alpine" ;; esac elif [ -f /etc/redhat-release ]; then OS_FAMILY="rhel" fi if command -v apt-get >/dev/null 2>&1; then PKG="apt" elif command -v dnf >/dev/null 2>&1; then PKG="dnf" elif command -v yum >/dev/null 2>&1; then PKG="yum" elif command -v apk >/dev/null 2>&1; then PKG="apk" fi } # 等待其他 apt 进程完成 wait_for_apt() { command -v fuser >/dev/null 2>&1 || return 0 while fuser /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock >/dev/null 2>&1; do printf '%b\n' "${YELLOW}等待其他 apt 进程完成...${RESET}" sleep 2 done } # 用系统的包管理器安装软件包 pkg_install() { detect_os case "$PKG" in apt) wait_for_apt DEBIAN_FRONTEND=noninteractive apt-get update -q >/dev/null 2>&1 || true DEBIAN_FRONTEND=noninteractive apt-get install -y "$@" ;; dnf) dnf install -y "$@" ;; yum) yum install -y "$@" ;; apk) apk add --no-cache "$@" ;; *) printf '%b\n' "${RED}未识别的包管理器,请手动安装:$*${RESET}" return 1 ;; esac } # 提供某个命令的软件包名(各发行版不同的在这里对上) pkg_for_cmd() { detect_os case "$1" in ip|ss) if [ "$OS_FAMILY" = "rhel" ]; then echo "iproute"; else echo "iproute2"; fi ;; nft) echo "nftables" ;; fuser) echo "psmisc" ;; *) echo "$1" ;; esac } # 缺哪个命令就装哪个包;装不上返回非 0,由调用方决定是否退出 ensure_cmds() { _ec_missing="" for _ec_cmd in "$@"; do command -v "$_ec_cmd" >/dev/null 2>&1 || _ec_missing="${_ec_missing} $(pkg_for_cmd "$_ec_cmd")" done [ -z "$_ec_missing" ] && return 0 printf '%b\n' "${YELLOW}正在安装依赖:${_ec_missing# }${RESET}" # shellcheck disable=SC2086 # 包名按空格分开传 pkg_install $_ec_missing || return 1 for _ec_cmd in "$@"; do if ! command -v "$_ec_cmd" >/dev/null 2>&1; then printf '%b\n' "${RED}安装后仍找不到 ${_ec_cmd},请手动安装 $(pkg_for_cmd "$_ec_cmd")${RESET}" return 1 fi done } # 下载远程脚本并做完整性校验:传输失败即停、非空、语法检查。 # 只能保证传输完整;仓库本身被篡改要靠发布签名来防。 fetch_verified_script() { # if ! curl -fsSL --retry 2 --connect-timeout 10 --max-time 60 "$1" -o "$2"; then printf '%b\n' "${RED}下载失败: $1${RESET}" >&2 rm -f "$2" return 1 fi if [ ! -s "$2" ]; then printf '%b\n' "${RED}下载的文件为空,已丢弃: $1${RESET}" >&2 rm -f "$2" return 1 fi # bash 能解析 sh 写法;没有 bash 的系统(Alpine)用 sh 检查 if command -v bash >/dev/null 2>&1; then _fv_shell=bash else _fv_shell=sh fi if ! "$_fv_shell" -n "$2" 2>/dev/null; then printf '%b\n' "${RED}下载的脚本未通过语法检查,已丢弃: $1${RESET}" >&2 rm -f "$2" return 1 fi return 0 } # ── lib/release.sh ──────────────────────────────────────────────────────────── # Snell 官方版本:从发布页取各大版本的最新版本号,生成下载地址。POSIX sh。 # 抓取失败时的兜底版本号 SNELL_V4_FALLBACK="v4.1.1" SNELL_V5_FALLBACK="v5.0.1" SNELL_V6_FALLBACK="v6.0.0rc2" # Snell 官方发布页(旧的 manual.nssurge.com/others/snell.html 已下线) SNELL_RELEASE_NOTES_URL="https://kb.nssurge.com/surge-knowledge-base/release-notes/snell" SNELL_RELEASE_NOTES_URL_ZH="https://kb.nssurge.com/surge-knowledge-base/zh/release-notes/snell" # 抓取官方发布页内容 fetch_snell_release_notes() { local notes notes=$(curl -sL --max-time 15 "$SNELL_RELEASE_NOTES_URL") if [ -z "$notes" ]; then notes=$(curl -sL --max-time 15 "$SNELL_RELEASE_NOTES_URL_ZH") fi echo "$notes" } # 把版本号转成定长可排序键,排序优先级:beta < rc < 正式版 # 6.0.0b4 -> 006.000.000.1.0004;6.0.0rc -> 006.000.000.2.0000;6.0.0rc2 -> 006.000.000.2.0002;6.0.0 -> 006.000.000.3.0000 snell_version_sort_key() { echo "${1#[vV]}" | awk '{ ver = $0 suffix = "" if (match(ver, /[a-zA-Z]+[0-9]*$/)) { suffix = tolower(substr(ver, RSTART)) ver = substr(ver, 1, RSTART - 1) } split(ver, part, ".") stage = 3 seq = 0 if (suffix != "") { stage = (suffix ~ /^rc/) ? 2 : 1 digits = suffix gsub(/[^0-9]/, "", digits) if (digits != "") seq = digits + 0 } printf "%03d.%03d.%03d.%d.%04d", part[1], part[2], part[3], stage, seq }' } # 从发布页中挑出指定大版本的最新版本(页面上的先后顺序不代表新旧,必须排序) pick_latest_snell_version() { local major="$1" local notes="$2" echo "$notes" \ | grep -oE "snell-server-v${major}\.[0-9]+\.[0-9]+[a-zA-Z0-9]*" \ | sed 's/^snell-server-v//' \ | sort -u \ | while read -r ver; do echo "$(snell_version_sort_key "$ver") ${ver}" done \ | sort \ | tail -n 1 \ | awk '{print $2}' } # 获取 Snell v4 最新版本 get_latest_snell_v4_version() { local ver ver=$(pick_latest_snell_version 4 "$(fetch_snell_release_notes)") if [ -n "$ver" ]; then echo "v${ver}" else echo "${SNELL_V4_FALLBACK}" fi } # 获取 Snell v5 最新版本 get_latest_snell_v5_version() { local ver ver=$(pick_latest_snell_version 5 "$(fetch_snell_release_notes)") if [ -n "$ver" ]; then echo "v${ver}" else echo "${SNELL_V5_FALLBACK}" fi } # 获取 Snell v6 最新版本 get_latest_snell_v6_version() { local ver ver=$(pick_latest_snell_version 6 "$(fetch_snell_release_notes)") if [ -n "$ver" ]; then echo "v${ver}" else echo "${SNELL_V6_FALLBACK}" fi } # 解析指定通道的最新版本号(失败时回落到内置常量) resolve_latest_version_for_channel() { case "$1" in v6) get_latest_snell_v6_version ;; v5) get_latest_snell_v5_version ;; v4) get_latest_snell_v4_version ;; *) return 1 ;; esac } # 生成指定通道 + 版本号的下载地址;不支持的架构返回非 0(不 exit,调用方可继续) snell_download_url_for() { local version_choice="$1" local resolved_version="$2" local arch arch=$(uname -m) case "$version_choice" in v4|v5|v6) ;; *) printf '%b\n' "${RED}不支持的 Snell 通道: ${version_choice}${RESET}" >&2 return 1 ;; esac if [ "$version_choice" = "v6" ] && { [ "$arch" = "armv7l" ] || [ "$arch" = "armv7" ]; }; then printf '%b\n' "${RED}Snell v6 暂不提供 armv7l 构建${RESET}" >&2 return 1 fi case "$arch" in "x86_64"|"amd64") echo "https://dl.nssurge.com/snell/snell-server-${resolved_version}-linux-amd64.zip" ;; "i386"|"i686") echo "https://dl.nssurge.com/snell/snell-server-${resolved_version}-linux-i386.zip" ;; "aarch64"|"arm64") echo "https://dl.nssurge.com/snell/snell-server-${resolved_version}-linux-aarch64.zip" ;; "armv7l"|"armv7") echo "https://dl.nssurge.com/snell/snell-server-${resolved_version}-linux-armv7l.zip" ;; *) printf '%b\n' "${RED}不支持的架构: ${arch}${RESET}" >&2 return 1 ;; esac } # ── lib/netinfo.sh ──────────────────────────────────────────────────────────── # 本机公网地址与所在国家(生成客户端配置时用)。POSIX sh。 # 本机公网 IPv4 / IPv6(取不到时为空) get_public_ipv4() { curl -s4 --connect-timeout 5 --max-time 10 https://api.ipify.org 2>/dev/null; } get_public_ipv6() { curl -s6 --connect-timeout 5 --max-time 10 https://api64.ipify.org 2>/dev/null; } # 查询 IP 所属国家代码(多接口回退,避免单一接口限流返回错误信息) get_ip_country() { local target="$1" local api="" local raw="" local result="" if [ -z "$target" ]; then echo "Unknown" return 1 fi for api in "https://ipinfo.io/${target}/country" \ "http://ip-api.com/line/${target}?fields=countryCode" \ "https://ipwho.is/${target}?fields=country_code" \ "https://ipapi.co/${target}/country/"; do raw=$(curl -s --connect-timeout 5 --max-time 10 "$api" 2>/dev/null) result=$(echo "$raw" | tr -d ' \t\r\n') case "$result" in [A-Za-z][A-Za-z]) ;; *) result=$(echo "$raw" | sed -n 's/.*"country_code"[[:space:]]*:[[:space:]]*"\([A-Za-z][A-Za-z]\)".*/\1/p' | head -n 1) ;; esac case "$result" in [A-Za-z][A-Za-z]) echo "$result" | tr '[:lower:]' '[:upper:]' return 0 ;; esac done echo "Unknown" return 1 } # ── lib/firewall.sh ─────────────────────────────────────────────────────────── # 开放 / 关闭端口:firewalld、ufw、iptables + ip6tables、nftables,系统在用哪个就配哪个, # 并持久化。所有脚本共用这一份(以前各脚本各有一份,CentOS 版才认 firewalld、 # Docker 版只开 TCP、重复安装会叠加重复规则)。POSIX sh。 # firewalld 正在运行 _fw_firewalld_active() { command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1 } # ufw 已启用(-w:不把 inactive 当成 active) _fw_ufw_active() { command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qw "active" } # both | tcp | udp → 协议列表 _fw_protos() { case "$1" in tcp) echo "tcp" ;; udp) echo "udp" ;; *) echo "tcp udp" ;; esac } # iptables 规则持久化:Debian 的 /etc/iptables、RHEL 的 iptables-services、Alpine 的 init 脚本 _fw_iptables_save() { if [ -x /etc/init.d/iptables ] && command -v rc-update >/dev/null 2>&1; then /etc/init.d/iptables save >/dev/null 2>&1 || true rc-update add iptables boot >/dev/null 2>&1 || true if [ -x /etc/init.d/ip6tables ]; then /etc/init.d/ip6tables save >/dev/null 2>&1 || true rc-update add ip6tables boot >/dev/null 2>&1 || true fi elif [ -f /etc/sysconfig/iptables ]; then iptables-save > /etc/sysconfig/iptables 2>/dev/null || true [ -f /etc/sysconfig/ip6tables ] && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true else mkdir -p /etc/iptables iptables-save > /etc/iptables/rules.v4 2>/dev/null || true command -v ip6tables-save >/dev/null 2>&1 && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true fi } # iptables / ip6tables:没有同样的规则才插入 _fw_iptables_open() { # _fio_changed=false for _fio_t in iptables ip6tables; do command -v "$_fio_t" >/dev/null 2>&1 || continue "$_fio_t" -L INPUT -n >/dev/null 2>&1 || continue # 内核或容器里不可用 for _fio_p in $2; do if ! "$_fio_t" -C INPUT -p "$_fio_p" --dport "$1" -j ACCEPT 2>/dev/null; then "$_fio_t" -I INPUT -p "$_fio_p" --dport "$1" -j ACCEPT 2>/dev/null && _fio_changed=true fi done done if [ "$_fio_changed" = true ]; then printf '%b\n' "${CYAN}在 iptables 中开放端口 $1${RESET}" _fw_iptables_save fi } _fw_iptables_close() { # _fic_changed=false for _fic_t in iptables ip6tables; do command -v "$_fic_t" >/dev/null 2>&1 || continue for _fic_p in tcp udp; do while "$_fic_t" -C INPUT -p "$_fic_p" --dport "$1" -j ACCEPT 2>/dev/null; do "$_fic_t" -D INPUT -p "$_fic_p" --dport "$1" -j ACCEPT 2>/dev/null || break _fic_changed=true done done done [ "$_fic_changed" = true ] && _fw_iptables_save return 0 } # 保存 nftables 规则(有持久化配置文件时) save_nftables_rules() { command -v nft >/dev/null 2>&1 || return 0 for _snr_f in /etc/nftables.conf /etc/sysconfig/nftables.conf /etc/nftables.nft; do [ -f "$_snr_f" ] || continue nft list ruleset > "$_snr_f" 2>/dev/null || true if command -v systemctl >/dev/null 2>&1; then systemctl enable nftables >/dev/null 2>&1 || true elif command -v rc-update >/dev/null 2>&1; then rc-update add nftables boot >/dev/null 2>&1 || true fi printf '%b\n' "${GREEN}nftables 规则已保存${RESET}" return 0 done return 0 } # nftables 里 hook 在 input 上的 filter 链("族 表 链" 每行一条)。 # iptables-nft 自己的 ip/ip6 filter 表已由 iptables 处理,跳过,免得规则重复。 _fw_nft_input_chains() { nft -a list ruleset 2>/dev/null | awk -v skip_ipt="$1" ' $1 == "table" { family = $2; table = $3; gsub(/[{}]/, "", table) } $1 == "chain" { chain = $2; gsub(/[{}]/, "", chain); in_chain = 1; next } in_chain && /type filter/ && /hook input/ { if (!(skip_ipt == "1" && (family == "ip" || family == "ip6") && table == "filter")) print family " " table " " chain } in_chain && /^[[:space:]]*}/ { in_chain = 0 } ' } # 在 nftables 现有的 input 链里放行端口(没有 nftables 防火墙时什么都不做) open_nftables_port() { # [both|tcp|udp] command -v nft >/dev/null 2>&1 || return 0 _onp_skip=0 command -v iptables >/dev/null 2>&1 && _onp_skip=1 _onp_chains=$(_fw_nft_input_chains "$_onp_skip") [ -n "$_onp_chains" ] || return 0 _onp_changed=false _onp_protos=$(_fw_protos "${2:-both}") while read -r _onp_family _onp_table _onp_chain; do [ -n "$_onp_family" ] || continue for _onp_p in $_onp_protos; do if ! nft list chain "$_onp_family" "$_onp_table" "$_onp_chain" 2>/dev/null | grep -q "$_onp_p dport $1 .*accept"; then nft insert rule "$_onp_family" "$_onp_table" "$_onp_chain" "$_onp_p" dport "$1" accept 2>/dev/null && _onp_changed=true fi done done < command -v nft >/dev/null 2>&1 || return 0 _cnp_rules=$(nft -a list ruleset 2>/dev/null | awk -v port="$1" ' $1 == "table" { family = $2; table = $3; gsub(/[{}]/, "", table) } $1 == "chain" { chain = $2; gsub(/[{}]/, "", chain) } ($0 ~ "(tcp|udp) dport " port " .*accept") && /# handle/ { print family " " table " " chain " " $NF } ') [ -n "$_cnp_rules" ] || return 0 while read -r _cnp_family _cnp_table _cnp_chain _cnp_handle; do [ -n "$_cnp_handle" ] || continue nft delete rule "$_cnp_family" "$_cnp_table" "$_cnp_chain" handle "$_cnp_handle" 2>/dev/null || true done < [both|tcp|udp] _op_protos=$(_fw_protos "${2:-both}") if _fw_firewalld_active; then printf '%b\n' "${CYAN}在 firewalld 中开放端口 $1${RESET}" for _op_p in $_op_protos; do firewall-cmd --permanent --add-port="$1/${_op_p}" >/dev/null 2>&1 || true firewall-cmd --add-port="$1/${_op_p}" >/dev/null 2>&1 || true done return 0 fi # ufw 装了但没启用时也写进去:以后启用 ufw 时端口仍是开的 if command -v ufw >/dev/null 2>&1; then printf '%b\n' "${CYAN}在 UFW 中开放端口 $1${RESET}" for _op_p in $_op_protos; do ufw allow "$1/${_op_p}" >/dev/null 2>&1 || true done _fw_ufw_active && return 0 fi _fw_iptables_open "$1" "$_op_protos" open_nftables_port "$1" "${2:-both}" return 0 } # 关闭端口:各个防火墙里放行它的规则都删掉 close_port() { # if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then for _cp_p in tcp udp; do firewall-cmd --permanent --remove-port="$1/${_cp_p}" >/dev/null 2>&1 || true firewall-cmd --remove-port="$1/${_cp_p}" >/dev/null 2>&1 || true done fi if command -v ufw >/dev/null 2>&1; then ufw delete allow "$1/tcp" >/dev/null 2>&1 || true ufw delete allow "$1/udp" >/dev/null 2>&1 || true ufw delete allow "$1" >/dev/null 2>&1 || true fi _fw_iptables_close "$1" close_nftables_port "$1" return 0 } # 端口在当前防火墙里是否放行(测试与状态显示用):firewalld / ufw / iptables / nftables 任一处放行即算 port_allowed() { # if _fw_firewalld_active; then firewall-cmd --query-port="$1/$2" >/dev/null 2>&1 return fi if _fw_ufw_active; then ufw status 2>/dev/null | grep -Eq "^$1(/$2)?[[:space:]]+ALLOW" return fi if command -v iptables >/dev/null 2>&1 && iptables -C INPUT -p "$2" --dport "$1" -j ACCEPT 2>/dev/null; then return 0 fi command -v nft >/dev/null 2>&1 && nft list ruleset 2>/dev/null | grep -q "$2 dport $1 .*accept" } # 端口是否有进程在监听(TCP 或 UDP) is_port_in_use() { # if command -v ss >/dev/null 2>&1; then ss -H -ltn "( sport = :$1 )" 2>/dev/null | grep -q . && return 0 ss -H -lun "( sport = :$1 )" 2>/dev/null | grep -q . && return 0 return 1 fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1 && return 0 lsof -nP -iUDP:"$1" >/dev/null 2>&1 return fi return 1 } # 显示占用指定端口的进程 show_port_occupier() { # if command -v ss >/dev/null 2>&1; then ss -ltnp "( sport = :$1 )" 2>/dev/null | sed 's/^/ /' ss -lunp "( sport = :$1 )" 2>/dev/null | sed 's/^/ /' return fi if command -v lsof >/dev/null 2>&1; then lsof -nP -iTCP:"$1" -sTCP:LISTEN 2>/dev/null | sed 's/^/ /' lsof -nP -iUDP:"$1" 2>/dev/null | sed 's/^/ /' fi } BLUE='\033[0;34m' # --- 脚本版本号 --- current_version="1.6" # Snell v6 加密模式:default / unshaped / unsafe-raw(客户端必须与服务端一致) SNELL_MODE="default" # --- 全局变量 --- SNELL_VERSION_CHOICE="" SNELL_VERSION="" CONTAINER_NAME="snell-server" IMAGE_NAME="my-snell" # 脚本所在目录(在 cd 到临时工作目录之前解析,供 create_dockerfile 查找 entrypoint.sh) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" # --- 基础函数 --- # Alpine 最小安装默认没有 curl,缺失时查公网 IP、拉取版本号都会静默失败 ensure_curl() { command -v curl >/dev/null 2>&1 && return 0 echo -e "${YELLOW}未检测到 curl,正在安装...${RESET}" if command -v apk >/dev/null 2>&1; then apk add --no-cache curl >/dev/null 2>&1 elif command -v apt-get >/dev/null 2>&1; then apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq curl >/dev/null 2>&1 elif command -v dnf >/dev/null 2>&1; then dnf install -y curl >/dev/null 2>&1 elif command -v yum >/dev/null 2>&1; then yum install -y curl >/dev/null 2>&1 fi if ! command -v curl >/dev/null 2>&1; then echo -e "${RED}curl 安装失败,请手动安装后重试。${RESET}" exit 1 fi } # 按容器名精确匹配;直接 grep docker ps 的输出会误中镜像名 jinqians/snell-server container_exists() { docker ps -a --format '{{.Names}}' 2>/dev/null | grep -qx "${CONTAINER_NAME}" } container_running() { docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "${CONTAINER_NAME}" } # Docker 开机自启 # Alpine 的 /etc/init.d/docker 声明了 need net,而 net 由 networking 提供, # 云镜像里 networking 在 default 运行级别。docker 若放在 boot 运行级别, # 开机时依赖不满足、不会被拉起,重启后所有容器都连不上,所以必须放在 default。 enable_docker_on_boot() { if [ -f /etc/alpine-release ]; then if rc-update show boot 2>/dev/null | grep -qw docker; then rc-update del docker boot >/dev/null 2>&1 echo -e "${YELLOW}已将 docker 从 boot 运行级别移到 default(否则重启后不会自动启动)${RESET}" fi rc-update add docker default >/dev/null 2>&1 else systemctl enable docker >/dev/null 2>&1 fi } check_docker() { if ! command -v docker >/dev/null 2>&1; then echo -e "${YELLOW}未检测到 Docker,是否自动安装?${RESET}" printf "输入 y 安装,其他键退出: " read -r install_docker if [ "$install_docker" != "y" ] && [ "$install_docker" != "Y" ]; then echo -e "${RED}取消安装。${RESET}" exit 1 fi echo -e "${CYAN}正在安装 Docker...${RESET}" if [ -f /etc/alpine-release ]; then apk add --no-cache docker docker-cli-compose else echo -e "${YELLOW}将从 https://get.docker.com 下载 Docker 官方安装脚本并直接管道执行。${RESET}" echo -e "${YELLOW}注意:管道执行远程脚本存在供应链风险,请确认你信任该来源。${RESET}" printf "确认继续安装 Docker?[y/N]: " read -r confirm_pipe_install if [ "$confirm_pipe_install" != "y" ] && [ "$confirm_pipe_install" != "Y" ]; then echo -e "${RED}已取消 Docker 安装。${RESET}" exit 1 fi curl -fsSL https://get.docker.com | sh fi if ! command -v docker >/dev/null 2>&1; then echo -e "${RED}Docker 安装失败,请手动安装。${RESET}" exit 1 fi echo -e "${GREEN}✓ Docker 安装成功${RESET}" fi # 每次都校正开机自启,已装过的机器重跑脚本也能修好 enable_docker_on_boot if ! docker info >/dev/null 2>&1; then echo -e "${YELLOW}Docker 服务未运行,正在尝试启动...${RESET}" if [ -f /etc/alpine-release ]; then rc-service docker start else systemctl start docker fi # dockerd 刚拉起时 socket 还没就绪,最多等 30 秒 local i=0 while ! docker info >/dev/null 2>&1 && [ "$i" -lt 30 ]; do sleep 1 i=$((i + 1)) done if ! docker info >/dev/null 2>&1; then echo -e "${RED}Docker 服务启动失败,请手动启动。${RESET}" exit 1 fi fi echo -e "${GREEN}✓ Docker 环境检查通过${RESET}" } # --- 版本选择与获取 --- 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 (RC)" while true; do printf "请输入选项 [1-3]: " read -r 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 (RC)${RESET}"; echo -e "${YELLOW}注意:v6 仍为预发布版本,协议可能存在不兼容更新${RESET}"; echo -e "${YELLOW}v6 已移除 QUIC 代理模式与 obfs,且不提供 armv7l 构建${RESET}"; echo -e "${YELLOW}加密模式:mode = ${SNELL_MODE}(客户端需配置相同的 mode)${RESET}"; break ;; *) echo -e "${RED}请输入正确的选项 [1-3]${RESET}" ;; esac done } # 读取已安装 v6 容器使用的 mode(读不到时回落到默认值) get_snell_mode() { local conf_file="/etc/snell-docker/snell-server.conf" local mode="" if [ -f "$conf_file" ]; then mode=$(grep -E '^[[:space:]]*mode[[:space:]]*=' "$conf_file" | head -n 1 | sed 's/^[^=]*=[[:space:]]*//' | tr -d ' ') fi if [ -z "$mode" ]; then mode="$SNELL_MODE" fi echo "$mode" } # Snell 官方发布页(旧的 manual.nssurge.com/others/snell.html 已下线) SNELL_RELEASE_NOTES_URL="https://kb.nssurge.com/surge-knowledge-base/release-notes/snell" SNELL_RELEASE_NOTES_URL_ZH="https://kb.nssurge.com/surge-knowledge-base/zh/release-notes/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 echo -e "${GREEN}获取到版本: ${SNELL_VERSION}${RESET}" } # --- 端口和配置 --- get_user_port() { while true; do printf "请输入要使用的端口号 (1-65535), 回车默认 [随机]: " read -r PORT if [ -z "$PORT" ]; then PORT=$(shuf -i 20000-65000 -n 1) echo -e "${YELLOW}使用随机端口: $PORT${RESET}" break fi case "$PORT" in ''|*[!0-9]*) echo -e "${RED}无效输入,请输入纯数字。${RESET}" continue ;; esac if [ "$PORT" -ge 1 ] && [ "$PORT" -le 65535 ]; then echo -e "${GREEN}已选择端口: $PORT${RESET}" break else echo -e "${RED}无效端口号,请输入 1 到 65535 之间的数字。${RESET}" fi done } # --- Docker 相关函数 --- create_dockerfile() { local snell_url snell_url=$(snell_download_url_for "$SNELL_VERSION_CHOICE" "$SNELL_VERSION") || exit 1 local arch=$(uname -m) local gnu_lib_dir="" local ld_linker_cmd="" case ${arch} in "x86_64"|"amd64") gnu_lib_dir="x86_64-linux-gnu" ld_linker_cmd="mkdir -p /lib64 && ln -sf /usr/glibc-compat/lib/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2" ;; "aarch64"|"arm64") gnu_lib_dir="aarch64-linux-gnu" ld_linker_cmd="ln -sf /usr/glibc-compat/lib/ld-linux-aarch64.so.1 /lib/ld-linux-aarch64.so.1" ;; "armv7l"|"armv7") gnu_lib_dir="arm-linux-gnueabihf" ld_linker_cmd="ln -sf /usr/glibc-compat/lib/ld-linux-armhf.so.3 /lib/ld-linux-armhf.so.3" ;; esac echo -e "${CYAN}创建 Dockerfile (多阶段构建: Debian提取glibc + Alpine运行)...${RESET}" # 准备 entrypoint.sh:容器首次启动时由它自动生成配置与 PSK, # 不再把含明文 PSK 的配置文件烘焙进镜像层 # 从仓库里运行时用本地的 docker/entrypoint.sh(脚本在 scripts/ 下) if [ -f "${SCRIPT_DIR}/../docker/entrypoint.sh" ]; then cp "${SCRIPT_DIR}/../docker/entrypoint.sh" ./entrypoint.sh echo -e "${GREEN}✓ 已复用 entrypoint.sh${RESET}" else echo -e "${CYAN}正在下载 entrypoint.sh...${RESET}" if ! curl -fsSL -o ./entrypoint.sh "${SNELL_RAW_BASE}/docker/entrypoint.sh"; then echo -e "${RED}✗ entrypoint.sh 下载失败${RESET}" return 1 fi if [ ! -s ./entrypoint.sh ] || ! sh -n ./entrypoint.sh 2>/dev/null; then echo -e "${RED}✗ entrypoint.sh 校验失败(空文件或语法错误),已丢弃${RESET}" rm -f ./entrypoint.sh return 1 fi echo -e "${GREEN}✓ entrypoint.sh 下载并校验通过${RESET}" fi cat > Dockerfile << EOF # 第一阶段: 使用 Debian 下载二进制并提供 glibc 运行时库 FROM debian:bookworm-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends curl unzip ca-certificates && \\ rm -rf /var/lib/apt/lists/* WORKDIR /app RUN curl -L -o snell.zip "${snell_url}" && \\ unzip -o snell.zip && \\ rm -f snell.zip && \\ chmod +x /app/snell-server # 第二阶段: Alpine 最终镜像,注入 glibc 运行时 FROM alpine:3.19 RUN apk add --no-cache ca-certificates WORKDIR /app COPY --from=builder /app/snell-server /app/snell-server COPY --from=builder /lib/${gnu_lib_dir}/ /usr/glibc-compat/lib/ # Snell may depend on libstdc++ and other runtime libraries under /usr/lib. COPY --from=builder /usr/lib/${gnu_lib_dir}/ /usr/glibc-compat/lib/ RUN ${ld_linker_cmd} ENV LD_LIBRARY_PATH=/usr/glibc-compat/lib RUN mkdir -p /etc/snell # 不再 COPY 含明文 PSK 的配置文件:配置与 PSK 由 entrypoint.sh 在容器首次启动时自动生成, # 经 -v /etc/snell-docker:/etc/snell 持久化到宿主机,避免 secret 烘焙进镜像层 COPY entrypoint.sh /app/entrypoint.sh RUN chmod +x /app/entrypoint.sh ENV SNELL_PORT=${PORT} ENV SNELL_VER=${SNELL_VERSION_CHOICE} ENV SNELL_MODE=${SNELL_MODE} EXPOSE ${PORT}/tcp ${PORT}/udp ENTRYPOINT ["/app/entrypoint.sh"] EOF echo -e "${GREEN}✓ Dockerfile 创建完成${RESET}" } create_config_file() { echo -e "${CYAN}准备配置目录...${RESET}" # 配置目录经 -v 挂载进容器;配置文件与 PSK 由容器内 entrypoint.sh 在首次启动时自动生成, # 不再把含明文 PSK 的配置文件烘焙进镜像层 mkdir -p /etc/snell-docker # 容器以 nobody(uid 65534)运行时,挂载目录必须对其可写,否则 entrypoint 无法生成配置 chown -R 65534:65533 /etc/snell-docker 2>/dev/null || \ echo -e "${YELLOW}提示: 无法设置 /etc/snell-docker 属主,若容器启动失败请手动执行 chown -R 65534:65533 /etc/snell-docker${RESET}" # 保存配置信息到变量 SNELL_PORT="$PORT" echo -e "${GREEN}✓ 配置目录就绪: /etc/snell-docker${RESET}" echo -e "${YELLOW}端口: ${PORT}${RESET}" echo -e "${YELLOW}PSK 将在容器首次启动时自动生成,可通过 docker logs 查看客户端配置${RESET}" } build_docker_image() { echo -e "${CYAN}构建 Docker 镜像...${RESET}" if docker build -t "${IMAGE_NAME}:${SNELL_VERSION}" -t "${IMAGE_NAME}:latest" .; then echo -e "${GREEN}✓ Docker 镜像构建成功${RESET}" else echo -e "${RED}✗ Docker 镜像构建失败${RESET}" return 1 fi } start_snell_container() { echo -e "${CYAN}启动 Snell 容器...${RESET}" # 停止并删除可能存在的旧容器 docker stop "${CONTAINER_NAME}" 2>/dev/null || true docker rm "${CONTAINER_NAME}" 2>/dev/null || true # 第一步:不带 --restart 先测试启动,避免崩溃循环 echo -e "${CYAN}测试启动容器...${RESET}" local run_output # -v /etc/snell-docker:/etc/snell:配置与 PSK 由 entrypoint 在首次启动时生成并持久化到宿主机 run_output=$(docker run -d \ --name "${CONTAINER_NAME}" \ -p "${SNELL_PORT}:${SNELL_PORT}/tcp" \ -p "${SNELL_PORT}:${SNELL_PORT}/udp" \ -v /etc/snell-docker:/etc/snell \ "${IMAGE_NAME}:latest" 2>&1) if [ $? -ne 0 ]; then echo -e "${RED}✗ 容器创建失败${RESET}" echo " $run_output" return 1 fi echo -e "${GREEN}✓ 容器已创建${RESET}" # 等待启动 echo -e "${CYAN}等待服务启动...${RESET}" sleep 5 # 检查容器是否在运行 local container_status=$(docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null) local exit_code=$(docker inspect --format='{{.State.ExitCode}}' "${CONTAINER_NAME}" 2>/dev/null) if [ "$container_status" = "running" ]; then # 再等几秒确认稳定性 sleep 3 container_status=$(docker inspect --format='{{.State.Status}}' "${CONTAINER_NAME}" 2>/dev/null) fi if [ "$container_status" != "running" ]; then echo -e "${RED}✗ 容器启动失败,状态: $container_status,退出码: $exit_code${RESET}" echo -e "${CYAN}容器日志:${RESET}" docker logs "${CONTAINER_NAME}" 2>&1 | sed 's/^/ /' if [ "$exit_code" = "127" ]; then echo -e "${RED}退出码 127 = 程序或命令找不到${RESET}" echo -e "${YELLOW}检查镜像内容...${RESET}" # 使用 sh 启动一个临时容器检查镜像内容 docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true echo -e "${CYAN}镜像内 /app 目录:${RESET}" docker run --rm --entrypoint sh "${IMAGE_NAME}:latest" -c "ls -la /app/ 2>&1" 2>&1 | sed 's/^/ /' echo -e "${CYAN}镜像内 /etc/snell 目录:${RESET}" docker run --rm --entrypoint sh -v /etc/snell-docker:/etc/snell "${IMAGE_NAME}:latest" -c "ls -la /etc/snell/ 2>&1" 2>&1 | sed 's/^/ /' echo -e "${CYAN}二进制文件信息:${RESET}" docker run --rm --entrypoint sh "${IMAGE_NAME}:latest" -c "file /app/snell-server 2>&1" 2>&1 | sed 's/^/ /' echo -e "${CYAN}手动执行测试:${RESET}" docker run --rm --entrypoint sh -v /etc/snell-docker:/etc/snell "${IMAGE_NAME}:latest" -c "/app/snell-server -c /etc/snell/snell-server.conf" 2>&1 | head -5 | sed 's/^/ /' fi docker rm -f "${CONTAINER_NAME}" 2>/dev/null || true return 1 fi echo -e "${GREEN}✓ 容器运行正常${RESET}" # 检查端口映射 local port_mapping=$(docker port "${CONTAINER_NAME}" 2>/dev/null) if [ -n "$port_mapping" ]; then echo -e "${GREEN}✓ 端口映射: $port_mapping${RESET}" fi # 显示容器日志 echo -e "${CYAN}容器日志:${RESET}" docker logs "${CONTAINER_NAME}" 2>&1 | tail -5 | sed 's/^/ /' # 测试成功,现在重建并启用自动重启 echo -e "${CYAN}启用自动重启策略...${RESET}" docker update --restart unless-stopped "${CONTAINER_NAME}" 2>/dev/null || true echo -e "${GREEN}✓ Snell 容器启动成功${RESET}" return 0 } # --- 安装函数 --- install_snell() { check_root ensure_curl check_docker # 检查是否已有容器运行 if container_exists; then echo -e "${YELLOW}检测到已存在的 Snell 容器,是否要重新安装?${RESET}" printf "输入 y 继续,其他键取消: " read -r confirm if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then return fi uninstall_snell fi select_snell_version # v6 不支持 armv7l local _arch=$(uname -m) if [ "$SNELL_VERSION_CHOICE" = "v6" ] && { [ "$_arch" = "armv7l" ] || [ "$_arch" = "armv7" ]; }; then echo -e "${RED}Snell v6 暂不支持 armv7l 架构,请选择 v4 或 v5。${RESET}" return 1 fi get_latest_snell_version get_user_port # 创建临时工作目录 WORK_DIR="/tmp/snell-docker-$(date +%s)" mkdir -p "$WORK_DIR" cd "$WORK_DIR" echo -e "${CYAN}开始 Docker 方式安装 Snell ${SNELL_VERSION}...${RESET}" # 创建配置文件 if ! create_config_file; then echo -e "${RED}安装失败!${RESET}" cd - >/dev/null rm -rf "$WORK_DIR" return 1 fi # 创建 Dockerfile if ! create_dockerfile; then echo -e "${RED}安装失败!${RESET}" cd - >/dev/null rm -rf "$WORK_DIR" return 1 fi # 构建镜像 if ! build_docker_image; then echo -e "${RED}安装失败!${RESET}" cd - >/dev/null rm -rf "$WORK_DIR" return 1 fi # 启动容器 if ! start_snell_container; then echo -e "${RED}安装失败!${RESET}" cd - >/dev/null rm -rf "$WORK_DIR" return 1 fi # 清理临时文件 cd - >/dev/null rm -rf "$WORK_DIR" echo -e "${GREEN}🎉 Snell Server 安装完成!${RESET}" # 配置防火墙 configure_firewall # 显示配置信息 show_information } # --- 管理函数 --- uninstall_snell() { check_root echo -e "${CYAN}正在卸载 Snell Docker 容器...${RESET}" # 停止并删除容器 if docker stop "${CONTAINER_NAME}" 2>/dev/null; then echo -e "${GREEN}✓ 容器已停止${RESET}" fi if docker rm "${CONTAINER_NAME}" 2>/dev/null; then echo -e "${GREEN}✓ 容器已删除${RESET}" fi # 询问是否删除镜像 printf "是否同时删除 Docker 镜像?[y/N]: " read -r remove_image if [ "$remove_image" = "y" ] || [ "$remove_image" = "Y" ]; then docker rmi "${IMAGE_NAME}:latest" 2>/dev/null || true docker rmi $(docker images "${IMAGE_NAME}" -q) 2>/dev/null || true echo -e "${GREEN}✓ Docker 镜像已删除${RESET}" fi # 防火墙里为它开的端口关掉(端口从配置里读,所以在删配置之前) if [ -f "/etc/snell-docker/snell-server.conf" ]; then old_port=$(grep 'listen' /etc/snell-docker/snell-server.conf | sed 's/.*://' | tr -d ' ') [ -n "$old_port" ] && close_port "$old_port" fi # 清理配置文件 if [ -d "/etc/snell-docker" ]; then rm -rf /etc/snell-docker echo -e "${GREEN}✓ 配置文件已删除${RESET}" fi echo -e "${GREEN}🗑️ Snell 已完全卸载${RESET}" } restart_snell() { check_root if ! container_exists; then echo -e "${RED}错误: Snell 容器不存在${RESET}" return 1 fi echo -e "${CYAN}正在重启 Snell 容器...${RESET}" if docker restart "${CONTAINER_NAME}"; then sleep 2 if container_running; then echo -e "${GREEN}✓ Snell 容器重启成功${RESET}" else echo -e "${RED}✗ 容器重启后异常${RESET}" echo -e "${YELLOW}查看日志: docker logs ${CONTAINER_NAME}${RESET}" fi else echo -e "${RED}✗ 容器重启失败${RESET}" fi } check_status() { echo -e "${CYAN}=== Snell Docker 容器状态 ===${RESET}" if container_exists; then echo -e "\n${CYAN}容器信息:${RESET}" docker ps -a --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" echo -e "\n${CYAN}资源使用情况:${RESET}" docker stats "${CONTAINER_NAME}" --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" echo -e "\n${CYAN}容器日志 (最后10行):${RESET}" docker logs --tail 10 "${CONTAINER_NAME}" else echo -e "${RED}Snell 容器不存在${RESET}" fi } # 网络连接诊断 network_diagnosis() { echo -e "${CYAN}🔍 开始 Snell 网络连接诊断...${RESET}" echo -e "${CYAN}===========================================${RESET}" # 检查配置文件是否存在 if [ ! -f "/etc/snell-docker/snell-server.conf" ]; then echo -e "${RED}❌ 配置文件不存在,请先安装 Snell${RESET}" return 1 fi # 读取配置 local port=$(grep 'listen' /etc/snell-docker/snell-server.conf | sed 's/.*://') local psk=$(grep 'psk' /etc/snell-docker/snell-server.conf | sed 's/psk[[:space:]]*=[[:space:]]*//') local version_choice version_choice=$(grep 'version-choice' /etc/snell-docker/snell-server.conf 2>/dev/null | sed 's/version-choice[[:space:]]*=[[:space:]]*//') if [ -z "$version_choice" ]; then version_choice="v4" if docker images "${IMAGE_NAME}" --format '{{.Tag}}' 2>/dev/null | grep -q 'v6'; then version_choice="v6" elif docker images "${IMAGE_NAME}" --format '{{.Tag}}' 2>/dev/null | grep -q 'v5'; then version_choice="v5" fi fi echo -e "${YELLOW}📋 服务配置信息:${RESET}" echo " 端口: $port" echo " 版本: $version_choice" echo " PSK: $(printf '%s' "$psk" | cut -c 1-10)..." echo "" # 1. 检查容器状态 echo -e "${CYAN}1️⃣ 检查容器状态...${RESET}" if container_running; then echo -e " ${GREEN}✅ 容器正在运行${RESET}" local container_ports=$(docker port "${CONTAINER_NAME}") echo " 端口映射: $container_ports" else echo -e " ${RED}❌ 容器未运行${RESET}" if container_exists; then echo -e " ${YELLOW}⚠️ 容器存在但已停止,尝试启动...${RESET}" docker start "${CONTAINER_NAME}" sleep 2 else echo -e " ${RED}❌ 容器不存在,请重新安装${RESET}" return 1 fi fi echo "" # 2. 检查 Docker 端口映射 echo -e "${CYAN}2️⃣ 检查 Docker 端口映射...${RESET}" local port_mapping=$(docker port "${CONTAINER_NAME}" 2>/dev/null) if [ -n "$port_mapping" ]; then echo -e " ${GREEN}✅ Docker 端口映射存在${RESET}" echo " $port_mapping" | sed 's/^/ /' # 检查宿主机端口是否在监听 if netstat -tlnp 2>/dev/null | grep ":$port " | grep -q LISTEN; then echo -e " ${GREEN}✅ 宿主机端口 $port 正在监听${RESET}" netstat -tlnp 2>/dev/null | grep ":$port " | head -1 | sed 's/^/ /' else echo -e " ${RED}❌ 宿主机端口 $port 未在监听${RESET}" fi else echo -e " ${RED}❌ Docker 端口映射不存在或异常${RESET}" echo -e " ${YELLOW}检查容器详细状态...${RESET}" # 详细检查容器状态 local container_status=$(docker ps -a --filter "name=${CONTAINER_NAME}" --format "{{.Status}}" 2>/dev/null) echo " 容器状态: $container_status" # 检查镜像是否存在 if ! docker images | grep -q "${IMAGE_NAME}"; then echo -e " ${RED}❌ Docker 镜像不存在,需要重新安装${RESET}" echo -e " ${YELLOW}请选择主菜单选项 1 重新安装 Snell${RESET}" return 1 fi echo -e " ${YELLOW}尝试重建容器...${RESET}" # 停止并删除容器 docker stop "${CONTAINER_NAME}" 2>/dev/null || true docker rm "${CONTAINER_NAME}" 2>/dev/null || true # 重新启动容器(配置已内置于镜像,无需挂载) echo -e " ${CYAN}重新创建容器...${RESET}" if docker run -d \ --name "${CONTAINER_NAME}" \ --restart unless-stopped \ -p "${port}:${port}/tcp" \ -p "${port}:${port}/udp" \ "${IMAGE_NAME}:latest" 2>/dev/null; then echo -e " ${GREEN}✅ 容器已重新创建${RESET}" sleep 5 # 验证新容器 local new_restarting=$(docker inspect --format='{{.State.Restarting}}' "${CONTAINER_NAME}" 2>/dev/null) if [ "$new_restarting" = "true" ]; then echo -e " ${RED}❌ 容器仍在重启,建议选项1重新安装${RESET}" docker logs --tail 5 "${CONTAINER_NAME}" 2>&1 | sed 's/^/ /' else local new_port_mapping=$(docker port "${CONTAINER_NAME}" 2>/dev/null) if [ -n "$new_port_mapping" ]; then echo -e " ${GREEN}✅ 端口映射已修复: $new_port_mapping${RESET}" else echo -e " ${RED}❌ 端口映射仍有问题${RESET}" fi fi else echo -e " ${RED}❌ 容器重新创建失败${RESET}" echo -e " ${YELLOW}可能是镜像问题,建议重新安装${RESET}" fi fi echo "" # 3. 检查防火墙规则 echo -e "${CYAN}3️⃣ 检查防火墙规则...${RESET}" if command -v iptables >/dev/null 2>&1; then if iptables -L INPUT -n | grep -q "dpt:$port"; then echo -e " ${GREEN}✅ iptables 规则存在${RESET}" iptables -L INPUT -n | grep "dpt:$port" | head -1 | sed 's/^/ /' else echo -e " ${RED}❌ iptables 规则不存在${RESET}" echo -e " ${YELLOW}尝试添加规则...${RESET}" iptables -I INPUT -p tcp --dport "$port" -j ACCEPT 2>/dev/null && \ echo -e " ${GREEN}✅ iptables 规则已添加${RESET}" || \ echo -e " ${RED}❌ 添加 iptables 规则失败${RESET}" fi else echo -e " ${YELLOW}⚠️ iptables 不可用${RESET}" fi if command -v ufw >/dev/null 2>&1; then if ufw status | grep -q "$port"; then echo -e " ${GREEN}✅ UFW 规则存在${RESET}" else echo -e " ${YELLOW}⚠️ UFW 规则可能不存在${RESET}" fi fi echo "" # 4. 外部端口连通性测试 echo -e "${CYAN}4️⃣ 测试外部端口连通性...${RESET}" local server_ip=$(curl -s4 --connect-timeout 5 https://api.ipify.org 2>/dev/null) if [ -n "$server_ip" ]; then echo " 服务器 IP: $server_ip" # 使用多种方法测试端口 echo -e " ${CYAN}测试端口 $port...${RESET}" # 方法1: 使用 nc (netcat) if command -v nc >/dev/null 2>&1; then if timeout 5 nc -z "$server_ip" "$port" 2>/dev/null; then echo -e " ${GREEN}✅ nc 测试通过${RESET}" else echo -e " ${RED}❌ nc 测试失败${RESET}" fi fi # 方法2: 使用 telnet if command -v telnet >/dev/null 2>&1; then if timeout 5 sh -c "echo '' | telnet $server_ip $port" 2>/dev/null | grep -q Connected; then echo -e " ${GREEN}✅ telnet 测试通过${RESET}" else echo -e " ${RED}❌ telnet 测试失败${RESET}" fi fi # 方法3: 使用在线端口检测 echo -e " ${YELLOW}💡 在线端口检测工具:${RESET}" echo " https://www.yougetsignal.com/tools/open-ports/" echo " https://tool.chinaz.com/port/" echo " 输入 IP: $server_ip, 端口: $port" else echo -e " ${RED}❌ 无法获取服务器 IP${RESET}" fi echo "" # 5. 容器内部详细检查 echo -e "${CYAN}5️⃣ 容器内部详细检查...${RESET}" if container_running; then # 检查容器内部端口 echo -e " ${CYAN}检查容器内部端口监听...${RESET}" local container_netstat=$(docker exec "${CONTAINER_NAME}" netstat -tln 2>/dev/null | grep ":$port " || echo "") if [ -n "$container_netstat" ]; then echo -e " ${GREEN}✅ 容器内端口正常监听${RESET}" echo " $container_netstat" | sed 's/^/ /' else echo -e " ${RED}❌ 容器内端口未监听${RESET}" fi # 检查 Snell 进程 echo -e " ${CYAN}检查 Snell 进程状态...${RESET}" local snell_process=$(docker exec "${CONTAINER_NAME}" ps aux 2>/dev/null | grep -v grep | grep snell || echo "") if [ -n "$snell_process" ]; then echo -e " ${GREEN}✅ Snell 进程正在运行${RESET}" echo "$snell_process" | sed 's/^/ /' # 检查进程状态是否正常 if echo "$snell_process" | grep -q "\[snell-server\]"; then echo -e " ${RED}⚠️ 检测到僵尸进程格式,Snell 可能未正常启动${RESET}" echo -e " ${YELLOW}尝试重新启动 Snell 服务...${RESET}" # 杀死可能的僵尸进程 docker exec "${CONTAINER_NAME}" pkill -f snell 2>/dev/null || true sleep 1 # 重新启动 Snell echo -e " ${CYAN}重新启动 Snell 服务...${RESET}" if docker exec -d "${CONTAINER_NAME}" sh -c "cd /app && exec ./snell-server -c /etc/snell/snell-server.conf" 2>/dev/null; then sleep 3 local new_snell_process=$(docker exec "${CONTAINER_NAME}" ps aux 2>/dev/null | grep -v grep | grep snell || echo "") if [ -n "$new_snell_process" ]; then echo -e " ${GREEN}✓ Snell 服务重启成功${RESET}" echo "$new_snell_process" | sed 's/^/ RESTART: /' else echo -e " ${RED}✗ Snell 服务重启失败${RESET}" fi else echo -e " ${RED}✗ 重启命令执行失败${RESET}" fi fi else echo -e " ${RED}❌ Snell 进程未运行${RESET}" fi # 检查配置文件 echo -e " ${CYAN}检查配置文件...${RESET}" local ls_result=$(docker exec "${CONTAINER_NAME}" ls -la /etc/snell/ 2>/dev/null || echo "") if [ -n "$ls_result" ]; then echo -e " ${GREEN}✅ 配置目录存在${RESET}" echo "$ls_result" | sed 's/^/ /' # 检查配置文件内容 local config_content=$(docker exec "${CONTAINER_NAME}" cat /etc/snell/snell-server.conf 2>/dev/null || echo "") if [ -n "$config_content" ]; then echo -e " ${GREEN}✅ 配置文件存在且有内容${RESET}" echo "$config_content" | sed 's/^/ /' else echo -e " ${RED}❌ 配置文件不存在或为空${RESET}" echo -e " ${YELLOW}配置文件应内置于镜像中,建议选项1重新安装${RESET}" fi else echo -e " ${RED}❌ 配置目录不存在${RESET}" echo -e " ${YELLOW}这表明容器挂载有严重问题${RESET}" fi # 检查 Snell 二进制文件 echo -e " ${CYAN}检查 Snell 二进制文件...${RESET}" local snell_binary=$(docker exec "${CONTAINER_NAME}" ls -la /app/snell-server 2>/dev/null || echo "") if [ -n "$snell_binary" ]; then echo -e " ${GREEN}✅ Snell 二进制文件存在${RESET}" echo "$snell_binary" | sed 's/^/ /' # 尝试手动运行测试 echo -e " ${CYAN}测试 Snell 二进制文件...${RESET}" local snell_test=$(docker exec "${CONTAINER_NAME}" timeout 3s /app/snell-server --help 2>&1 || echo "") if echo "$snell_test" | grep -q "usage\|help\|version"; then echo -e " ${GREEN}✅ Snell 二进制文件可执行${RESET}" else echo -e " ${RED}❌ Snell 二进制文件可能有问题${RESET}" echo "$snell_test" | sed 's/^/ /' | head -3 # 检查二进制文件完整性 echo -e " ${CYAN}检查二进制文件完整性...${RESET}" local file_info=$(docker exec "${CONTAINER_NAME}" file /app/snell-server 2>/dev/null || echo "") echo " 文件信息: $file_info" local file_size=$(docker exec "${CONTAINER_NAME}" stat -c%s /app/snell-server 2>/dev/null || echo "0") echo " 文件大小: $file_size bytes" if [ "$file_size" -lt 100000 ]; then echo -e " ${RED}❌ binary文件大小异常,可能下载不完整${RESET}" fi fi else echo -e " ${RED}❌ Snell 二进制文件不存在${RESET}" echo -e " ${YELLOW}这表明 Docker 镜像构建失败${RESET}" echo -e " ${YELLOW}需要重新安装以重建镜像${RESET}" fi # 检查完整的容器日志 echo -e " ${CYAN}检查完整容器日志...${RESET}" local full_logs=$(docker logs --tail 50 "${CONTAINER_NAME}" 2>&1) if [ -n "$full_logs" ]; then echo -e " ${YELLOW}最近日志 (最后10行):${RESET}" echo "$full_logs" | tail -10 | sed 's/^/ /' # 检查特定错误 local error_logs=$(echo "$full_logs" | grep -i "error\|failed\|refused\|cannot\|permission\|bind" || echo "") if [ -n "$error_logs" ]; then echo -e " ${RED}发现错误日志:${RESET}" echo "$error_logs" | sed 's/^/ /' fi else echo -e " ${YELLOW}无日志输出${RESET}" fi # 尝试修复 local needs_fix=false if [ -z "$snell_process" ]; then needs_fix=true elif [ -z "$container_netstat" ]; then needs_fix=true fi if [ "$needs_fix" = "true" ]; then echo -e " ${YELLOW}尝试修复容器...${RESET}" echo -e " ${CYAN}重启容器中的 Snell 服务...${RESET}" # 清理可能的僵尸进程 docker exec "${CONTAINER_NAME}" pkill -f snell 2>/dev/null || true docker exec "${CONTAINER_NAME}" pkill -f "sleep" 2>/dev/null || true sleep 2 # 方法1: 尝试直接启动 echo -e " ${CYAN}尝试直接启动 Snell 服务...${RESET}" if docker exec -d "${CONTAINER_NAME}" sh -c "cd /app && exec ./snell-server -c /etc/snell/snell-server.conf" 2>/dev/null; then sleep 5 # 检查是否启动成功 local fixed_process=$(docker exec "${CONTAINER_NAME}" ps aux 2>/dev/null | grep -v grep | grep snell || echo "") local fixed_netstat=$(docker exec "${CONTAINER_NAME}" netstat -tln 2>/dev/null | grep ":$port " || echo "") if [ -n "$fixed_process" ] && [ -n "$fixed_netstat" ]; then echo -e " ${GREEN}✅ Snell 服务修复成功${RESET}" echo "$fixed_process" | sed 's/^/ FIX: /' echo "$fixed_netstat" | sed 's/^/ PORT: /' elif [ -n "$fixed_process" ]; then echo -e " ${YELLOW}⚠️ 进程已启动但端口仍未监听${RESET}" echo "$fixed_process" | sed 's/^/ FIX: /' else echo -e " ${RED}❌ 手动启动失败,建议重建容器${RESET}" # 显示启动错误信息 echo -e " ${CYAN}检查启动错误...${RESET}" local start_error=$(docker exec "${CONTAINER_NAME}" sh -c "cd /app && timeout 5s ./snell-server -c /etc/snell/snell-server.conf" 2>&1 || echo "") if [ -n "$start_error" ]; then echo " 启动错误: $start_error" | head -3 fi fi else echo -e " ${RED}❌ 启动命令执行失败${RESET}" fi fi fi echo "" # 6. 生成客户端配置 echo -e "${CYAN}6️⃣ 客户端配置检查...${RESET}" if [ -n "$server_ip" ]; then echo -e " ${GREEN}Surge 配置:${RESET}" if [ "$version_choice" = "v6" ]; then echo " MySnell = snell, $server_ip, $port, psk=$psk, version=6, mode=$(get_snell_mode), reuse=true, tfo=true" elif [ "$version_choice" = "v5" ]; then echo " MySnell_v4 = snell, $server_ip, $port, psk=$psk, version=4, reuse=true, tfo=true" echo " MySnell_v5 = snell, $server_ip, $port, psk=$psk, version=5, reuse=true, tfo=true" else echo " MySnell = snell, $server_ip, $port, psk=$psk, version=4, reuse=true, tfo=true" fi echo "" echo -e " ${YELLOW}确保客户端配置与上述配置完全一致${RESET}" fi echo "" # 7. 自动修复建议和操作 echo -e "${CYAN}🔧 自动修复选项:${RESET}" echo -e "${YELLOW}检测到的问题:${RESET}" local issues_found=0 local port_mapping=$(docker port "${CONTAINER_NAME}" 2>/dev/null) local snell_process=$(docker exec "${CONTAINER_NAME}" ps aux 2>/dev/null | grep -v grep | grep snell || echo "") local container_listening=$(docker exec "${CONTAINER_NAME}" netstat -tln 2>/dev/null | grep ":$port " || echo "") if [ -z "$port_mapping" ]; then echo " • Docker 端口映射缺失" issues_found=$((issues_found + 1)) fi if [ -z "$snell_process" ]; then echo " • Snell 进程未运行" issues_found=$((issues_found + 1)) elif echo "$snell_process" | grep -q "\[snell-server\]"; then echo " • Snell 进程可能是僵尸进程" issues_found=$((issues_found + 1)) fi if [ -z "$container_listening" ]; then echo " • 容器内端口未监听" issues_found=$((issues_found + 1)) fi if [ "$issues_found" -gt 0 ]; then echo "" echo -e "${CYAN}修复选项:${RESET}" echo -e "${YELLOW}1. 快速修复 (重启容器):${RESET}" echo " docker restart ${CONTAINER_NAME}" echo "" echo -e "${YELLOW}2. 重建容器 (推荐):${RESET}" echo " 选择脚本主菜单的选项 1 重新安装" echo "" echo -e "${YELLOW}3. 手动修复命令:${RESET}" echo " # 停止当前容器" echo " docker stop ${CONTAINER_NAME}" echo " docker rm ${CONTAINER_NAME}" echo "" echo " # 重新创建容器" echo " docker run -d \\" echo " --name ${CONTAINER_NAME} \\" echo " --restart unless-stopped \\" echo " -p ${port}:${port}/tcp \\" echo " -p ${port}:${port}/udp \\" echo " ${IMAGE_NAME}:latest" echo "" # 提供一键修复选项 echo -e "${GREEN}是否要执行自动修复? [y/N]:${RESET}" printf " " read -r fix_choice if [ "$fix_choice" = "y" ] || [ "$fix_choice" = "Y" ]; then echo -e "${CYAN}执行自动修复...${RESET}" # 停止并删除容器 docker stop "${CONTAINER_NAME}" 2>/dev/null || true docker rm "${CONTAINER_NAME}" 2>/dev/null || true # 重新创建容器(配置已内置于镜像) if docker run -d \ --name "${CONTAINER_NAME}" \ --restart unless-stopped \ -p "${port}:${port}/tcp" \ -p "${port}:${port}/udp" \ "${IMAGE_NAME}:latest" 2>/dev/null; then echo -e " ${GREEN}✅ 容器重建成功${RESET}" sleep 8 # 给更多时间启动 # 验证修复结果 echo -e " ${CYAN}验证修复结果...${RESET}" if container_running; then local new_port_mapping=$(docker port "${CONTAINER_NAME}" 2>/dev/null) if [ -n "$new_port_mapping" ]; then echo -e " ${GREEN}✅ 端口映射已恢复: $new_port_mapping${RESET}" # 等待服务启动并检查 sleep 5 local new_snell_process=$(docker exec "${CONTAINER_NAME}" ps aux 2>/dev/null | grep -v grep | grep snell || echo "") local new_port_listen=$(docker exec "${CONTAINER_NAME}" netstat -tln 2>/dev/null | grep ":$port " || echo "") if [ -n "$new_snell_process" ] && [ -n "$new_port_listen" ]; then echo -e " ${GREEN}✅ Snell 服务已启动并监听端口${RESET}" echo "$new_snell_process" | sed 's/^/ PROCESS: /' echo "$new_port_listen" | sed 's/^/ LISTEN: /' echo -e " ${GREEN}🎉 修复成功!请重新测试连接${RESET}" elif [ -n "$new_snell_process" ]; then echo -e " ${YELLOW}⚠️ 服务已启动但端口未监听${RESET}" echo "$new_snell_process" | sed 's/^/ PROCESS: /' echo -e " ${YELLOW}可能需要等待更长时间或重新安装${RESET}" else echo -e " ${YELLOW}⚠️ 容器已启动,但 Snell 服务可能还在启动中...${RESET}" echo -e " ${YELLOW}请等待 10-20 秒后重新运行诊断${RESET}" fi else echo -e " ${RED}❌ 端口映射仍然有问题${RESET}" echo -e " ${YELLOW}请检查容器状态:${RESET}" docker ps --filter "name=${CONTAINER_NAME}" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | sed 's/^/ /' fi else echo -e " ${RED}❌ 容器启动失败${RESET}" echo -e " ${CYAN}容器启动日志:${RESET}" docker logs --tail 10 "${CONTAINER_NAME}" 2>&1 | sed 's/^/ /' fi else echo -e " ${RED}❌ 容器重建失败${RESET}" echo -e " ${YELLOW}可能是镜像问题,建议选择选项1重新安装${RESET}" fi fi else echo " ${GREEN}✅ 未发现明显问题${RESET}" echo " ${YELLOW}如果仍无法连接,可能是 VPS 提供商防火墙限制${RESET}" fi echo "" echo -e "${CYAN}🔧 常见问题解决方案:${RESET}" echo -e "${CYAN}🔧 通用解决方案:${RESET}" echo -e "${YELLOW}1. VPS 提供商防火墙:${RESET}" echo " ▸ 登录 VPS 控制面板,开放端口 $port" echo " ▸ 阿里云/腾讯云等需要在安全组中开放端口" echo "" echo -e "${YELLOW}2. 网络运营商限制:${RESET}" echo " ▸ 某些端口可能被运营商封禁" echo " ▸ 尝试更换端口: docker stop snell-server && 重新安装" echo "" echo -e "${YELLOW}3. 容器重启:${RESET}" echo " ▸ docker restart ${CONTAINER_NAME}" echo " ▸ docker logs -f ${CONTAINER_NAME}" echo "" echo -e "${YELLOW}4. 重新开放端口:${RESET}" echo " ▸ iptables -I INPUT -p tcp --dport $port -j ACCEPT" echo " ▸ 检查 /etc/iptables/rules.v4 是否保存规则" echo "" echo -e "${CYAN}===========================================${RESET}" echo -e "${GREEN}✅ 诊断完成!${RESET}" } # --- 辅助函数 --- configure_firewall() { echo -e "${CYAN}配置防火墙...${RESET}" # TCP 与 UDP(Snell 的 UDP 转发要 UDP);firewalld / ufw / iptables / nftables 都认 open_port "$SNELL_PORT" echo -e "${YELLOW}请确保您的 VPS 提供商防火墙也开放了端口 ${SNELL_PORT}${RESET}" } show_information() { if [ ! -f "/etc/snell-docker/snell-server.conf" ]; then echo -e "${RED}未找到配置文件。${RESET}" return fi local port=$(grep 'listen' /etc/snell-docker/snell-server.conf | sed 's/.*://') local psk=$(grep 'psk' /etc/snell-docker/snell-server.conf | sed 's/psk[[:space:]]*=[[:space:]]*//') local version_choice version_choice=$(grep 'version-choice' /etc/snell-docker/snell-server.conf 2>/dev/null | sed 's/version-choice[[:space:]]*=[[:space:]]*//') if [ -z "$version_choice" ]; then version_choice="v4" if docker images "${IMAGE_NAME}" --format '{{.Tag}}' 2>/dev/null | grep -q 'v6'; then version_choice="v6" elif docker images "${IMAGE_NAME}" --format '{{.Tag}}' 2>/dev/null | grep -q 'v5'; then version_choice="v5" fi fi # 获取服务器 IP local ipv4_addr=$(curl -s4 --connect-timeout 5 https://api.ipify.org 2>/dev/null) local ipv6_addr=$(curl -s6 --connect-timeout 5 https://api64.ipify.org 2>/dev/null) clear echo -e "${BLUE}============================================${RESET}" echo -e "${GREEN}🐳 Snell Docker 配置信息:${RESET}" echo -e "${BLUE}============================================${RESET}" if [ -n "$ipv4_addr" ]; then local ip_country_ipv4=$(get_ip_country "${ipv4_addr}") echo -e "${GREEN}--- IPv4 Surge 配置 (Snell ${version_choice}) ---${RESET}" if [ "$version_choice" = "v6" ]; then echo -e "${GREEN}${ip_country_ipv4} = snell, ${ipv4_addr}, ${port}, psk=${psk}, version=6, mode=$(get_snell_mode), reuse=true, tfo=true${RESET}" elif [ "$version_choice" = "v5" ]; then echo -e "${GREEN}${ip_country_ipv4}_v4 = snell, ${ipv4_addr}, ${port}, psk=${psk}, version=4, reuse=true, tfo=true${RESET}" echo -e "${GREEN}${ip_country_ipv4}_v5 = snell, ${ipv4_addr}, ${port}, psk=${psk}, version=5, reuse=true, tfo=true${RESET}" else echo -e "${GREEN}${ip_country_ipv4} = snell, ${ipv4_addr}, ${port}, psk=${psk}, version=4, reuse=true, tfo=true${RESET}" fi fi if [ -n "$ipv6_addr" ]; then local ip_country_ipv6=$(get_ip_country "${ipv6_addr}") echo -e "\n${GREEN}--- IPv6 Surge 配置 (Snell ${version_choice}) ---${RESET}" if [ "$version_choice" = "v6" ]; then echo -e "${GREEN}${ip_country_ipv6} = snell, ${ipv6_addr}, ${port}, psk=${psk}, version=6, mode=$(get_snell_mode), reuse=true, tfo=true${RESET}" elif [ "$version_choice" = "v5" ]; then echo -e "${GREEN}${ip_country_ipv6}_v4 = snell, ${ipv6_addr}, ${port}, psk=${psk}, version=4, reuse=true, tfo=true${RESET}" echo -e "${GREEN}${ip_country_ipv6}_v5 = snell, ${ipv6_addr}, ${port}, psk=${psk}, version=5, reuse=true, tfo=true${RESET}" else echo -e "${GREEN}${ip_country_ipv6} = snell, ${ipv6_addr}, ${port}, psk=${psk}, version=4, reuse=true, tfo=true${RESET}" fi fi echo "" echo -e "${YELLOW}服务器端口: ${RESET}${port}" echo -e "${YELLOW}PSK 密钥: ${RESET}${psk}" echo -e "${YELLOW}容器名称: ${RESET}${CONTAINER_NAME}" echo -e "${YELLOW}镜像名称: ${RESET}${IMAGE_NAME}:latest" echo -e "\n${YELLOW}配置文件: ${RESET}/etc/snell-docker/snell-server.conf" echo -e "\n${CYAN}Docker 管理命令:${RESET}" echo -e "查看日志: ${YELLOW}docker logs ${CONTAINER_NAME}${RESET}" echo -e "进入容器: ${YELLOW}docker exec -it ${CONTAINER_NAME} sh${RESET}" echo -e "重启容器: ${YELLOW}docker restart ${CONTAINER_NAME}${RESET}" echo -e "${BLUE}============================================${RESET}" } # --- 主菜单 --- show_menu() { clear echo -e "${CYAN}============================================${RESET}" echo -e "${CYAN} 🐳 Snell Docker 管理脚本 v${current_version}${RESET}" echo -e "${CYAN}============================================${RESET}" # 检查容器状态 if container_exists; then if container_running; then echo -e "服务状态: ${GREEN}运行中 🟢${RESET}" else echo -e "服务状态: ${RED}已停止 🔴${RESET}" fi else echo -e "服务状态: ${YELLOW}未安装 ⚪${RESET}" fi echo -e "${CYAN}--------------------------------------------${RESET}" echo -e "${GREEN}1.${RESET} 🚀 安装 Snell (Docker)" echo -e "${GREEN}2.${RESET} 🗑️ 卸载 Snell" echo -e "${GREEN}3.${RESET} 🔄 重启服务" echo -e "${GREEN}4.${RESET} 📋 查看配置信息" echo -e "${GREEN}5.${RESET} 📊 查看详细状态" echo -e "${GREEN}6.${RESET} 网络连接诊断" echo -e "${GREEN}7.${RESET} Docker 常用命令" echo -e "${GREEN}0.${RESET} 🚪 退出脚本" echo -e "${CYAN}============================================${RESET}" printf "请输入选项 [0-7]: " read -r num } show_docker_commands() { clear echo -e "${CYAN}============================================${RESET}" echo -e "${CYAN} 🐳 Docker 常用管理命令${RESET}" echo -e "${CYAN}============================================${RESET}" echo -e "${GREEN}查看容器状态:${RESET}" echo " docker ps -a --filter \"name=${CONTAINER_NAME}\"" echo "" echo -e "${GREEN}查看实时日志:${RESET}" echo " docker logs -f ${CONTAINER_NAME}" echo "" echo -e "${GREEN}进入容器shell:${RESET}" echo " docker exec -it ${CONTAINER_NAME} sh" echo "" echo -e "${GREEN}容器资源监控:${RESET}" echo " docker stats ${CONTAINER_NAME}" echo "" echo -e "${GREEN}手动重启容器:${RESET}" echo " docker restart ${CONTAINER_NAME}" echo "" echo -e "${GREEN}手动停止/启动:${RESET}" echo " docker stop ${CONTAINER_NAME}" echo " docker start ${CONTAINER_NAME}" echo "" echo -e "${GREEN}查看镜像:${RESET}" echo " docker images | grep ${IMAGE_NAME}" echo "" echo -e "${GREEN}容器端口映射:${RESET}" echo " docker port ${CONTAINER_NAME}" echo -e "${CYAN}============================================${RESET}" printf "${CYAN}按任意键返回主菜单...${RESET}" read -r dummy } # --- 主程序 --- main() { while true; do show_menu case "$num" in 1) install_snell ;; 2) uninstall_snell ;; 3) restart_snell ;; 4) show_information ;; 5) check_status ;; 6) network_diagnosis ;; 7) show_docker_commands ;; 0) echo -e "${GREEN}感谢使用,再见!🎉${RESET}"; exit 0 ;; *) echo -e "${RED}请输入正确的选项 [0-7]${RESET}";; esac if [ "$num" != "7" ]; then echo "" printf "${CYAN}按任意键返回主菜单...${RESET}" read -r dummy fi done } # 启动主程序 main