本页目录

服务器生命周期:接入与移除

一套脚本把"买 VPS → Grafana 仪表盘出现指标"压缩成一条命令。接入 = bootstrap(放公钥/设 hostname/SSH config)→ 签发 mTLS 证书 → 部署 otel-collector → 配置 DIRECT 规则 → 校验到家;移除 = 停服务 → 吊销证书 → 清规则。全程幂等,每步独立判断,中途失败可重试。本章展开脚本完整逻辑与脱敏源码。

一、用法

# IP 模式(新买 VPS,仅需 IP + root 密码)
./add-server.sh <IP> --region cn|us|jp|eu|sg [--port N] [--dry-run]

# 别名模式(已有 SSH config + 免密)
./add-server.sh <ssh别名> [显示名] [--dry-run]

# 移除
./remove-server.sh <ssh别名> [fleet-name] [--purge] [--dry-run]

前置⁠:本机已建 CA,中转 rathole 就绪,DNS otlp.liz6.com 可解析。脚本用 set -euo pipefail,任何一步失败即停,重跑已完成的步骤自动跳过。

二、add-server.sh:从裸 IP 到上线

脚本分两阶段:bootstrap(仅 IP 模式,把裸 IP 变成 SSH 别名)→ 监控接入(IP 和别名模式共用 6 步)。

2.1 Bootstrap:裸 IP 预处理

当输入是 IP 时,先执行 5 个小步骤把远程机纳入 SSH 管理:

① 区域检测⁠:curl ip-api.com→ countryCode 映射:CN→cn,JP→jp,US→us,SG→sg 等。失败则提示手动 --region 指定。

② 自动命名⁠:扫 ~/.ssh/configHost li-<region>-node-<N> 条目,取 max(N)+1 生成新名。先查 IP 是否已有别名→有则复用;同 Host 指向同 IP→跳过,指向不同 IP→警告。

③ 放公钥⁠:ssh -o PasswordAuthentication=no root@<IP> echo ok→已有免密则跳过;否则 ssh-copy-id

④ 设 hostname:ssh root@<IP> hostname 看当前值→已是目标名则跳过;否则 hostnamectl set-hostname

⑤ 写 SSH config + 指纹确认⁠:追加 Host 块(含 StrictHostKeyChecking accept-new),首次连接需手动确认指纹。

_assign_name() {
  local region="$1" ip="$2"
  # 先查这个 IP 是否已有别名
  local existing; existing=$(grep -B5 "$ip" ~/.ssh/config 2>/dev/null \
    | grep '^Host ' | grep -v 'HostName' | sed 's/Host //' | head -1 || true)
  if [[ -n "$existing" ]]; then echo "$existing"; return; fi
  # 自动分配 li-<region>-node-<N>
  local max_n; max_n=$(grep -oP "Host li-${region}-node-\K\d+" ~/.ssh/config 2>/dev/null \
    | sort -n | tail -1 || echo 0)
  echo "li-${region}-node-$((max_n + 1))"
}

_deploy_key() {
  if ssh -o ConnectTimeout=8 -o PasswordAuthentication=no -p "$PORT" "root@$IP" \
    'echo ok' 2>/dev/null | grep -q ok; then
    echo "已有免密登录, 跳过"; return
  fi
  ssh-copy-id -o ConnectTimeout=15 -p "$PORT" "root@$IP"
}

_set_hostname() {
  local cur; cur=$(ssh root@$IP hostname 2>/dev/null | tr -d '\r\n' || true)
  if [[ "$cur" == "$1" ]]; then echo "已是 $1, 跳过"; return; fi
  ssh root@$IP "hostnamectl set-hostname '$1'"
}

2.2 监控接入 6 步(IP 和别名模式共用)

set -euo pipefail,每步幂等。脚本头部先判断输入类型:

_is_ip() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; }
if _is_ip "$INPUT"; then
  BOOTSTRAP=1                            # 先跑 bootstrap
else
  HOST="$INPUT"; BOOTSTRAP=0             # 跳过 bootstrap,直接接入
fi

1/6 签发客户端证书

