#!/bin/bash ## bash only set -e # We strict the whole script CONFIG_BASE="/etc/natfrp" LOW_USER="natfrp" # Utility functions log_I() { echo -e "\e[32m[+] $1\e[0m"; } log_W() { echo -e "\e[33m[!] $1\e[0m"; } log_E() { echo -e "\e[31m[-] $1\e[0m"; } # Check root permission if [ "$EUID" -ne 0 ]; then log_E "请使用 root 用户运行此脚本" exit 1 fi case "${1:-}" in ""|docker|podman|direct|uninstall) ;; *) log_E "未知参数: $1, 支持的参数为 docker、podman、direct 或 uninstall" exit 1 ;; esac # Check if selinux is enabled if command -v getenforce &>/dev/null; then if [[ $(getenforce) == "Enforcing" ]]; then log_W "SELinux 处于启用状态, 您可以继续运行脚本,但我们建议禁用 SELinux 以避免潜在问题" fi fi ask_for_creds() { while true; do read -r -e -p "请输入 SakuraFrp 的访问密钥 (可在 https://www.natfrp.com/user/ 获取): " api_key echo if [[ "$api_key" =~ ^[a-z0-9]{16,}$ ]]; then break; fi log_E "访问密钥错误, 请到 https://www.natfrp.com/user/ 获取" done while true; do while true; do IFS= read -r -e -p "请输入远程管理密码 (至少八个字符): " remote_pass echo if [[ ${#remote_pass} -ge 8 && "$remote_pass" != *[![:graph:]]* ]]; then break; fi log_E "远程管理密码至少需要 8 个字符" done IFS= read -r -e -p "请再次输入远程管理密码: " remote_pass_confirm echo if [[ "$remote_pass" == "$remote_pass_confirm" ]]; then break; fi log_E "两次输入的远程管理密码不一致, 请确认知晓自己正在输入的内容" done clear } check_executable() { version=$(sudo -H -u "${LOW_USER}" "$1" -v) || { log_E "无法获取 $1 版本信息, 文件可能已损坏, 请尝试再次执行脚本重新下载" exit 1 } log_I "$1 版本: $version" } press_enter() { read -p "按 Enter 键以继续, 或 Ctrl-C 取消..." } # The container runs as root; direct installs run as LOW_USER. prepare_config_dir() { mkdir -p "$CONFIG_BASE" chown -R "$1" "$CONFIG_BASE" chmod 700 "$CONFIG_BASE" if [[ -f "$CONFIG_BASE/config.json" ]]; then chmod 600 "$CONFIG_BASE/config.json" fi } # docker install should be the default, use podman only if necessary container_install() { echo -e "\e[96m[*] 检测到 $1, 将使用容器安装模式\e[0m - 将在您的系统上创建名为 natfrp-service 的容器 - 将为您创建 ${CONFIG_BASE} 文件夹用于存储启动器配置文件\n" ask_for_creds prepare_config_dir 0:0 if "$1" ps -a --format '{{.Names}}' | grep -q '^natfrp-service$'; then log_W "已存在名为 natfrp-service 的容器" read -p " - 是否移除已存在的容器? [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then "$1" stop natfrp-service -t 5 &>/dev/null "$1" rm natfrp-service else log_E "请手动移除已存在的容器后重新运行脚本" exit 1 fi fi log_I "容器安装模式选择" echo "请选择镜像来源:" echo "1. 官方 (natfrp.com/launcher)" echo "2. GitHub (ghcr.io/natfrp/launcher)" echo "3. Docker Hub (docker.io/natfrp/launcher)" read -p "请输入选项 [1-3] (默认1): " source case $source in 2) image="ghcr.io/natfrp/launcher" ;; 3) image="docker.io/natfrp/launcher" ;; *) image="natfrp.com/launcher" ;; esac platform_flag="" if [[ $use_32bit -eq 1 ]]; then platform_flag="--platform linux/386" fi "$1" run -d \ --network=host \ --restart=always \ --pull=always \ --name=natfrp-service \ $platform_flag \ -v ${CONFIG_BASE}:/run:z \ -e "NATFRP_TOKEN=$api_key" \ -e "NATFRP_REMOTE=$remote_pass" \ -e "TZ=${TZ:-Asia/Shanghai}" \ $image || \ { log_E "容器模式安装失败, 请检查 $1 是否正常运行以及是否能正常访问镜像" exit 1 } echo -e "\n\e[96m容器模式安装成功, 您可使用下面的命令管理服务:\e[0m - 查看日志\t$1 logs natfrp-service - 停止服务\t$1 stop natfrp-service - 启动服务\t$1 start natfrp-service \n请登录远程管理界面启动和管理隧道: https://www.natfrp.com/remote/v2 请登录远程管理界面启动和管理隧道: https://www.natfrp.com/remote/v2 请登录远程管理界面启动和管理隧道: https://www.natfrp.com/remote/v2 请登录远程管理界面启动和管理隧道: https://www.natfrp.com/remote/v2 请登录远程管理界面启动和管理隧道: https://www.natfrp.com/remote/v2\n" log_I "输入回车键将开始输出启动器日志供您查看初步运行状态, 如需退出请按 Ctrl+C" read -p "" "$1" logs -f natfrp-service exit 0 } # install all files, should be run before any other bareinstall_ functions bareinstall_base() { # Create user if ! id -u ${LOW_USER} &>/dev/null; then log_I "正在创建 ${LOW_USER} 用户" useradd -r -m -s /sbin/nologin ${LOW_USER} else echo -e "\e[32m[*] ${LOW_USER} 用户已存在\e[0m" fi # Download binaries cd "/home/${LOW_USER}" || { log_E "无法切换到 /home/${LOW_USER} 目录" exit 1 } do_download=1 if [[ -f frpc && -f natfrp-service ]]; then log_W "已存在 SakuraFrp 启动器文件" read -p " - 是否删除已有的文件重新下载? [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then rm -f frpc natfrp-service else do_download=0 fi fi if [[ $do_download == 1 ]]; then log_I "正在下载启动器..." curl -Lo - "https://nya.globalslb.net/natfrp/client/launcher-unix/latest/natfrp-service_linux_$arch.tar.zst" | tar -xI zstd --overwrite chmod +x frpc natfrp-service chown ${LOW_USER}: frpc natfrp-service fi # Make sure binaries can be executed check_executable "./frpc" check_executable "./natfrp-service" # Update config file prepare_config_dir "${LOW_USER}:" config_file="${CONFIG_BASE}/config.json" if [[ -f $config_file ]]; then log_W "已存在 SakuraFrp 启动器配置文件" read -p " - 是否清空设置? 这可能有助于修复问题: [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then rm -f $config_file fi fi ( umask 077 if [[ ! -f "$config_file" ]]; then echo '{}' > "$config_file" fi chown "${LOW_USER}:" "$config_file" : > "$config_file.tmp" chmod 600 "$config_file.tmp" jq --arg token "$api_key" \ --arg remote_key "$(sudo -H -u "$LOW_USER" ./natfrp-service remote-kdf "$remote_pass")" \ '. + { token: $token, remote_management: true, remote_management_key: $remote_key, log_stdout: true }' "$config_file" > "$config_file.tmp" chown "${LOW_USER}:" "$config_file.tmp" mv "$config_file.tmp" "$config_file" ) } bareinstall_systemd() { bareinstall_base # Stop existing service to avoid conflicts systemctl stop natfrp.service &>/dev/null ||: # Install systemd unit log_I "正在安装 systemd 服务" cat >"/etc/systemd/system/natfrp.service" </home/${LOW_USER}/start.sh </dev/null) || return 1 executable="${executable% (deleted)}" [[ "$executable" == "/home/${LOW_USER}/natfrp-service" || "$executable" == "/home/${LOW_USER}/frpc" ]] } stop_direct_processes() { local entry pid local pids=() for entry in /proc/[0-9]*/exe; do pid="${entry#/proc/}" pid="${pid%/exe}" if is_direct_process "$pid"; then pids+=("$pid") kill -TERM "$pid" 2>/dev/null fi done if [[ ${#pids[@]} -gt 0 ]]; then log_I "正在结束直接安装的启动器及 frpc 进程: ${pids[*]}" sleep 5 for pid in "${pids[@]}"; do if is_direct_process "$pid"; then kill -KILL "$pid" 2>/dev/null fi done fi } uninstall() { set +e read -p " - 确认要卸载 SakuraFrp 启动器吗? [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then for rt in docker podman; do if command -v "$rt" &>/dev/null && "$rt" ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^natfrp-service$'; then "$rt" stop natfrp-service -t 5 &>/dev/null "$rt" rm natfrp-service &>/dev/null && log_I "已删除 $rt 容器" fi done if [[ -f /etc/systemd/system/natfrp.service ]]; then if systemctl -q is-enabled natfrp.service; then systemctl disable --now natfrp.service &>/dev/null elif systemctl -q is-active natfrp.service; then systemctl stop natfrp.service &>/dev/null fi rm -f /etc/systemd/system/natfrp.service &>/dev/null && log_I "已删除 systemd 服务" systemctl daemon-reload &>/dev/null fi stop_direct_processes if [[ -f /home/${LOW_USER}/natfrp-service ]]; then rm -rf /home/${LOW_USER}/frpc /home/${LOW_USER}/natfrp-service &>/dev/null && log_I "已删除 /home/${LOW_USER} 中的程序文件" fi if id -u ${LOW_USER} &>/dev/null; then userdel ${LOW_USER} &>/dev/null && log_I "已删除 ${LOW_USER} 账户" fi read -p " - 是否删除启动器数据 (包括登录信息、日志等)? [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then if [[ -d /home/${LOW_USER} ]]; then rm -rf /home/${LOW_USER} &>/dev/null && log_I "已删除 /home/${LOW_USER} 中的启动器数据" fi if [[ -d ${CONFIG_BASE} ]]; then rm -rf ${CONFIG_BASE} &>/dev/null && log_I "已删除 ${CONFIG_BASE} 中的启动器数据" fi else log_I "启动器数据未删除, 您可再次执行卸载脚本以删除" fi log_I "SakuraFrp 启动器已成功卸载" return 0 else log_I "SakuraFrp 启动器未被卸载" return 1 fi } ######### Main script start ######### # Detect ancient CPU on x86_64 systems use_32bit=0 if [[ $(uname -m) == "x86_64" ]]; then v2_flags=(popcnt sse4_2 ssse3) cpu_flags=$(grep -m1 '^flags' /proc/cpuinfo | cut -d: -f2) for f in "${v2_flags[@]}"; do if ! echo "$cpu_flags" | grep -qw "$f"; then use_32bit=1 break fi done if [[ $use_32bit -eq 1 ]]; then log_W "当前 CPU 不支持 x86-64-v2 指令集, 将使用 32 位 (x86) 版本" fi fi if [[ $1 == "uninstall" ]]; then uninstall exit 0 fi # Find any existing installation before selecting a runtime. known_install="" check_installation() { local rt names local installs=() for rt in docker podman; do if ! command -v "$rt" &>/dev/null; then continue fi if ! names=$("$rt" ps -a --format '{{.Names}}' 2>/dev/null); then log_W "无法查询 $rt 中的已有安装, 请检查该运行时是否正常运行" continue fi if grep -q '^natfrp-service$' <<< "$names"; then installs+=("$rt") fi done if [[ -f "/etc/systemd/system/natfrp.service" || -f "/home/${LOW_USER}/natfrp-service" ]]; then installs+=("direct") fi if [[ ${#installs[@]} -gt 0 ]]; then log_I "检测到已有安装: ${installs[*]}" if [[ ${#installs[@]} -gt 1 ]]; then log_W "存在多种安装方式, 卸载操作会尝试移除全部已有安装" fi known_install="${installs[0]}" return 0 fi return 1 } check_installation || : # Check the selected runtime before offering to remove an existing installation. crt="" case "${1:-$known_install}" in docker|podman) rt="${1:-$known_install}" if "$rt" info &>/dev/null; then crt="$rt" else log_E "$rt 不可用, 请检查它是否已安装并正常运行" exit 1 fi ;; direct) ;; *) if docker info &>/dev/null; then crt="docker" elif podman info &>/dev/null; then crt="podman" fi ;; esac if [[ -n "$known_install" ]]; then if [[ -n "$1" && "$1" != "$known_install" ]]; then log_W "检测到已有 $known_install 安装, 与指定的安装方式 $1 不符" read -p " - 是否卸载已有安装并继续使用 $1 安装? [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then log_W "卸载会中断所有隧道; 如果当前 SSH 通过隧道连接, 连接将断开, 后续安装可能无法继续" if uninstall; then set -e known_install="" else exit 1 fi else exit 1 fi else log_I "检测到已有 $known_install 安装, 将更新 $known_install 部署" fi fi # Use a container runtime (docker/podman) if available if [[ -n "$crt" ]]; then if [[ "$crt" == "podman" ]]; then if ! command -v systemctl &>/dev/null || [[ ! -d /run/systemd/system ]]; then log_W "未检测到 systemd, 请手动配置 Podman 开机自启" elif ! systemctl is-enabled --quiet podman-restart.service; then log_W "podman-restart.service 未启用, 容器可能无法在系统重启后自动启动" read -p " - 是否启用 podman-restart.service 开机自启? [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then if systemctl enable podman-restart.service; then log_I "已启用 podman-restart.service 开机自启" else log_W "无法启用 podman-restart.service, 请手动检查服务配置" fi else log_W "未启用开机自启, 系统重启后可能需要手动启动容器" fi fi fi container_install "$crt" exit 0 fi log_W "您正在使用非容器安装模式" log_I "我们建议您总是使用 Docker 容器安装模式, 以便于管理和减少兼容性问题" # Check SELinux if command -v getenforce &>/dev/null; then if [[ $(getenforce) == "Enforcing" ]]; then log_E "SELinux 处于启用状态, 为避免出现问题, 请安装 Docker 后重新运行脚本以使用容器部署" exit 1 fi fi # Install deps required_tools=(curl jq zstd sudo) missing_tools=() for tool in "${required_tools[@]}"; do if ! command -v "$tool" &>/dev/null; then missing_tools+=("$tool") fi done if [[ ${#missing_tools[@]} -ne 0 ]]; then log_I "正在使用包管理器安装必要的工具程序: ${missing_tools[*]}" if command -v apt-get &>/dev/null; then apt-get update && apt-get install -y "${missing_tools[@]}" elif command -v yum &>/dev/null; then yum install -y "${missing_tools[@]}" elif command -v zypper &>/dev/null; then zypper install -y "${missing_tools[@]}" elif command -v opkg &>/dev/null; then opkg update && opkg install "${missing_tools[@]}" elif command -v apk &>/dev/null; then apk --update add "${missing_tools[@]}" else log_E "当前系统的包管理器暂时不受支持, 请手动安装 ${missing_tools[*]} 后重新运行脚本" exit 1 fi fi missing_tools=() for tool in "${required_tools[@]}"; do if ! command -v "$tool" &>/dev/null; then missing_tools+=("$tool") fi done if [[ ${#missing_tools[@]} -ne 0 ]]; then log_E "以下必要的工具程序未成功安装, 请手动安装后重新运行脚本: ${missing_tools[*]}" exit 1 fi # Migrate config files from old script if [[ -f "/home/natfrp/.config/natfrp-service/config.json" && ${CONFIG_BASE} != "/home/natfrp/.config/natfrp-service" ]]; then log_W "检测到旧的配置文件, 将为您迁移到 ${CONFIG_BASE} 目录" read -p " - 是否迁移配置文件? [y/N] " -r choice if [[ $choice =~ ^[Yy]$ ]]; then systemctl stop natfrp.service &>/dev/null || log_W "无法停止旧服务, 将尝试直接迁移" prepare_config_dir 0:0 if ( shopt -s dotglob nullglob mv -f /home/natfrp/.config/natfrp-service/* "$CONFIG_BASE/" ); then rmdir /home/natfrp/.config/natfrp-service 2>/dev/null || : log_I "已迁移旧配置文件到 ${CONFIG_BASE} 目录" else log_W "无法完整迁移旧配置文件, 数据迁移可能不完整" fi prepare_config_dir 0:0 fi fi # Determine filename machine="$(uname -m)" case "$machine" in "x86_64") if [[ $use_32bit -eq 1 ]]; then arch="386" else arch="amd64" fi ;; "i386") arch="386" ;; "aarch64") arch="arm64" ;; "armv7l") arch="armv7" ;; "mips") arch="mips" ;; "mipsel") arch="mipsle" ;; "mips64") arch="mips64" ;; "mips64le") arch="mips64le" ;; "riscv64") arch="riscv64" ;; "loongarch64") arch="loong64" ;; *) log_E "不支持当前系统架构: $machine" exit 1 ;; esac ask_for_creds # Check init ls -l /proc/1/exe | grep -q systemd && is_systemd=1 || is_systemd=0 if [ $is_systemd -eq 1 ]; then echo -e "\e[96m[*] 将使用 systemd 安装模式\e[0m - 将在您的系统上创建名为 ${LOW_USER} 的用户 - 将在 /home/${LOW_USER} 目录下保存 SakuraFrp 启动器文件 - 将为您创建 ${CONFIG_BASE} 文件夹用于存储启动器配置文件 - 将创建 systemd 服务并启动 SakuraFrp 启动器\n" press_enter bareinstall_systemd else echo -e "\e[96m[*] 未发现 systemd, 将只安装启动器文件\e[0m - 将在您的系统上创建名为 ${LOW_USER} 的用户 - 将在 /home/${LOW_USER} 目录下保存 SakuraFrp 启动器文件 - 将为您创建 ${CONFIG_BASE} 文件夹用于存储启动器配置文件 - 将在 /home/${LOW_USER}/start.sh 创建一个脚本用于运行 SakuraFrp 启动器 \e[31m- 需手动启动, 不会开机自启 - 关闭后不会自动重启\e[0m\n" press_enter bareinstall_other fi