#!/usr/bin/env bash # myvpn-install-mac.sh — install the myvpn agent + CLI on macOS for everyday use. # Installs binaries to /usr/local/bin, a persistent state/key dir at /var/lib/myvpn, # and myvpn-up / myvpn-down wrappers pointing at the production coord + DERP. # sudo bash myvpn-install-mac.sh # run from the repo root (finds dist/), or # sudo bash myvpn-install-mac.sh # with agent-darwin-* + myvpn-darwin-* next to it set -euo pipefail # --dry-run prints the command listing and exits without installing # anything. Used by install-mac-test.sh to verify the listing is in # sync with cmd/cli/main.go usage(). No sudo / no Darwin check needed. print_command_listing() { echo "" echo "================================================================" echo "可用命令清单(全部需 sudo)" echo "================================================================" echo "[高层 wrapper 脚本 — 日常用这两个]" echo "myvpn-up 一站起:起 agent + up + status(首次交互 myvpn login,之后凭 refresh token 自动连)" echo "myvpn-down 一站停:关 tunnel + 停 agent" echo "" echo "[myvpn 子命令 — 注意中间是空格不是横线;agent 已起前提下]" echo "myvpn login 交互登录(邮箱+密码),成员首登强制改密" echo "myvpn logout 注销当前账号,清除本地 token" echo "myvpn passwd 修改当前账号密码" echo "myvpn up 加入 mesh(agent 已起;wrapper 用 myvpn-up)" echo "myvpn down 离开 mesh(agent 保留;wrapper 用 myvpn-down)" echo "myvpn status 显示连接状态、虚拟 IP、relay-only 标记" echo "myvpn peers 列出对端 + 链路模式 + 真实 IP" echo "myvpn watch 实时跟踪事件流" echo "myvpn devices 列出账号下所有设备(含离线 + last_seen)" echo "myvpn relay-only on 强制走中继(关闭直连,不发探测)" echo "myvpn relay-only off 关闭强制中继,恢复自动决策" echo "myvpn relay-only status 查询当前 relay-only 状态" echo "myvpn longlived on 登录长期有效(refresh token 永不过期)" echo "myvpn longlived off 关闭长期有效,恢复 refresh token 过期" echo "myvpn longlived status 查询当前 longlived 状态" echo "myvpn autostart on 开机自启(macOS LaunchDaemon)" echo "myvpn autostart off 关闭开机自启" echo "myvpn autostart status 查询开机自启状态" echo "myvpn upgrade 升级到最新版本(check+下载+安装+自动回滚)" echo "myvpn help 显示所有命令的详细帮助" echo "================================================================" } if [ "${1:-}" = "--dry-run" ]; then print_command_listing exit 0 fi [ "$(uname -s)" = "Darwin" ] || { echo "macOS only" >&2; exit 1; } [ "$EUID" -eq 0 ] || { echo "run with sudo" >&2; exit 1; } ARCH="$(uname -m)"; case "$ARCH" in arm64) GA=arm64;; x86_64) GA=amd64;; *) echo "bad arch $ARCH" >&2; exit 1;; esac HERE="$(cd "$(dirname "$0")" && pwd)" find_bin() { # -f (not -x): self-upgrade downloads land as 0644 (no exec bit). The source # need not be executable — install -m 0755 below sets the dest mode anyway. for c in "dist/$1-darwin-$GA" "bin/$1" "$HERE/$1-darwin-$GA" "$HERE/$1"; do [ -f "$c" ] && { echo "$c"; return 0; } done; return 1 } AGENT="$(find_bin agent || true)"; CLI="$(find_bin myvpn || true)" [ -n "$AGENT" ] && [ -n "$CLI" ] || { echo "ERROR: agent/myvpn darwin binaries not found (run from repo root, or put them next to this script)" >&2; exit 1; } mkdir -p /usr/local/bin install -m 0755 "$AGENT" /usr/local/bin/myvpn-agent install -m 0755 "$CLI" /usr/local/bin/myvpn # 清 Gatekeeper quarantine + ad-hoc 重签(防 macOS Gatekeeper 拦截 / SIGKILL)。 # binary 经 scp / AirDrop / 解压等方式传入目标机可能带 com.apple.quarantine xattr; # 非公证的 ad-hoc 签名 binary 一旦带 quarantine,首次启动会被 Gatekeeper 拦 # ("Apple 无法验证…是否包含恶意软件")。故先清 quarantine,再 remove 旧签名 → ad-hoc 重签。 for bin in /usr/local/bin/myvpn /usr/local/bin/myvpn-agent; do /usr/bin/xattr -d com.apple.quarantine "$bin" 2>/dev/null || true /usr/bin/codesign --remove-signature "$bin" 2>/dev/null || true /usr/bin/codesign --force --sign - "$bin" echo "✓ 清 quarantine + ad-hoc 签名 $bin" done mkdir -p /var/lib/myvpn && chmod 700 /var/lib/myvpn cat > /usr/local/bin/myvpn-up <<'EOF' #!/bin/bash set -e COORD=http://47.108.253.164:8080 DERP=47.108.253.164:8443 SOCK=/var/run/myvpn-agent.sock DIR=/var/lib/myvpn export MYVPN_SOCKET="$SOCK" # Start agent if not already running. if [ -z "$(ps -axww -o pid=,args= 2>/dev/null | grep "[m]yvpn-agent .*$SOCK")" ]; then mkdir -p "$DIR" && chmod 700 "$DIR" # agent 自写 --log-file(并 Dup2 stderr 落该文件),故把 nohup 的 stdout/stderr # 导到 /dev/null —— 否则 nohup 会在调用方 cwd 留下 nohup.out(sudo 跑时还是 root 拥有, # 会污染目录、卡住后续 scp)。 nohup /usr/local/bin/myvpn-agent --coord="$COORD" --real-tun --derp="$DERP" \ --socket="$SOCK" --state-file="$DIR/state.json" --key-file="$DIR/wg.key" \ --listen-port=51820 --log-file=/var/log/myvpn-agent.log >/dev/null 2>&1 & sleep 2 fi # v1.21: no account.env anymore. If we already have a live session (persisted # refresh token in state.json), `myvpn up` reconnects with no prompt. Only when # there's no usable session do we drop into an interactive `myvpn login` (the # typed password is used once for auth, then discarded — never written to disk). if /usr/local/bin/myvpn status 2>/dev/null | grep -q "^logged in: yes"; then : else echo "首次启动 — 请输入邮箱+密码登录(密码仅用于本次认证,不会保存到磁盘):" /usr/local/bin/myvpn login fi /usr/local/bin/myvpn up sleep 1 /usr/local/bin/myvpn status EOF chmod 0755 /usr/local/bin/myvpn-up cat > /usr/local/bin/myvpn-down <<'EOF' #!/bin/bash SOCK=/var/run/myvpn-agent.sock export MYVPN_SOCKET="$SOCK" agent_pids() { # ps works in every context (incl. a no-tty daemon, where the process-grep # tools return empty on macOS). The [m] bracket trick keeps grep from matching # its own cmdline. ps -axww -o pid=,args= 2>/dev/null | grep "[m]yvpn-agent .*$SOCK" | awk '{print $1}' } /usr/local/bin/myvpn down 2>/dev/null || true if [ -z "$(agent_pids)" ]; then echo "myvpn stopped (tunnel down, no agent running)" exit 0 fi kill -TERM $(agent_pids) 2>/dev/null || true waited=0 while [ -n "$(agent_pids)" ]; do [ "$waited" -ge 40 ] && break sleep 0.25 waited=$((waited + 1)) done if [ -n "$(agent_pids)" ]; then kill -KILL $(agent_pids) 2>/dev/null || true waited=0 while [ -n "$(agent_pids)" ]; do [ "$waited" -ge 12 ] && break sleep 0.25 waited=$((waited + 1)) done fi if [ -n "$(agent_pids)" ]; then echo "ERROR: myvpn-agent still running after SIGKILL; not safe to start a new one" >&2 exit 1 fi echo "myvpn stopped (tunnel down, agent stopped)" EOF chmod 0755 /usr/local/bin/myvpn-down cat > /usr/local/bin/myvpn-status <<'EOF' #!/bin/bash # Show the running agent's status (points the CLI at the agent's socket). export MYVPN_SOCKET=/var/run/myvpn-agent.sock exec /usr/local/bin/myvpn status "$@" EOF chmod 0755 /usr/local/bin/myvpn-status # Capture installed CLI version line for the install summary (first line only; # wrap in brace group + "|| true" so pipefail won't kill the script if myvpn # is missing or fails). INSTALLED_VERSION="$({ /usr/local/bin/myvpn version 2>/dev/null || true; } | head -1)" INSTALLED_VERSION="${INSTALLED_VERSION:-(version unknown)}" echo "================================================================" echo "已安装: $INSTALLED_VERSION" echo "================================================================" echo " /usr/local/bin/myvpn-agent /usr/local/bin/myvpn /usr/local/bin/myvpn-up /usr/local/bin/myvpn-down" echo " state dir: /var/lib/myvpn" echo echo "下一步:" echo " 1) sudo myvpn-down 先停旧 agent(若有);首次安装也无害" echo " 2) sudo myvpn-up 起新 agent + 登录 + 加入 mesh" echo " 首次启动会交互式询问一次邮箱+密码(密码仅用于本次认证,不写磁盘)" echo " 之后由持久化的 refresh token(180 天滑动续期)自动连上,无需再输密码" echo " up 完会问'是否开启登录长期有效?'——常开设备(NAS/服务器)可选 y(refresh 永不过期)" echo " 3) sudo myvpn autostart on 开机自启(需先 myvpn-up 成功登录一次)" echo echo "一键执行(复制此行;首次会暂停问邮箱密码):" echo " sudo myvpn-down && sudo myvpn-up && sudo myvpn autostart on" print_command_listing