CERTD="$CA_DIR/out/clients/$FLEET_NAME"
if [[ -d "$CERTD" ]]; then
  echo "证书已存在, 跳过"
else
  "$CA_DIR/issue-client-cert.sh" "$FLEET_NAME"  # EC P-256, clientAuth, 825 天
fi

2/6 探测远程架构 + 下载二进制

ARCH=$(ssh $HOST uname -m)
case "$ARCH" in
  x86_64|amd64) GOARCH=amd64;;
  aarch64|arm64) GOARCH=arm64;;
esac
# 从 GitHub release 下载 otelcol-contrib → ~/distfiles/ → scp 推送到远程
# 本地 ~/distfiles/ 缓存,版本不变时不重复下载

3/6 mihomo DIRECT 规则(本机+N100)

关键的防御性逻辑——远程 VPS 的公网 IP 必须加到本机和 N100 两边的 mihomo DIRECT 规则,否则 SSH 和 OTLP 承载连接会穿 TUN 代理:

# 解析: SSH 别名 → HostName → getent 解析真实 IP
resolve_host_ip() {
  local hostname; hostname=$(ssh -G "$1" 2>/dev/null | awk '/^hostname / {print $2; exit}')
  # 如果 HostName 是域名, getent 解析
  if [[ "$hostname" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "$hostname"; return
  fi
  getent ahosts "$hostname" 2>/dev/null | awk '{print $1; exit}'
}

# 判断是否公网 IP (内网段已由网段规则覆盖,不加冗余 /32)
is_public_ip() {
  local ip="$1"
  [[ "$ip" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|169\.254\.|224\.) ]] && return 1
  return 0
}

mihomo_add_direct() {
  local ip="$1" desc="$2"
  # 本机: grep 检查→已存在则跳过; 否则 sed 插入 + API 热重载
  if ! grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null; then
    sed -i "/255\.255\.255\.255\/32,DIRECT,no-resolve/a\  - IP-CIDR,${ip}/32,DIRECT,no-resolve   # $desc" \
      "$DESKTOP_MIHOMO_CONFIG"
    curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
      -H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
      -d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null
  fi
  # N100: ssh + grep→已存在则跳过; 否则 ssh sed + nikki reload
  if ! ssh $N100_HOST "grep -qF '${ip}/32' $N100_PROFILE" 2>/dev/null; then
    ssh $N100_HOST "sed -i '/255\.255\.255\.255\/32,DIRECT,no-resolve/a\  - IP-CIDR,${ip}/32,DIRECT,no-resolve   # $desc' \
      $N100_PROFILE && /etc/init.d/nikki reload" 2>/dev/null || \
      echo "[!] N100 不可达,请稍后手动加"
  fi
}

每边都幂等判断:已有则跳过,不存在则 sed 插在 255.255.255.255/32,DIRECT 那条策略下面(保持规则位置一致),然后 API 热重载不重启服务。

4/6 推送二进制+配置+证书

scp "$BIN" "$HOST:/usr/local/bin/otelcol-contrib"
scp "$TEMPLATE_DIR/config.yaml" "$HOST:/etc/otelcol-fleet/config.yaml"
scp "$CERTD/client.crt" "$CERTD/client.key" "$CERTD/ca.crt" "$HOST:/etc/otelcol-fleet/"
ssh $HOST 'chmod 0600 /etc/otelcol-fleet/client.key'

# 写入 env: 三个变量注入 config
ssh $HOST "cat > /etc/otelcol-fleet/env" <<EOF
FLEET_HOSTNAME=$FLEET_NAME
FLEET_OTLP_ENDPOINT=$OTLP_ENDPOINT
FLEET_COLLECT_INTERVAL=$FLEET_COLLECT_INTERVAL
EOF

config.yaml 是模板,用 env 注入端点/主机名——多台部署同一份模板+不同 env。

5/6 安装 systemd 并启动

scp "$TEMPLATE_DIR/otelcol-fleet.service" "$HOST:/etc/systemd/system/"
ssh $HOST 'systemctl daemon-reload && systemctl enable --now otelcol-fleet'

unit 设计:host_metrics 需读 /proc//sys,User=root 最省事;Restart=on-failure + RestartSec=5;RuntimeDirectory+StateDirectory 自动建目录。

6/6 校验

sleep 35  # 采集间隔 30s + batch buffer 5s
N=$(curl -s "http://127.0.0.1:9090/api/v1/query" \
    --data-urlencode "query=count({host_name=\"$FLEET_NAME\"})" \
    | grep -oP '"value":\[[^]]*,"\K[0-9]+' | head -1 || true)
if [[ -n "$N" && "$N" -gt 0 ]]; then
  echo "[+] 完成! Prometheus 收到 $N 条序列. Grafana 下拉自动出现 $FLEET_NAME."
else
  echo "[!] 未收到指标, 排查: ssh $HOST journalctl -u otelcol-fleet -n50"
fi

2.3 常见失败

步骤症状原因解决
2/6SSH timeoutmihomo 出口瞬时切海外绕路等几秒重试
5/6active(auto-restart)队列目录未建已修:create_directory:true
6/6active 但序列=0TLS 握手失败 / DNS 不通journalctl -u otelcol-fleet 看 OTLP 报错

三、remove-server.sh:完整移除

# 停服务+删配置 → 吊销证书 → 清 DIRECT → 清 SSH config → 完成
./remove-server.sh <ssh-host> [fleet-name] [--purge]

--purge 额外删远程二进制 + 落盘队列。不传 fleet-name 时从 SSH config 的 HostName 反查。

1/4 停服务+删配置

ssh $HOST 'systemctl disable --now otelcol-fleet 2>/dev/null || true
rm -f /etc/systemd/system/otelcol-fleet.service
rm -rf /etc/otelcol-fleet
systemctl daemon-reload'
# --purge: 额外 rm /usr/local/bin/otelcol-contrib + /var/lib/otelcol-fleet

2/4 吊销证书

# CN 追加到 ca/out/revoked.txt(去重)
# sync-revoked.sh: 逐行生成 "abort CN <name>" → revoked.caddy
# systemctl reload caddy → 即时生效, 证书未到期也 403
"$CA_DIR/revoke-client-cert.sh" "$NAME"

3/4 清 DIRECT 规则(两边幂等)

mihomo_remove_direct() {
  local ip="$1"
  # 本机: grep→不存在则跳过; 存在则 sed 删行 + API 热重载
  grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null || return
  sed -i "/${ip//./\\.}\\/32.*DIRECT,no-resolve/d" "$DESKTOP_MIHOMO_CONFIG"
  curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
    -H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
    -d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null
  # N100 同理, ssh grep+sed+reload
}

4/4 清理 SSH 记录

# ~/.ssh/config: 删 Host <name> 块(含下属缩进行)
sed -i '/^Host '"$name"'$/,/^[^[:space:]]/d' ~/.ssh/config
# known_hosts: ssh-keygen -R <name> + ssh-keygen -R <ip>

Prometheus 历史数据

已有序列随 staleness 自然过期(5 分钟),Grafana 下拉自动消失。需立即删除时启用 admin API 后 POST /api/v1/admin/tsdb/delete_series

四、配套文件

otelcol-fleet.service

[Unit]
Description=OpenTelemetry Collector (metrics-fleet agent)
After=network-online.target

[Service]
Type=simple
EnvironmentFile=/etc/otelcol-fleet/env
ExecStart=/usr/local/bin/otelcol-contrib --config /etc/otelcol-fleet/config.yaml
Restart=on-failure
RestartSec=5
User=root
RuntimeDirectory=otelcol-fleet
StateDirectory=otelcol-fleet

[Install]
WantedBy=multi-user.target

host_metrics 需要读 /proc//sys,User=root 最省事;如需降权需额外配 CAP_SYS_PTRACE+CAP_DAC_READ_SEARCH

命名规范

前缀区域编号
li-home-家里li-home-0(本机),li-home-1(Windows),li-home-router(N100 旁路由)
li-cn-node-国内N
li-us-node-美国N
li-jp-node-日本N

当前覆盖:家里 3 台(本机/Windows/N100)、国内 2 台(旧中转+腾讯云)、美国 3 台(VPS)、日本 1 台。

五、附录:完整脚本

add-server.sh

#!/usr/bin/env bash
# 接入一台服务器到监控 fleet (push 模型:远程跑 otel-collector hostmetrics, mTLS 推回家).
# 支持从裸 IP 启动全流程,也支持已有 SSH 别名. 所有步骤幂等:已完成的静默跳过.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
source ./fleet.env
source ./mihomo-sync.sh

# ── 参数 ──
DRY_RUN=0; PORT=22; REGION="auto"
INPUT=""; FLEET_NAME=""
for a in "$@"; do
  case "$a" in
    --port)   shift; PORT="$1";;
    --region) shift; REGION="$1";;
    --dry-run) DRY_RUN=1;;
    -*) echo "[!] 未知: $a" >&2; exit 1;;
    *) [[ -z "$INPUT" ]] && INPUT="$a" || FLEET_NAME="$a";;
  esac
