#!/usr/bin/env bash # uninstall-linux.sh — fully remove a package-installed systemd myvpn client. set -u YES=0 for arg in "$@"; do case "$arg" in --yes) YES=1 ;; --help|-h) echo "Usage: sudo $0 [--yes]"; exit 0 ;; *) echo "ERROR: unknown argument: $arg" >&2; exit 2 ;; esac done ROOT_PREFIX="${MYVPN_UNINSTALL_ROOT:-}" case "$ROOT_PREFIX" in ""|/) ROOT_PREFIX="" ;; /*) ROOT_PREFIX="${ROOT_PREFIX%/}" ;; *) echo "ERROR: MYVPN_UNINSTALL_ROOT must be an absolute path" >&2; exit 2 ;; esac root_path() { printf '%s%s\n' "$ROOT_PREFIX" "$1"; } effective_uid="$(id -u)" platform="$(uname -s)" if [ -n "$ROOT_PREFIX" ]; then effective_uid="${MYVPN_UNINSTALL_TEST_UID:-$effective_uid}" platform="${MYVPN_UNINSTALL_TEST_PLATFORM:-$platform}" fi [ "$effective_uid" -eq 0 ] || { echo "ERROR: 卸载需要 root。请用: sudo $0" >&2 exit 1 } [ "$platform" = "Linux" ] || { echo "ERROR: uninstall-linux.sh 只能在 Linux 上运行。" >&2 exit 1 } echo "即将完整卸载 myvpn:程序、服务、登录状态、凭据和产品日志都会被删除。" echo "本地卸载不会删除控制台中的设备记录;如需删除,请让管理员在控制台另行操作。" if [ "$YES" -ne 1 ]; then printf "继续卸载?输入 y 确认 [y/N]: " answer="" IFS= read -r answer || true case "$answer" in y|Y|yes|YES|Yes) ;; *) echo "已取消,未做任何更改。"; exit 0 ;; esac fi cleanup_failed=0 cleanup_warning() { echo "WARNING: $*" >&2 cleanup_failed=1 } remove_files() { if ! rm -f "$@"; then cleanup_warning "删除 myvpn 文件失败,请检查上方错误。" return fi for target in "$@"; do if [ -e "$target" ] || [ -L "$target" ]; then cleanup_warning "删除后路径仍然存在:$target" fi done } remove_tree() { target="$1" [ -e "$target" ] || [ -L "$target" ] || return 0 if ! rm -rf "$target"; then cleanup_warning "删除目录失败:$target" return fi if [ -e "$target" ] || [ -L "$target" ]; then cleanup_warning "删除后目录仍然存在:$target" fi } systemctl_best_effort() { command -v systemctl >/dev/null 2>&1 || return 0 systemctl "$@" >/dev/null 2>&1 || true } # Stop periodic re-assert/upgrade triggers first, then gracefully tear down the # client tunnel and the optional exit-node datapath while their helpers exist. systemctl_best_effort disable --now myvpn-exit-node-refresh.timer systemctl_best_effort disable --now myvpn-agent-upgrade.path CLI="$(root_path /usr/local/bin/myvpn)" if [ -x "$CLI" ]; then MYVPN_SOCKET="$(root_path /run/myvpn-agent/agent.sock)" "$CLI" down >/dev/null 2>&1 || true fi EXIT_DISABLE="$(root_path /usr/local/share/myvpn/exit-node-disable.sh)" if [ -x "$EXIT_DISABLE" ]; then if ! "$EXIT_DISABLE" >/dev/null 2>&1; then cleanup_warning "出口节点网络清理失败,请手工检查转发与防火墙配置。" fi fi systemctl_best_effort disable --now \ myvpn-exit-node.service myvpn-agent.service \ myvpn-exit-node-refresh.service \ myvpn-agent-upgrade.service UNIT_DIR="$(root_path /etc/systemd/system)" remove_files \ "$UNIT_DIR/myvpn-agent.service" \ "$UNIT_DIR/myvpn-exit-node.service" \ "$UNIT_DIR/myvpn-exit-node-refresh.service" \ "$UNIT_DIR/myvpn-exit-node-refresh.timer" \ "$UNIT_DIR/myvpn-agent-upgrade.path" \ "$UNIT_DIR/myvpn-agent-upgrade.service" \ "$UNIT_DIR/multi-user.target.wants/myvpn-agent.service" \ "$UNIT_DIR/multi-user.target.wants/myvpn-exit-node.service" \ "$UNIT_DIR/paths.target.wants/myvpn-agent-upgrade.path" \ "$UNIT_DIR/myvpn-exit-node.service.wants/myvpn-exit-node-refresh.timer" DROPIN_DIR="$UNIT_DIR/myvpn-agent.service.d" [ -n "$DROPIN_DIR" ] && [ "$DROPIN_DIR" != "$ROOT_PREFIX" ] && remove_tree "$DROPIN_DIR" systemctl_best_effort daemon-reload systemctl_best_effort reset-failed remove_files \ "$(root_path /etc/profile.d/myvpn-cli.sh)" \ "$(root_path /etc/polkit-1/rules.d/10-myvpn-resolved.rules)" \ "$(root_path /etc/polkit-1/localauthority/50-local.d/10-myvpn-resolved.pkla)" \ "$(root_path /usr/local/bin/myvpn-agent)" \ "$(root_path /usr/local/bin/myvpn-agent.bak)" \ "$(root_path /usr/local/bin/myvpn-agent.new)" \ "$(root_path /usr/local/bin/myvpn)" \ "$(root_path /usr/local/bin/myvpn.bak)" \ "$(root_path /usr/local/bin/myvpn.new)" \ "$(root_path /usr/local/bin/myvpn-up)" \ "$(root_path /usr/local/bin/myvpn-down)" \ "$(root_path /var/run/myvpn-upgrade.lock)" \ "$(root_path /var/log/myvpn-upgrade.log)" STATE_DIR="$(root_path /var/lib/myvpn-agent)" RUNTIME_DIR="$(root_path /run/myvpn-agent)" CONFIG_DIR="$(root_path /etc/myvpn)" for tree in "$STATE_DIR" "$RUNTIME_DIR" "$CONFIG_DIR"; do [ -n "$tree" ] && [ "$tree" != "$ROOT_PREFIX" ] && remove_tree "$tree" done # Account deletion is deliberately marker-based. install-linux.sh records the # exact passwd/group entries only when it created them. We additionally require # the current entry to be unchanged and the user to remain a <1000 nologin # system account. Pre-existing or subsequently repurposed identities survive. SHARE_DIR="$(root_path /usr/local/share/myvpn)" USER_MARKER="$SHARE_DIR/installer-created-user" GROUP_MARKER="$SHARE_DIR/installer-created-group" user_gone=0 if [ -f "$USER_MARKER" ]; then expected_user="$(cat "$USER_MARKER" 2>/dev/null || true)" current_user="$(getent passwd myvpn 2>/dev/null || true)" IFS=: read -r account _ uid _ _ _ shell <<<"$current_user" case "$uid" in ''|*[!0-9]*) uid=999999 ;; esac if [ -n "$expected_user" ] && [ "$current_user" = "$expected_user" ] && \ [ "$account" = "myvpn" ] && [ "$uid" -lt 1000 ] && \ { [ "$shell" = "/usr/sbin/nologin" ] || [ "$shell" = "/sbin/nologin" ]; }; then if userdel myvpn >/dev/null 2>&1; then user_gone=1 else cleanup_warning "无法删除安装器创建的 myvpn 用户,请手工检查。" fi elif [ -n "$current_user" ]; then echo "WARNING: myvpn 用户不是安装器记录的未修改 nologin 系统账号,已保留。" >&2 else user_gone=1 fi elif getent passwd myvpn >/dev/null 2>&1; then echo "WARNING: myvpn 用户没有安装器创建标记,已保留。" >&2 else user_gone=1 fi if [ -f "$GROUP_MARKER" ]; then expected_group="$(cat "$GROUP_MARKER" 2>/dev/null || true)" current_group="$(getent group myvpn 2>/dev/null || true)" if [ "$user_gone" -eq 1 ] && [ -n "$expected_group" ] && [ "$current_group" = "$expected_group" ]; then groupdel myvpn >/dev/null 2>&1 || cleanup_warning "无法删除安装器创建的 myvpn 组,请手工检查。" elif [ -n "$current_group" ] && [ "$user_gone" -ne 1 ]; then echo "WARNING: myvpn 组仍可能被保留账号使用,已保留。" >&2 fi elif getent group myvpn >/dev/null 2>&1; then echo "WARNING: myvpn 组没有安装器创建标记,已保留。" >&2 fi if [ "$cleanup_failed" -eq 0 ]; then [ -n "$SHARE_DIR" ] && [ "$SHARE_DIR" != "$ROOT_PREFIX" ] && remove_tree "$SHARE_DIR" fi if [ "$cleanup_failed" -ne 0 ]; then echo "ERROR: myvpn 卸载不完整;已保留卸载脚本和标记,请处理警告后重试。" >&2 exit 1 fi echo "myvpn 已从本机完整卸载。" echo "提醒:控制台中的设备记录仍然保留,需要管理员另行删除。"