服务器生命周期:接入与移除
一套脚本把"买 VPS → Grafana 仪表盘出现指标"压缩成一条命令。接入 = bootstrap(放公钥/设 hostname/SSH config)→ 签发 mTLS 证书 → 部署 otel-collector → 配置 DIRECT 规则 → 校验到家;移除 = 停服务 → 吊销证书 → 清规则。全程幂等,每步独立判断,中途失败可重试。本章展开脚本完整逻辑与脱敏源码。
一、用法
./add-server.sh <IP> --region cn|us|jp|eu|sg [--port N] [--dry-run]
./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/config 中 Host 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"
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))"
}
_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 else
HOST="$INPUT"; BOOTSTRAP=0 fi
1/6 签发客户端证书
CERTD="$CA_DIR/out/clients/$FLEET_NAME"
if [[ -d "$CERTD" ]]; then
echo "证书已存在, 跳过"
else
"$CA_DIR/issue-client-cert.sh" "$FLEET_NAME" fi
2/6 探测远程架构 + 下载二进制
ARCH=$(ssh $HOST uname -m)
case "$ARCH" in
x86_64|amd64) GOARCH=amd64;;
aarch64|arm64) GOARCH=arm64;;
esac
3/6 mihomo DIRECT 规则(本机+N100)
关键的防御性逻辑——远程 VPS 的公网 IP 必须加到本机和 N100 两边的 mihomo DIRECT 规则,否则 SSH 和 OTLP 承载连接会穿 TUN 代理:
resolve_host_ip() {
local hostname; hostname=$(ssh -G "$1" 2>/dev/null | awk '/^hostname / {print $2; exit}')
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}'
}
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"
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
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'
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 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/6 | SSH timeout | mihomo 出口瞬时切海外绕路 | 等几秒重试 |
| 5/6 | active(auto-restart) | 队列目录未建 | 已修:create_directory:true |
| 6/6 | active 但序列=0 | TLS 握手失败 / DNS 不通 | journalctl -u otelcol-fleet 看 OTLP 报错 |
三、remove-server.sh:完整移除
./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'
2/4 吊销证书
"$CA_DIR/revoke-client-cert.sh" "$NAME"
3/4 清 DIRECT 规则(两边幂等)
mihomo_remove_direct() {
local ip="$1"
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
}
4/4 清理 SSH 记录
sed -i '/^Host '"$name"'$/,/^[^[:space:]]/d' ~/.ssh/config
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
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; }
_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 " 公钥已部署"
}
_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"
}
_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))"
}
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
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
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")
_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 规则同步辅助)
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}'
}
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
}
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
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
}
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
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
}
相关文档