done
[[ -n "$INPUT" ]] || { echo "用法: add-server.sh <IP|ssh别名> [显示名] [--port N] [--region auto|cn|us|jp]" >&2; exit 1; }

# ── 输入类型:IP 还是 SSH 别名 ──
_is_ip() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; }
if _is_ip "$INPUT"; then
  IP="$INPUT"; BOOTSTRAP=1
else
  HOST="$INPUT"; BOOTSTRAP=0
fi
FLEET_NAME="${FLEET_NAME:-$INPUT}"

# ── 辅助:幂等放公钥 ──
_deploy_key() {
  echo "==> 放公钥"
  if ssh -o ConnectTimeout=8 -o PasswordAuthentication=no -p "$PORT" "root@$IP" \
    'echo ok' 2>/dev/null | grep -q ok; then
    echo "    已有免密,跳过"; return
  fi
  ssh-copy-id -o ConnectTimeout=15 -p "$PORT" "root@$IP"
  echo "    公钥已部署"
}

# ── 辅助:幂等设 hostname ──
_set_hostname() {
  local name="$1"
  echo "==> 设置远程 hostname: $name"
  local cur
  cur=$(ssh -o ConnectTimeout=10 -p "$PORT" "root@$IP" 'hostname' 2>/dev/null | tr -d '\r\n' || true)
  if [[ "$cur" == "$name" ]]; then echo "    已是 $name,跳过"; return; fi
  ssh -o ConnectTimeout=10 -p "$PORT" "root@$IP" "hostnamectl set-hostname '$name'"
  echo "    已设为 $name"
}

# ── 辅助:幂等写 ssh config ──
_add_ssh_config() {
  local name="$1" ip="$2" port="$3"
  echo "==> 同步 ~/.ssh/config"
  if grep -q "^Host $name$" ~/.ssh/config 2>/dev/null; then
    local existing_ip; existing_ip=$(grep -A5 "^Host $name$" ~/.ssh/config | awk '/HostName / {print $2; exit}')
    if [[ "$existing_ip" == "$ip" ]]; then
      echo "    Host $name$ip 已存在,跳过"; return
    else
      echo "    [!] Host $name 存在但指向 $existing_ip (非 $ip),跳过写入"; return
    fi
  fi
  cat >> ~/.ssh/config <<EOF

# metrics-fleet $(date +%Y-%m-%d)
Host $name
    HostName $ip
    Port $port
    User root
    IdentityFile ~/.ssh/id_ed25519
    StrictHostKeyChecking accept-new
EOF
  echo "    已追加 Host $name ($ip:$port)"
}

# ── 辅助:自动分配命名 ──
_assign_name() {
  local region="$1" ip="$2"
  local existing; existing=$(grep -B5 "$ip" ~/.ssh/config 2>/dev/null \
    | grep '^Host ' | grep -v 'HostName' | sed 's/Host //' | head -1 || true)
  if [[ -n "$existing" ]]; then echo "$existing"; return; fi
  local max_n; max_n=$(grep -oP "Host li-${region}-node-\K\d+" ~/.ssh/config 2>/dev/null \
    | sort -n | tail -1 || echo 0)
  echo "li-${region}-node-$((max_n + 1))"
}

# ═══ Bootstrap:仅 IP 输入 ═══
if [[ "$BOOTSTRAP" == 1 ]]; then
  echo "=== Bootstrap:从裸 IP 启动 ==="
  [[ "$REGION" != "auto" ]] || { echo "[!] IP 模式需指定 --region" >&2; exit 1; }
  HOST=$(_assign_name "$REGION" "$IP")
  echo "    区域:$REGION | 别名:$HOST"
  FLEET_NAME="${FLEET_NAME:-$HOST}"
  _deploy_key
  _set_hostname "$HOST"
  _add_ssh_config "$HOST" "$IP" "$PORT"
  ssh -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 "$HOST" 'echo ok' >/dev/null
else
  echo "=== SSH 别名模式:$HOST ==="
  FLEET_NAME="${FLEET_NAME:-$HOST}"
fi

if [[ "$DRY_RUN" == 1 ]]; then
  echo; echo "[dry-run] 将执行:签发证书+探测架构+DIRECT+推送+systemd+校验"
  exit 0
fi

# ═══ 监控接入 6 步(幂等) ═══
SSH="ssh -o ConnectTimeout=10 $HOST"
OTLP_ENDPOINT="https://${RELAY_HOST}:${RELAY_PORT}${OTLP_PATH}"

echo "==> [1/6] 签发客户端证书:$FLEET_NAME"
CERTD="$CA_DIR/out/clients/$FLEET_NAME"
if [[ -d "$CERTD" ]]; then echo "    证书已存在,跳过"
else "$CA_DIR/issue-client-cert.sh" "$FLEET_NAME"; fi

echo "==> [2/6] 探测远程架构"
ARCH="$($SSH uname -m)"
case "$ARCH" in
  x86_64|amd64) GOARCH=amd64;;
  aarch64|arm64) GOARCH=arm64;;
  *) echo "[!] 不支持的架构:$ARCH" >&2; exit 1;;
esac

echo "==> [3/6] mihomo DIRECT 规则(本机+N100)"
REMOTE_IP=$(resolve_host_ip "$HOST")
if [[ -n "$REMOTE_IP" ]] && is_public_ip "$REMOTE_IP"; then
  mihomo_add_direct "$REMOTE_IP" "metrics-fleet:$FLEET_NAME"
else
  echo "    内网/无法解析,跳过 DIRECT"
fi

echo "==> [4/6] 推送二进制+配置+证书"
DISTFILES="$HOME/distfiles"
BIN="$DISTFILES/otelcol-contrib_${OTELCOL_VERSION}_linux_${GOARCH}"
if [[ ! -x "$BIN" ]]; then
  mkdir -p "$DISTFILES"
  TARURL="https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTELCOL_VERSION}/otelcol-contrib_${OTELCOL_VERSION}_linux_${GOARCH}.tar.gz"
  tmp="$(mktemp -d)"
  curl -fSL "$TARURL" -o "$tmp/o.tgz"
  tar -xzf "$tmp/o.tgz" -C "$tmp" otelcol-contrib
  mv "$tmp/otelcol-contrib" "$BIN"
  chmod +x "$BIN"; rm -rf "$tmp"
fi
$SSH 'install -d -m 0750 /etc/otelcol-fleet'
scp -q "$BIN" "$HOST:/usr/local/bin/otelcol-contrib"
scp -q "$HERE/../otel/config.yaml" "$HOST:/etc/otelcol-fleet/config.yaml"
scp -q "$CERTD/client.crt" "$CERTD/client.key" "$CERTD/ca.crt" "$HOST:/etc/otelcol-fleet/"
$SSH 'chmod 0600 /etc/otelcol-fleet/client.key'
$SSH "cat > /etc/otelcol-fleet/env" <<EOF
FLEET_HOSTNAME=$FLEET_NAME
FLEET_OTLP_ENDPOINT=$OTLP_ENDPOINT
FLEET_COLLECT_INTERVAL=$FLEET_COLLECT_INTERVAL
EOF

echo "==> [5/6] 安装 systemd 服务并启动"
scp -q "$HERE/../otel/otelcol-fleet.service" "$HOST:/etc/systemd/system/otelcol-fleet.service"
$SSH 'systemctl daemon-reload && systemctl enable --now otelcol-fleet'

echo "==> [6/6] 校验"
sleep 3
if $SSH 'systemctl is-active --quiet otelcol-fleet'; then echo "    远程服务:active"
else echo "[!] 未起来:ssh $HOST journalctl -u otelcol-fleet -n50" >&2; exit 1; fi
echo "    等待指标到家..."
sleep 35
N="$(curl -s "http://127.0.0.1:9090/api/v1/query" \
  --data-urlencode "query=count({host_name=\"$FLEET_NAME\"})" \
  | grep -oP '"value":\[[^]]*,"\K[0-9]+' | head -1 || true)"
if [[ -n "${N:-}" && "$N" -gt 0 ]]; then
  echo "[+] 完成! Prometheus 收到 $N 条序列. Grafana 下拉自动出现 $FLEET_NAME."
else
  echo "[!] 远程已启动但未收到指标. 排查: ssh $HOST journalctl -u otelcol-fleet -n50"
fi

remove-server.sh

#!/usr/bin/env bash
# 移除一台服务器:停 collector → 吊销证书 → 清 DIRECT → 清 SSH config → 完成.
# 所有步骤幂等,已清除的静默跳过.
set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
source ./fleet.env
source ./mihomo-sync.sh

HOST="${1:?用法:remove-server.sh <ssh-host> [fleet-name] [--purge] [--dry-run]}"
NAME="$HOST"; PURGE=0; DRY_RUN=0
shift
for a in "$@"; do
  case "$a" in
    --purge) PURGE=1;;
    --dry-run) DRY_RUN=1;;
    *) NAME="$a";;
  esac
done
SSH="ssh -o ConnectTimeout=10 $HOST"
IP=$(resolve_host_ip "$HOST")

# ── 辅助:清理 ssh config ──
_remove_ssh_config() {
  local name="$1"
  if ! grep -q "^Host $name$" ~/.ssh/config 2>/dev/null; then
    echo "    ~/.ssh/config:Host $name 未找到,跳过"; return
  fi
  sed -i '/^Host '"$name"'$/,/^[^[:space:]]/{/^Host '"$name"'$/d;/^[[:space:]]/d}' ~/.ssh/config
  echo "    已从 ~/.ssh/config 移除 Host $name"
}

# ═══ 主流程 ═══
echo "=== 移除服务器:$NAME ($HOST) ==="

echo "==> [1/4] 停止并卸载远程 collector"
if [[ "$DRY_RUN" == 0 ]]; then
  $SSH 'systemctl disable --now otelcol-fleet 2>/dev/null || true
  rm -f /etc/systemd/system/otelcol-fleet.service
  rm -rf /etc/otelcol-fleet
  systemctl daemon-reload' 2>/dev/null || true
  echo "    collector 已停止并卸载配置"
  if [[ "$PURGE" == 1 ]]; then
    $SSH 'rm -f /usr/local/bin/otelcol-contrib; rm -rf /var/lib/otelcol-fleet' 2>/dev/null || true
    echo "    已 purge 二进制与队列"
  fi
fi

echo "==> [2/4] 吊销客户端证书:$NAME"
[[ "$DRY_RUN" == 0 ]] && "$CA_DIR/revoke-client-cert.sh" "$NAME"

echo "==> [3/4] mihomo DIRECT 规则移除"
if [[ -n "$IP" ]] && is_public_ip "$IP"; then
  mihomo_remove_direct "$IP"
else
  echo "    内网/无法解析,跳过"
fi

echo "==> [4/4] 清理 SSH 记录"
_remove_ssh_config "$NAME"
ssh-keygen -R "$NAME" 2>/dev/null || true

echo "[+] $NAME 已完整移除 (collector+证书+DIRECT+SSH 记录)."
echo "    已有指标随 staleness 自然过期. 如需立即删除:Prometheus admin API delete_series."

mihomo-sync.sh(DIRECT 规则同步辅助)

#!/usr/bin/env bash
# mihomo DIRECT IP 规则同步:本机+N100 旁路由.
# 由 add-server.sh / remove-server.sh source. 幂等.

# 解析 SSH 别名 → IP
resolve_host_ip() {
  local hostname; hostname=$(ssh -G "$1" 2>/dev/null | awk '/^hostname / {print $2; exit}')
  [[ -z "$hostname" ]] && { echo ""; return; }
  if [[ "$hostname" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
    echo "$hostname"; return
  fi
  getent ahosts "$hostname" 2>/dev/null | awk '{print $1; exit}'
}

# 判断是否公网 IP
is_public_ip() {
  local ip="$1"
  [[ -z "$ip" ]] && return 1
  [[ "$ip" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.|169\.254\.|224\.) ]] && return 1
  return 0
}

# 加 DIRECT 规则(两边幂等)
mihomo_add_direct() {
  local ip="$1" desc="${2:-}"
  local comment=""; [[ -n "$desc" ]] && comment="   # $desc"
  local line="  - IP-CIDR,${ip}/32,DIRECT,no-resolve${comment}"
  # 本机
  if ! grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null; then
    echo "$SUDO_PASSWORD" | sudo -S sed -i \
      "/255\.255\.255\.255\/32,DIRECT,no-resolve/a\\${line}" "$DESKTOP_MIHOMO_CONFIG"
    curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
      -H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
      -d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null \
      && echo "    本机:已加 ${ip}/32 DIRECT" || echo "    本机:已写配置(热重载失败)"
  else echo "    本机:${ip}/32 已存在,跳过"; fi
  # N100
  if ! ssh -o ConnectTimeout=8 "$N100_HOST" "grep -qF '${ip}/32' $N100_PROFILE" 2>/dev/null; then
    ssh -o ConnectTimeout=8 "$N100_HOST" \
      "sed -i \"/255\\.255\\.255\\.255\\/32,DIRECT,no-resolve/a\\${line}\" $N100_PROFILE \
       && (/etc/init.d/nikki reload 2>/dev/null || true)" 2>/dev/null \
      && echo "    N100:已加 ${ip}/32 DIRECT" || echo "    [!] N100 不可达,请稍后手动加"
  else echo "    N100:${ip}/32 已存在,跳过"; fi
}

# 删 DIRECT 规则(两边幂等)
mihomo_remove_direct() {
  local ip="$1"; local escaped; escaped=$(echo "$ip" | sed 's/\./\\./g')
  # 本机
  if grep -qF "${ip}/32" "$DESKTOP_MIHOMO_CONFIG" 2>/dev/null; then
    echo "$SUDO_PASSWORD" | sudo -S sed -i "/${escaped}\\/32.*DIRECT,no-resolve/d" "$DESKTOP_MIHOMO_CONFIG"
    curl -sf -X PUT "http://127.0.0.1:9999/configs?force=true" \
      -H "Authorization: Bearer ${DESKTOP_MIHOMO_SECRET}" \
      -d "{\"path\":\"${DESKTOP_MIHOMO_CONFIG}\"}" >/dev/null \
      && echo "    本机:已删 ${ip}/32" || echo "    本机:已写配置(热重载失败)"
  fi
  # N100
  if ssh -o ConnectTimeout=8 "$N100_HOST" "grep -qF '${ip}/32' $N100_PROFILE" 2>/dev/null; then
    ssh -o ConnectTimeout=8 "$N100_HOST" \
      "sed -i \"/${escaped}\\/32.*DIRECT,no-resolve/d\" $N100_PROFILE \
       && (/etc/init.d/nikki reload 2>/dev/null || true)" 2>/dev/null \
      && echo "    N100:已删 ${ip}/32" || echo "    [!] N100 不可达,请稍后手动删"
  fi
}

相关文档