网站搭建在本地,通过 frp 内网穿透分别连接到主线 VPS1 和备线 VPS2,本地监控脚本探测 frpc 状态,主线故障时自动切换 Cloudflare DNS 到备线 VPS2,实现服务故障转移。
脚本本身提供安装、配置、查看日志、卸载等功能。![]()
🎉 脚本界面
========================================
frpc 双线监控 + CF DNS 自动切换管理器
========================================
1. 安装/配置监控脚本 (首次安装)
2. 修改配置
3. 查看实时日志
4. 卸载并清理所有文件
0. 退出
========================================
请输入选项编号:
✨ 功能特性
| 功能模块 | 说明 | 状态 |
|---|---|---|
| 🎯双线热备 | 主线+备线 VPS 自动切换 | ✅ |
| 🔄DNS 自动故障转移 | 基于 Cloudflare API 实时切换 | ✅ |
| 🐳Docker 原生支持 | 自动检测和管理 frpc 容器 | ✅ |
| 📊Dashboard 健康检查 | 隧道状态实时监控 | ✅ |
| 🔧交互式配置向导 | 首次安装零门槛 | ✅ |
| 📝灵活配置管理 | 支持单项修改和批量管理 | ✅ |
| 📈日志轮转 | 自动管理日志文件 | ✅ |
| 🚦Systemd 集成 | 开机自启 + 守护进程 | ✅ |
🏗️ 架构原理

📋 工作流程
- 健康检查 🔍
- 每 30 秒检查两个 frpc 容器的 Dashboard API
- 验证隧道状态是否为
running - 自动重启无响应的 frpc 容器
- 故障判断 🧠
- 主线宕机 → 立即切换所有域名到备线 IP
- 主线恢复 → 自动切回主线 IP
- 双线均宕 → 保持最后状态,等待恢复
- DNS 更新 ☁️
- 调用 Cloudflare API 批量更新 A 记录
- TTL=60 秒,实现快速收敛
- 支持代理模式开关
🚀 快速开始
- 操作系统: Linux (CentOS 7+, Ubuntu 16.04+, Debian 9+)
- 权限: Root 或 sudo 权限
- 依赖: Docker 已安装并运行
- 网络: 能够访问 Cloudflare API
⚙️ 安装步骤
1️⃣ 创建 frpc-failover.sh 文件
sudo nano /opt/frpc-failover.sh
2️⃣ 粘贴 👇脚本代码 后 Ctrl+X → Y → Enter 保存退出
#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换 - 管理脚本 v1.0
# =============================================================
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# 路径定义
INSTALL_DEPS_SCRIPT="/opt/install-deps.sh"
MONITOR_SCRIPT="/opt/frpc-monitor.sh"
SERVICE_FILE="/etc/systemd/system/frpc-monitor.service"
LOGROTATE_FILE="/etc/logrotate.d/frpc-monitor"
LOG_FILE="/var/log/frpc-monitor.log"
STATE_FILE="/tmp/frpc_monitor_state"
# 检查权限
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}请使用 root 用户或 sudo 运行此脚本${NC}"
exit 1
fi
# ================================================================
# 主菜单
# ================================================================
print_menu() {
clear
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} frpc 双线监控 + CF DNS 自动切换管理器${NC}"
echo -e "${CYAN}========================================${NC}"
echo " 1. 安装/配置监控脚本 (首次安装)"
echo -e " ${YELLOW}2. 修改配置${NC}"
echo " 3. 查看实时日志"
echo " 4. 卸载并清理所有文件"
echo -e " ${YELLOW}0. 退出${NC}"
echo -e "${CYAN}========================================${NC}"
}
# ================================================================
# -------- 安装流程 --------
# ================================================================
# -------- 1. 安装依赖(curl / jq / docker)--------
install_deps() {
echo -e "${GREEN}[1/5] 检查并安装系统依赖 (curl, jq, docker)...${NC}"
cat <<'DEPS_EOF' > "$INSTALL_DEPS_SCRIPT"
#!/bin/bash
if command -v apt-get &>/dev/null; then
PKG_MANAGER="apt-get"; UPDATE_CMD="apt-get update -y"; INSTALL_CMD="apt-get install -y"
elif command -v dnf &>/dev/null; then
PKG_MANAGER="dnf"; UPDATE_CMD="dnf makecache -y"; INSTALL_CMD="dnf install -y"
elif command -v yum &>/dev/null; then
PKG_MANAGER="yum"; UPDATE_CMD="yum makecache -y"; INSTALL_CMD="yum install -y"
else
echo "未识别的包管理器,请手动安装:curl、jq"; exit 1
fi
$UPDATE_CMD &>/dev/null
install_pkg() {
local name=$1 cmd=$2
if command -v "$cmd" &>/dev/null; then
echo " ${name} 已安装"
else
echo " 安装 ${name}..."
$INSTALL_CMD "$name" &>/dev/null \
&& echo " ${name} 安装成功" \
|| { echo " ${name} 安装失败"; exit 1; }
fi
}
install_pkg "curl" "curl"
install_pkg "jq" "jq"
install_pkg "docker" "docker"
DEPS_EOF
chmod +x "$INSTALL_DEPS_SCRIPT"
bash "$INSTALL_DEPS_SCRIPT"
}
# -------- 辅助:读取必填字符串,不允许为空 --------
# 用法: _read_required VAR "提示语"
_read_required() {
local _var=$1 _prompt=$2 _val
while true; do
read -p "$_prompt" _val
if [[ -n "$_val" ]]; then
printf -v "$_var" '%s' "$_val"
return
fi
echo -e " ${RED}不能为空,请重新输入${NC}"
done
}
# -------- 辅助:读取端口(1-65535),允许回车使用默认值 --------
# 用法: _read_port VAR "提示语" 默认值
_read_port() {
local _var=$1 _prompt=$2 _default=$3 _val
while true; do
read -p "$_prompt" _val
_val=${_val:-$_default}
if [[ "$_val" =~ ^[0-9]+$ ]] && (( _val >= 1 && _val <= 65535 )); then
printf -v "$_var" '%s' "$_val"
return
fi
echo -e " ${RED}请输入 1~65535 的整数${NC}"
done
}
# -------- 2. 交互式采集配置 --------
# 返回值:0=成功,1=用户取消(未输入域名)
collect_config() {
echo -e "\n${GREEN}[2/5] 检测 frpc 容器...${NC}"
mapfile -t FRPC_CONTAINERS < <(docker ps --format "{{.Names}}" 2>/dev/null | grep frpc | sort)
if [[ ${#FRPC_CONTAINERS[@]} -eq 0 ]]; then
echo -e "${YELLOW}未检测到运行中的 frpc 容器,请确保 Docker 已启动且容器名包含 frpc${NC}"
else
echo -e "${GREEN}检测到以下 frpc 容器:${NC}"
for c in "${FRPC_CONTAINERS[@]}"; do echo " - $c"; done
fi
echo -e "\n${CYAN}[3/5] 请输入配置信息:${NC}"
_read_required CF_API_TOKEN "Cloudflare API Token: "
_read_required CF_ZONE_ID "Cloudflare Zone ID: "
_read_required VPS1_IP "主线 VPS1 IP: "
_read_required VPS2_IP "备线 VPS2 IP: "
echo -e "\n${CYAN}--- frpc 容器名 ---${NC}"
if [[ ${#FRPC_CONTAINERS[@]} -ge 2 ]]; then
read -p "frpc01 容器名 (默认 ${FRPC_CONTAINERS[0]}): " C1
FRPC1_CONTAINER=${C1:-${FRPC_CONTAINERS[0]}}
read -p "frpc02 容器名 (默认 ${FRPC_CONTAINERS[1]}): " C2
FRPC2_CONTAINER=${C2:-${FRPC_CONTAINERS[1]}}
else
echo -e "${YELLOW}未自动检测到足够容器,请手动输入${NC}"
_read_required FRPC1_CONTAINER "frpc01 容器名: "
_read_required FRPC2_CONTAINER "frpc02 容器名: "
fi
echo -e "\n${CYAN}--- frpc Dashboard 端口 ---${NC}"
_read_port FRPC1_DASH_PORT "frpc01 Dashboard 端口 (默认 7401): " 7401
_read_port FRPC2_DASH_PORT "frpc02 Dashboard 端口 (默认 7402): " 7402
echo -e "\n${CYAN}--- frpc Dashboard 认证 ---${NC}"
_read_required FRPC_USER "Dashboard 用户名: "
_read_required FRPC_PASS "Dashboard 密码: "
echo -e "\n${CYAN}--- 需要切换的域名 ---${NC}"
DOMAIN_LIST=()
_read_domains_input
DOMAINS=("${DOMAIN_LIST[@]}")
if [[ ${#DOMAINS[@]} -eq 0 ]]; then
echo -e "${RED}未输入任何域名,操作取消${NC}"
return 1
fi
echo -e "\n${GREEN}已录入 ${#DOMAINS[@]} 个域名:${NC}"
for d in "${DOMAINS[@]}"; do echo " - $d"; done
return 0
}
# -------- 3. 生成主监控脚本 --------
generate_monitor_script() {
echo -e "\n${GREEN}[4/5] 生成监控脚本...${NC}"
# heredoc 使用单引号定界符,防止变量展开。
# FRPC1_URL / FRPC2_URL 使用占位符,由后续 sed 替换为静态字符串,
# 避免 heredoc 内 ${FRPC1_DASH_PORT} 因未赋值而展开为空。
cat <<'SCRIPT_EOF' > "$MONITOR_SCRIPT"
#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换脚本
# 依赖:curl / jq / docker
# =============================================================
CF_API_TOKEN="CF_TOKEN_PLACEHOLDER"
CF_ZONE_ID="ZONE_ID_PLACEHOLDER"
VPS1_IP="VPS1_IP_PLACEHOLDER"
VPS2_IP="VPS2_IP_PLACEHOLDER"
DOMAINS=(
DOMAIN_ARRAY_PLACEHOLDER
)
FRPC1_DASH_PORT=FRPC1_DASH_PORT_PLACEHOLDER
FRPC2_DASH_PORT=FRPC2_DASH_PORT_PLACEHOLDER
FRPC1_URL="FRPC1_URL_PLACEHOLDER"
FRPC2_URL="FRPC2_URL_PLACEHOLDER"
FRPC_USER="USER_PLACEHOLDER"
FRPC_PASS="PASS_PLACEHOLDER"
FRPC1_CONTAINER="CON1_PLACEHOLDER"
FRPC2_CONTAINER="CON2_PLACEHOLDER"
DNS_TTL=60
CHECK_INTERVAL=30
RESTART_WAIT=15
CF_PROXIED=false
LOG_FILE="LOG_FILE_PLACEHOLDER"
STATE_FILE="STATE_FILE_PLACEHOLDER"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" >&2; }
log_ok() { log "[OK] $1"; }
log_warn() { log "[!!] $1"; }
log_err() { log "[XX] $1"; }
log_info() { log "[--] $1"; }
load_state() { [[ -f "$STATE_FILE" ]] && source "$STATE_FILE" || LAST_STATE="UNKNOWN"; }
save_state() { echo "LAST_STATE=\"$1\"" > "$STATE_FILE"; }
check_dashboard() {
local code
code=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 --max-time 8 \
-u "${FRPC_USER}:${FRPC_PASS}" "$1" 2>/dev/null)
[[ "$code" == "200" ]]
}
restart_frpc() {
local container=$1
log_info " 重启容器: ${container}"
if docker restart "$container" &>/dev/null; then
log_ok " ${container} 重启成功,等待 ${RESTART_WAIT}s"
sleep "$RESTART_WAIT"
return 0
else
log_err " ${container} 重启失败"
return 1
fi
}
get_vps_status() {
local dashboard_url=$1 container=$2
if ! check_dashboard "$dashboard_url"; then
log_warn "${container}: Dashboard 无响应,尝试重启"
if restart_frpc "$container" && check_dashboard "$dashboard_url"; then
log_ok "${container} 重启后 Dashboard 恢复"
else
log_err "${container} 重启后仍无响应,按宕机处理"
echo "VPS_DOWN"; return
fi
fi
local result
result=$(curl -s --connect-timeout 5 --max-time 8 \
-u "${FRPC_USER}:${FRPC_PASS}" "$dashboard_url" 2>/dev/null)
if echo "$result" | jq -e '.. | objects | select(.status == "running")' &>/dev/null; then
echo "OK"
else
log_err "${container}: Dashboard 正常但隧道全部离线,按宕机处理"
echo "VPS_DOWN"
fi
}
get_record_info() {
local domain=$1 response
response=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${domain}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
local record_id current_ip
record_id=$(echo "$response" | jq -r '.result[0].id // ""')
current_ip=$(echo "$response" | jq -r '.result[0].content // ""')
echo "${record_id}|${current_ip}"
}
update_record() {
local domain=$1 target_ip=$2
[[ -z "$domain" ]] && return 2
local info record_id current_ip
local max_retries=3 attempt delay
# ---------- 第一步:获取记录 ID(含重试)----------
for (( attempt=1; attempt<=max_retries; attempt++ )); do
info=$(get_record_info "$domain")
record_id=$(echo "$info" | cut -d'|' -f1)
current_ip=$(echo "$info" | cut -d'|' -f2)
[[ -n "$record_id" ]] && break
delay=$(( attempt * 2 ))
log_warn " | ${domain} -> 获取记录失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
sleep "$delay"
done
if [[ -z "$record_id" ]]; then
log_err " | ${domain} -> 获取记录失败,已重试 ${max_retries} 次,放弃"
return 1
fi
if [[ "$current_ip" == "$target_ip" ]]; then
log_info " | ${domain} -> 已是 ${target_ip},无需更新"
return 0
fi
# ---------- 第二步:更新记录(含重试)----------
local response success
for (( attempt=1; attempt<=max_retries; attempt++ )); do
response=$(curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${record_id}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${domain}\",\"content\":\"${target_ip}\",\"ttl\":${DNS_TTL},\"proxied\":${CF_PROXIED}}")
success=$(echo "$response" | jq -r '.success')
if [[ "$success" == "true" ]]; then
log_ok " | ${domain} -> ${current_ip} => ${target_ip}"
return 0
fi
delay=$(( attempt * 2 ))
log_warn " | ${domain} -> 更新失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
sleep "$delay"
done
log_err " | ${domain} -> 更新失败,已重试 ${max_retries} 次,放弃: $(echo "$response" | jq -r '.errors[0].message // "未知错误"')"
return 1
}
switch_all() {
local target_ip=$1 label=$2 ok=0 fail=0
local failed_domains=()
log "========================================"
log "[>>] 批量切换 -> ${target_ip} (${label})"
log "----------------------------------------"
for domain in "${DOMAINS[@]}"; do
update_record "$domain" "$target_ip"
case $? in
0) ((ok++)) ;;
1) ((fail++)); failed_domains+=("$domain") ;;
esac
sleep 0.5
done
# ---------- 对失败域名做一轮补偿重试 ----------
if [[ ${#failed_domains[@]} -gt 0 ]]; then
log "----------------------------------------"
log_warn "对 ${#failed_domains[@]} 个失败域名进行补偿重试 (等待 5s)..."
sleep 5
local retry_ok=0 retry_fail=0
for domain in "${failed_domains[@]}"; do
update_record "$domain" "$target_ip"
case $? in
0) ((retry_ok++)); ((ok++)); ((fail--)) ;;
1) ((retry_fail++)) ;;
esac
sleep 1
done
log_info "补偿结果: 补救成功 ${retry_ok} 个,仍失败 ${retry_fail} 个"
fi
log "----------------------------------------"
if [[ $fail -eq 0 ]]; then
log_ok "完成:全部 ${ok} 个成功"
else
log_warn "完成:成功 ${ok} 个,最终失败 ${fail} 个"
fi
log "========================================"
}
log "========================================"
log "[>>] frpc 监控启动 | 域名数:${#DOMAINS[@]} | VPS1:${VPS1_IP} | VPS2:${VPS2_IP} | 间隔:${CHECK_INTERVAL}s"
log "========================================"
load_state
while true; do
VPS1_STATUS=$(get_vps_status "$FRPC1_URL" "$FRPC1_CONTAINER")
VPS2_STATUS=$(get_vps_status "$FRPC2_URL" "$FRPC2_CONTAINER")
log "[--] VPS1:${VPS1_STATUS} VPS2:${VPS2_STATUS} 上次:${LAST_STATE}"
case "${VPS1_STATUS}|${VPS2_STATUS}" in
"OK|OK")
CURRENT_STATE="BOTH_OK"
if [[ "$LAST_STATE" == "VPS1_FAIL" || "$LAST_STATE" == "BOTH_FAIL" ]]; then
log_ok "主线 VPS1 恢复,切回主线"
switch_all "$VPS1_IP" "恢复主线"
else
log_ok "双线正常,DNS 保持主线 VPS1"
fi
;;
"VPS_DOWN|OK")
CURRENT_STATE="VPS1_FAIL"
if [[ "$LAST_STATE" != "VPS1_FAIL" ]]; then
log_warn "主线 VPS1 宕机,切换备线 VPS2"
switch_all "$VPS2_IP" "切换备线"
else
log_warn "主线 VPS1 仍宕机,DNS 保持备线 VPS2"
fi
;;
"OK|VPS_DOWN")
CURRENT_STATE="VPS2_FAIL"
if [[ "$LAST_STATE" != "VPS2_FAIL" ]]; then
log_info "备线 VPS2 宕机,确保 DNS 指向主线 VPS1"
switch_all "$VPS1_IP" "确保主线"
else
log_info "备线 VPS2 仍宕机,DNS 正常指向主线 VPS1"
fi
;;
"VPS_DOWN|VPS_DOWN")
CURRENT_STATE="BOTH_FAIL"
log_err "双线均宕机!DNS 保持不变,等待恢复"
;;
esac
save_state "$CURRENT_STATE"
LAST_STATE="$CURRENT_STATE"
sleep "$CHECK_INTERVAL"
done
SCRIPT_EOF
# ---------- 替换占位符 ----------
# 普通字符串占位符(sed -e 批量处理)
sed -i \
-e "s|CF_TOKEN_PLACEHOLDER|${CF_API_TOKEN}|g" \
-e "s|ZONE_ID_PLACEHOLDER|${CF_ZONE_ID}|g" \
-e "s|VPS1_IP_PLACEHOLDER|${VPS1_IP}|g" \
-e "s|VPS2_IP_PLACEHOLDER|${VPS2_IP}|g" \
-e "s|LOG_FILE_PLACEHOLDER|${LOG_FILE}|g" \
-e "s|STATE_FILE_PLACEHOLDER|${STATE_FILE}|g" \
-e "s|CON1_PLACEHOLDER|${FRPC1_CONTAINER}|g" \
-e "s|CON2_PLACEHOLDER|${FRPC2_CONTAINER}|g" \
"$MONITOR_SCRIPT"
# 数值变量(不带引号)
sed -i \
-e "s|FRPC1_DASH_PORT_PLACEHOLDER|${FRPC1_DASH_PORT}|g" \
-e "s|FRPC2_DASH_PORT_PLACEHOLDER|${FRPC2_DASH_PORT}|g" \
"$MONITOR_SCRIPT"
# URL 静态写入(含端口数字,用 sed 安全)
sed -i \
-e "s|FRPC1_URL_PLACEHOLDER|http://127.0.0.1:${FRPC1_DASH_PORT}/api/status|g" \
-e "s|FRPC2_URL_PLACEHOLDER|http://127.0.0.1:${FRPC2_DASH_PORT}/api/status|g" \
"$MONITOR_SCRIPT"
# 用户名、密码可能含特殊字符,用 perl \Q...\E 逐字面替换
perl -i -pe "s|USER_PLACEHOLDER|\Q${FRPC_USER}\E|g" "$MONITOR_SCRIPT"
perl -i -pe "s|PASS_PLACEHOLDER|\Q${FRPC_PASS}\E|g" "$MONITOR_SCRIPT"
# 域名数组通过环境变量传给 perl,彻底避免域名中特殊字符被解释
local DOMAIN_BLOCK
DOMAIN_BLOCK=$(printf ' "%s"\n' "${DOMAINS[@]}")
DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
my $block = $ENV{"DOMAIN_BLOCK"};
s/DOMAIN_ARRAY_PLACEHOLDER/$block/;
' "$MONITOR_SCRIPT"
chmod +x "$MONITOR_SCRIPT"
echo -e " ${GREEN}监控脚本生成完毕${NC}"
}
# -------- 4. 配置 systemd 和 logrotate --------
setup_services() {
echo -e "${GREEN}[5/5] 配置 Systemd 服务和 Logrotate...${NC}"
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=frpc Monitor with CF DNS Failover
After=network.target docker.service
Requires=docker.service
[Service]
Type=simple
ExecStart=/bin/bash ${MONITOR_SCRIPT}
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
cat > "$LOGROTATE_FILE" <<EOF
${LOG_FILE} {
daily
rotate 3
compress
delaycompress
missingok
notifempty
copytruncate
}
EOF
systemctl daemon-reload
systemctl enable frpc-monitor
systemctl restart frpc-monitor
sleep 2
local STATUS
STATUS=$(systemctl is-active frpc-monitor)
echo -e "\n${CYAN}========================================${NC}"
if [[ "$STATUS" == "active" ]]; then
echo -e "${GREEN}安装配置完成,服务运行正常!${NC}"
else
echo -e "${RED}服务启动异常,请选择「3. 查看实时日志」排查${NC}"
fi
echo -e "${CYAN}========================================${NC}"
echo -e "脚本位置 : ${MONITOR_SCRIPT}"
echo -e "服务状态 : ${STATUS}"
echo -e "实时日志 : tail -f ${LOG_FILE}"
echo ""
read -p "按回车键返回主菜单..."
}
# -------- 安装主流程 --------
install_script() {
install_deps
collect_config || { read -p "按回车键返回主菜单..."; return; }
generate_monitor_script
setup_services
}
# ================================================================
# -------- 修改配置 --------
# ================================================================
# 从监控脚本中读取带双引号字符串变量值(兼容值含任意特殊字符)
_cfg_get() {
local key=$1
KEY="$key" perl -ne '
if (/^\Q$ENV{KEY}\E="(.*)"$/) { print $1; exit }
' "$MONITOR_SCRIPT"
}
# 安全替换带双引号的字符串变量(perl 逐字面匹配,兼容特殊字符)
_cfg_set() {
local key=$1 val=$2 old_val
old_val=$(_cfg_get "$key")
OLD_VAL="$old_val" NEW_VAL="$val" KEY="$key" perl -i -0pe '
my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
' "$MONITOR_SCRIPT"
}
# 替换不带引号的数值变量;若为 Dashboard 端口则同步更新对应 URL
_cfg_set_num() {
local key=$1 val=$2
sed -i "s|^${key}=.*|${key}=${val}|" "$MONITOR_SCRIPT"
local url_key old_url new_url
case "$key" in
FRPC1_DASH_PORT) url_key="FRPC1_URL" ;;
FRPC2_DASH_PORT) url_key="FRPC2_URL" ;;
*) return ;;
esac
old_url=$(_cfg_get "$url_key")
new_url="http://127.0.0.1:${val}/api/status"
OLD_VAL="$old_url" NEW_VAL="$new_url" KEY="$url_key" perl -i -0pe '
my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
' "$MONITOR_SCRIPT"
}
# 修改后重启服务
_restart_service() {
echo -e "${GREEN}正在重启 frpc-monitor 服务以应用配置...${NC}"
systemctl restart frpc-monitor
sleep 2
local status
status=$(systemctl is-active frpc-monitor)
if [[ "$status" == "active" ]]; then
echo -e "${GREEN}服务已重启,配置生效${NC}"
else
echo -e "${RED}服务重启异常,请选择「3. 查看实时日志」排查${NC}"
fi
}
# 获取域名数组到全局变量 DOMAIN_LIST
_load_domains() {
DOMAIN_LIST=()
while IFS= read -r line; do
local d
d=$(echo "$line" | tr -d ' "')
[[ -n "$d" ]] && DOMAIN_LIST+=("$d")
done < <(awk '/^DOMAINS=\(/{found=1; next} found && /^\)/{exit} found{print}' "$MONITOR_SCRIPT")
}
# 将 DOMAIN_LIST 写回监控脚本
_save_domains() {
if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
echo -e "${RED}域名列表为空,不允许保存(至少保留一个域名)${NC}"
return 1
fi
local DOMAIN_BLOCK
DOMAIN_BLOCK=$(printf ' "%s"\n' "${DOMAIN_LIST[@]}")
DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
my $block = $ENV{"DOMAIN_BLOCK"};
s/DOMAINS=\(.*?\)/DOMAINS=(\n$block\n)/s;
' "$MONITOR_SCRIPT"
}
# 辅助:读取多个域名(支持空格分隔),追加到 DOMAIN_LIST
_read_domains_input() {
echo -e "${YELLOW}每行可输入一个或多个域名(空格分隔),空行回车结束:${NC}"
while true; do
read -p "域名: " domain_line
[[ -z "$domain_line" ]] && break
for d in $domain_line; do
DOMAIN_LIST+=("$d")
echo -e " ${GREEN}已添加: ${d}${NC}"
done
done
}
# 辅助:读取并更新字符串配置项,不允许为空,自动重启服务
_update_cfg() {
local key=$1 prompt=$2 v
read -p "$prompt" v
if [[ -z "$v" ]]; then
echo -e "${RED}输入不能为空,已取消${NC}"
read -p "按回车键继续..."; return 1
fi
_cfg_set "$key" "$v"
echo -e "${GREEN}已更新${NC}"
_restart_service
read -p "按回车键继续..."
}
# 辅助:读取并更新数值配置项,自动重启服务
# 用法: _update_num KEY "提示语" min [max](max=0 表示不限上界)
_update_num() {
local key=$1 prompt=$2 min=${3:-1} max=${4:-0} v valid=false
read -p "$prompt" v
if [[ "$v" =~ ^[0-9]+$ ]]; then
if (( max > 0 )); then
(( v >= min && v <= max )) && valid=true
else
(( v >= min )) && valid=true
fi
fi
if [[ "$valid" == true ]]; then
_cfg_set_num "$key" "$v"
echo -e "${GREEN}已更新${NC}"
_restart_service
else
(( max > 0 )) \
&& echo -e "${RED}请输入 ${min}~${max} 的整数${NC}" \
|| echo -e "${RED}请输入不小于 ${min} 的整数${NC}"
fi
read -p "按回车键继续..."
}
# -------- 子菜单:域名管理 --------
modify_domains() {
while true; do
clear
_load_domains
echo -e "${CYAN}==============================${NC}"
echo -e "${CYAN} 域名管理${NC}"
echo -e "${CYAN}==============================${NC}"
local i=1
for d in "${DOMAIN_LIST[@]}"; do echo " ${i}. ${d}"; ((i++)); done
echo ""
echo " a. 添加域名"
echo " d. 删除域名"
echo " 0. 返回上级菜单"
echo -e "${CYAN}==============================${NC}"
read -p "请输入操作: " op
case $op in
a|A)
local before=${#DOMAIN_LIST[@]}
_read_domains_input
if [[ ${#DOMAIN_LIST[@]} -gt $before ]]; then
_save_domains && _restart_service
else
echo -e "${YELLOW}未输入任何域名,已取消${NC}"
fi
read -p "按回车键继续..."
;;
d|D)
if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
echo -e "${RED}当前没有域名可删除${NC}"
read -p "按回车键继续..."; continue
fi
if [[ ${#DOMAIN_LIST[@]} -eq 1 ]]; then
echo -e "${RED}至少需要保留一个域名,无法删除最后一项${NC}"
read -p "按回车键继续..."; continue
fi
local idx
read -p "请输入要删除的域名序号 (1-${#DOMAIN_LIST[@]}): " idx
if ! [[ "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > ${#DOMAIN_LIST[@]} )); then
echo -e "${RED}无效序号${NC}"; sleep 2; continue
fi
local removed="${DOMAIN_LIST[$((idx-1))]}"
DOMAIN_LIST=("${DOMAIN_LIST[@]:0:$((idx-1))}" "${DOMAIN_LIST[@]:$idx}")
_save_domains \
&& echo -e "${GREEN}已删除: ${removed}${NC}" \
&& _restart_service
read -p "按回车键继续..."
;;
0) break ;;
*) echo -e "${RED}无效操作${NC}"; sleep 1 ;;
esac
done
}
# -------- 子菜单:运行参数 --------
modify_runtime_params() {
while true; do
clear
local cur_interval cur_ttl cur_proxied cur_rwait
cur_interval=$(grep -m1 "^CHECK_INTERVAL=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_ttl=$(grep -m1 "^DNS_TTL=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_proxied=$(grep -m1 "^CF_PROXIED=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_rwait=$(grep -m1 "^RESTART_WAIT=" "$MONITOR_SCRIPT" | cut -d= -f2)
echo -e "${CYAN}==============================${NC}"
echo -e "${CYAN} 运行参数设置${NC}"
echo -e "${CYAN}==============================${NC}"
echo " 1. 检测间隔 (CHECK_INTERVAL) : ${cur_interval}s"
echo " 2. DNS TTL (DNS_TTL) : ${cur_ttl}s"
echo " 3. CF 代理 (CF_PROXIED) : ${cur_proxied}"
echo " 4. 重启等待 (RESTART_WAIT) : ${cur_rwait}s"
echo " 0. 返回上级菜单"
echo -e "${CYAN}==============================${NC}"
read -p "请选择参数编号: " p
case $p in
1) _update_num "CHECK_INTERVAL" "新的检测间隔(秒,当前 ${cur_interval},建议 ≥10): " 10 ;;
2) _update_num "DNS_TTL" "新的 DNS TTL(秒,当前 ${cur_ttl}): " 1 ;;
3)
read -p "CF_PROXIED (true/false,当前 ${cur_proxied}): " v
if [[ "$v" == "true" || "$v" == "false" ]]; then
_cfg_set_num "CF_PROXIED" "$v"
echo -e "${GREEN}已更新${NC}"; _restart_service
else
echo -e "${RED}只能输入 true 或 false${NC}"
fi
read -p "按回车键继续..." ;;
4) _update_num "RESTART_WAIT" "新的重启等待时间(秒,当前 ${cur_rwait}): " 1 ;;
0) break ;;
*) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
esac
done
}
# -------- 子菜单:单项配置 --------
modify_single_item() {
while true; do
clear
local cur_token cur_zone cur_vps1 cur_vps2 cur_con1 cur_con2
local cur_dash1 cur_dash2 cur_user cur_pass_raw cur_pass stars
cur_token=$(_cfg_get "CF_API_TOKEN")
cur_zone=$(_cfg_get "CF_ZONE_ID")
cur_vps1=$(_cfg_get "VPS1_IP")
cur_vps2=$(_cfg_get "VPS2_IP")
cur_con1=$(_cfg_get "FRPC1_CONTAINER")
cur_con2=$(_cfg_get "FRPC2_CONTAINER")
cur_user=$(_cfg_get "FRPC_USER")
cur_dash1=$(grep -m1 "^FRPC1_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_dash2=$(grep -m1 "^FRPC2_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_pass_raw=$(_cfg_get "FRPC_PASS")
stars="${cur_pass_raw//?/*}"
if [[ -z "$cur_pass_raw" ]]; then
cur_pass="(未设置)"
else
cur_pass="${cur_pass_raw:0:2}${stars:2}"
fi
echo -e "${CYAN}==============================${NC}"
echo -e "${CYAN} 单项配置修改${NC}"
echo -e "${CYAN}==============================${NC}"
echo " 1. CF API Token : ${cur_token:0:8}…(已隐藏)"
echo " 2. CF Zone ID : ${cur_zone}"
echo " 3. 主线 VPS1 IP : ${cur_vps1}"
echo " 4. 备线 VPS2 IP : ${cur_vps2}"
echo " 5. frpc01 容器名 : ${cur_con1}"
echo " 6. frpc02 容器名 : ${cur_con2}"
echo " 7. frpc01 Dashboard 端口 : ${cur_dash1}"
echo " 8. frpc02 Dashboard 端口 : ${cur_dash2}"
echo " 9. Dashboard 用户名 : ${cur_user}"
echo " 10. Dashboard 密码 : ${cur_pass}"
echo " 0. 返回上级菜单"
echo -e "${CYAN}==============================${NC}"
read -p "请选择项目编号: " item
case $item in
1) _update_cfg "CF_API_TOKEN" "新的 CF API Token: " ;;
2) _update_cfg "CF_ZONE_ID" "新的 CF Zone ID: " ;;
3) _update_cfg "VPS1_IP" "新的 VPS1 IP: " ;;
4) _update_cfg "VPS2_IP" "新的 VPS2 IP: " ;;
5) _update_cfg "FRPC1_CONTAINER" "新的 frpc01 容器名: " ;;
6) _update_cfg "FRPC2_CONTAINER" "新的 frpc02 容器名: " ;;
7) _update_num "FRPC1_DASH_PORT" "新的 frpc01 Dashboard 端口(当前 ${cur_dash1}): " 1 65535 ;;
8) _update_num "FRPC2_DASH_PORT" "新的 frpc02 Dashboard 端口(当前 ${cur_dash2}): " 1 65535 ;;
9) _update_cfg "FRPC_USER" "新的 Dashboard 用户名: " ;;
10) _update_cfg "FRPC_PASS" "新的 Dashboard 密码: " ;;
0) break ;;
*) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
esac
done
}
# -------- 修改配置入口 --------
modify_config() {
if [[ ! -f "$MONITOR_SCRIPT" ]]; then
echo -e "${RED}未找到监控脚本 ${MONITOR_SCRIPT},请先执行「1. 安装/配置监控脚本」${NC}"
read -p "按回车键返回主菜单..."; return
fi
while true; do
clear
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} 修改配置${NC}"
echo -e "${CYAN}========================================${NC}"
echo " 1. 修改单项配置 (Token / IP / 容器名 / 端口 / 密码等)"
echo " 2. 管理域名列表 (添加 / 删除域名)"
echo " 3. 修改运行参数 (检测间隔 / TTL / 代理模式等)"
echo " 4. 重新向导配置 (覆盖所有配置,重新完整填写)"
echo " 0. 返回主菜单"
echo -e "${CYAN}========================================${NC}"
read -p "请输入选项编号: " sub
case $sub in
1) modify_single_item ;;
2) modify_domains ;;
3) modify_runtime_params ;;
4)
echo -e "${YELLOW}此操作将覆盖当前所有配置,重新引导填写!${NC}"
read -p "确认继续?(y/n): " confirm
if [[ "$confirm" == "y" ]]; then
if collect_config; then
generate_monitor_script
_restart_service
echo -e "${GREEN}配置已全部更新${NC}"
fi
read -p "按回车键继续..."
fi
;;
0) break ;;
*) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
esac
done
}
# ================================================================
# -------- 查看日志 --------
# ================================================================
view_logs() {
if [[ ! -f "$LOG_FILE" ]]; then
echo -e "${RED}日志文件不存在,服务可能尚未启动${NC}"
read -p "按回车键返回主菜单..."; return
fi
echo -e "${GREEN}实时日志输出中,按 Ctrl+C 退出查看...${NC}"
echo ""
# 使用 sed -u 实时替换英文状态码为中文
# UNKNOWN -> 未知
# BOTH_OK -> 双线正常
# VPS1_FAIL -> 主线故障
# VPS2_FAIL -> 备线故障
# BOTH_FAIL -> 双线故障
# OK -> 正常
# VPS_DOWN -> 连接中断
tail -f "$LOG_FILE" | sed -u \
-e 's/\[OK\]/[√]/g' \
-e 's/\[!!\]/[!]/g' \
-e 's/\[XX\]/[✗]/g' \
-e 's/\[--\]/[-]/g' \
-e 's/\[>>\]/[>]/g' \
-e 's/\[\.\.\]/[.]/g' \
-e 's/UNKNOWN/未知/g' \
-e 's/BOTH_OK/双线正常/g' \
-e 's/VPS1_FAIL/主线故障/g' \
-e 's/VPS2_FAIL/备线故障/g' \
-e 's/BOTH_FAIL/双线故障/g' \
-e 's/\bOK\b/正常/g' \
-e 's/VPS_DOWN/连接中断/g'
}
# ================================================================
# -------- 卸载 --------
# ================================================================
uninstall_script() {
echo -e "${YELLOW}此操作将停止服务并删除所有相关文件(包括本脚本)!${NC}"
echo ""
read -p "确认卸载?(y/n): " confirm
[[ "$confirm" != "y" ]] && return
echo -e "${RED}正在卸载...${NC}"
systemctl stop frpc-monitor 2>/dev/null
systemctl disable frpc-monitor 2>/dev/null
rm -f "$SERVICE_FILE"
systemctl daemon-reload
systemctl reset-failed 2>/dev/null
rm -f "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "${LOG_FILE}"* "$STATE_FILE" "$LOGROTATE_FILE"
echo -e "\n${CYAN}--- 验证清理结果 ---${NC}"
for f in "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "$SERVICE_FILE" "$LOG_FILE" "$LOGROTATE_FILE" "$STATE_FILE"; do
if [[ ! -e "$f" ]]; then
echo -e " ${GREEN}$f 已删除${NC}"
else
echo -e " ${RED}$f 仍存在${NC}"
fi
done
echo ""
echo -e " 服务状态: $(systemctl is-active frpc-monitor 2>&1)"
echo ""
echo -e "${GREEN}卸载完成${NC}"
echo -e "${YELLOW}本脚本将在退出后自动删除:$0${NC}"
echo ""
read -p "按回车键退出并完成清理..."
# 后台延迟删除脚本自身,留出主进程退出时间
(sleep 1 && rm -f "$0") &
exit 0
}
# ================================================================
# 主循环
# ================================================================
while true; do
print_menu
read -p "请输入选项编号: " choice
case $choice in
1) install_script ;;
2) modify_config ;;
3) view_logs ;;
4) uninstall_script ;;
0) echo -e "退出"; exit 0 ;;
*) echo -e "${RED}无效选项${NC}"; sleep 2 ;;
esac
done
#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换 - 管理脚本 v1.1
# =============================================================
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
# 路径定义
INSTALL_DEPS_SCRIPT="/opt/install-deps.sh"
MONITOR_SCRIPT="/opt/frpc-monitor.sh"
SERVICE_FILE="/etc/systemd/system/frpc-monitor.service"
LOGROTATE_FILE="/etc/logrotate.d/frpc-monitor"
LOG_FILE="/var/log/frpc-monitor.log"
STATE_FILE="/tmp/frpc_monitor_state"
# 检查权限
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}请使用 root 用户或 sudo 运行此脚本${NC}"
exit 1
fi
# ================================================================
# 主菜单
# ================================================================
print_menu() {
clear
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} frpc 双线监控 + CF DNS 自动切换管理器${NC}"
echo -e "${CYAN}========================================${NC}"
echo " 1. 安装/配置监控脚本 (首次安装)"
echo -e " ${YELLOW}2. 修改配置${NC}"
echo " 3. 查看实时日志"
echo " 4. 卸载并清理所有文件"
echo -e " ${YELLOW}0. 退出${NC}"
echo -e "${CYAN}========================================${NC}"
}
# ================================================================
# -------- 安装流程 --------
# ================================================================
# -------- 1. 安装依赖(curl / jq / docker)--------
install_deps() {
echo -e "${GREEN}[1/5] 检查并安装系统依赖 (curl, jq, docker)...${NC}"
cat <<'DEPS_EOF' > "$INSTALL_DEPS_SCRIPT"
#!/bin/bash
if command -v apt-get &>/dev/null; then
PKG_MANAGER="apt-get"; UPDATE_CMD="apt-get update -y"; INSTALL_CMD="apt-get install -y"
elif command -v dnf &>/dev/null; then
PKG_MANAGER="dnf"; UPDATE_CMD="dnf makecache -y"; INSTALL_CMD="dnf install -y"
elif command -v yum &>/dev/null; then
PKG_MANAGER="yum"; UPDATE_CMD="yum makecache -y"; INSTALL_CMD="yum install -y"
else
echo "未识别的包管理器,请手动安装:curl、jq"; exit 1
fi
$UPDATE_CMD &>/dev/null
install_pkg() {
local name=$1 cmd=$2
if command -v "$cmd" &>/dev/null; then
echo " ${name} 已安装"
else
echo " 安装 ${name}..."
$INSTALL_CMD "$name" &>/dev/null \
&& echo " ${name} 安装成功" \
|| { echo " ${name} 安装失败"; exit 1; }
fi
}
install_pkg "curl" "curl"
install_pkg "jq" "jq"
install_pkg "docker" "docker"
DEPS_EOF
chmod +x "$INSTALL_DEPS_SCRIPT"
bash "$INSTALL_DEPS_SCRIPT"
}
# -------- 辅助:读取必填字符串,不允许为空 --------
# 用法: _read_required VAR "提示语"
_read_required() {
local _var=$1 _prompt=$2 _val
while true; do
read -p "$_prompt" _val
if [[ -n "$_val" ]]; then
printf -v "$_var" '%s' "$_val"
return
fi
echo -e " ${RED}不能为空,请重新输入${NC}"
done
}
# -------- 辅助:读取端口(1-65535),允许回车使用默认值 --------
# 用法: _read_port VAR "提示语" 默认值
_read_port() {
local _var=$1 _prompt=$2 _default=$3 _val
while true; do
read -p "$_prompt" _val
_val=${_val:-$_default}
if [[ "$_val" =~ ^[0-9]+$ ]] && (( _val >= 1 && _val <= 65535 )); then
printf -v "$_var" '%s' "$_val"
return
fi
echo -e " ${RED}请输入 1~65535 的整数${NC}"
done
}
# -------- 2. 交互式采集配置 --------
# 返回值:0=成功,1=用户取消(未输入域名)
collect_config() {
echo -e "\n${GREEN}[2/5] 检测 frpc 容器...${NC}"
mapfile -t FRPC_CONTAINERS < <(docker ps --format "{{.Names}}" 2>/dev/null | grep frpc | sort)
if [[ ${#FRPC_CONTAINERS[@]} -eq 0 ]]; then
echo -e "${YELLOW}未检测到运行中的 frpc 容器,请确保 Docker 已启动且容器名包含 frpc${NC}"
else
echo -e "${GREEN}检测到以下 frpc 容器:${NC}"
for c in "${FRPC_CONTAINERS[@]}"; do echo " - $c"; done
fi
echo -e "\n${CYAN}[3/5] 请输入配置信息:${NC}"
_read_required CF_API_TOKEN "Cloudflare API Token: "
_read_required CF_ZONE_ID "Cloudflare Zone ID: "
_read_required VPS1_IP "主线 VPS1 IP: "
_read_required VPS2_IP "备线 VPS2 IP: "
echo -e "\n${CYAN}--- frpc 容器名 ---${NC}"
if [[ ${#FRPC_CONTAINERS[@]} -ge 2 ]]; then
read -p "frpc01 容器名 (默认 ${FRPC_CONTAINERS[0]}): " C1
FRPC1_CONTAINER=${C1:-${FRPC_CONTAINERS[0]}}
read -p "frpc02 容器名 (默认 ${FRPC_CONTAINERS[1]}): " C2
FRPC2_CONTAINER=${C2:-${FRPC_CONTAINERS[1]}}
else
echo -e "${YELLOW}未自动检测到足够容器,请手动输入${NC}"
_read_required FRPC1_CONTAINER "frpc01 容器名: "
_read_required FRPC2_CONTAINER "frpc02 容器名: "
fi
echo -e "\n${CYAN}--- frpc Dashboard 端口 ---${NC}"
_read_port FRPC1_DASH_PORT "frpc01 Dashboard 端口 (默认 7401): " 7401
_read_port FRPC2_DASH_PORT "frpc02 Dashboard 端口 (默认 7402): " 7402
echo -e "\n${CYAN}--- frpc Dashboard 认证 ---${NC}"
_read_required FRPC_USER "Dashboard 用户名: "
_read_required FRPC_PASS "Dashboard 密码: "
echo -e "\n${CYAN}--- 需要切换的域名 ---${NC}"
DOMAIN_LIST=()
_read_domains_input
DOMAINS=("${DOMAIN_LIST[@]}")
if [[ ${#DOMAINS[@]} -eq 0 ]]; then
echo -e "${RED}未输入任何域名,操作取消${NC}"
return 1
fi
echo -e "\n${GREEN}已录入 ${#DOMAINS[@]} 个域名:${NC}"
for d in "${DOMAINS[@]}"; do echo " - $d"; done
return 0
}
# -------- 3. 生成主监控脚本 --------
generate_monitor_script() {
echo -e "\n${GREEN}[4/5] 生成监控脚本...${NC}"
# heredoc 使用单引号定界符,防止变量展开。
# FRPC1_URL / FRPC2_URL 使用占位符,由后续 sed 替换为静态字符串,
# 避免 heredoc 内 ${FRPC1_DASH_PORT} 因未赋值而展开为空。
cat <<'SCRIPT_EOF' > "$MONITOR_SCRIPT"
#!/bin/bash
# =============================================================
# frpc 双线监控 + Cloudflare DNS 自动切换脚本
# 依赖:curl / jq / docker
# =============================================================
CF_API_TOKEN="CF_TOKEN_PLACEHOLDER"
CF_ZONE_ID="ZONE_ID_PLACEHOLDER"
VPS1_IP="VPS1_IP_PLACEHOLDER"
VPS2_IP="VPS2_IP_PLACEHOLDER"
DOMAINS=(
DOMAIN_ARRAY_PLACEHOLDER
)
FRPC1_DASH_PORT=FRPC1_DASH_PORT_PLACEHOLDER
FRPC2_DASH_PORT=FRPC2_DASH_PORT_PLACEHOLDER
FRPC1_URL="FRPC1_URL_PLACEHOLDER"
FRPC2_URL="FRPC2_URL_PLACEHOLDER"
FRPC_USER="USER_PLACEHOLDER"
FRPC_PASS="PASS_PLACEHOLDER"
FRPC1_CONTAINER="CON1_PLACEHOLDER"
FRPC2_CONTAINER="CON2_PLACEHOLDER"
DNS_TTL=60
CHECK_INTERVAL=30
RESTART_WAIT=15
CF_PROXIED=false
LOG_FILE="LOG_FILE_PLACEHOLDER"
STATE_FILE="STATE_FILE_PLACEHOLDER"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" >&2; }
log_ok() { log "[OK] $1"; }
log_warn() { log "[!!] $1"; }
log_err() { log "[XX] $1"; }
log_info() { log "[--] $1"; }
log_status() { log "[ST] $1"; }
load_state() { [[ -f "$STATE_FILE" ]] && source "$STATE_FILE" || LAST_STATE="UNKNOWN"; }
save_state() { echo "LAST_STATE=\"$1\"" > "$STATE_FILE"; }
check_dashboard() {
local code
code=$(curl -s -o /dev/null -w "%{http_code}" \
--connect-timeout 5 --max-time 8 \
-u "${FRPC_USER}:${FRPC_PASS}" "$1" 2>/dev/null)
[[ "$code" == "200" ]]
}
restart_frpc() {
local container=$1
log_info " 重启容器: ${container}"
if docker restart "$container" &>/dev/null; then
log_ok " ${container} 重启成功,等待 ${RESTART_WAIT}s"
sleep "$RESTART_WAIT"
return 0
else
log_err " ${container} 重启失败"
return 1
fi
}
get_vps_status() {
local dashboard_url=$1 container=$2
if ! check_dashboard "$dashboard_url"; then
log_warn "${container}: Dashboard 无响应,尝试重启"
if restart_frpc "$container" && check_dashboard "$dashboard_url"; then
log_ok "${container} 重启后 Dashboard 恢复"
else
log_err "${container} 重启后仍无响应,按宕机处理"
echo "VPS_DOWN"; return
fi
fi
local result
result=$(curl -s --connect-timeout 5 --max-time 8 \
-u "${FRPC_USER}:${FRPC_PASS}" "$dashboard_url" 2>/dev/null)
if echo "$result" | jq -e '.. | objects | select(.status == "running")' &>/dev/null; then
echo "OK"
else
log_err "${container}: Dashboard 正常但隧道全部离线,按宕机处理"
echo "VPS_DOWN"
fi
}
get_record_info() {
local domain=$1 response
response=$(curl -s -X GET \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${domain}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json")
local record_id current_ip
record_id=$(echo "$response" | jq -r '.result[0].id // ""')
current_ip=$(echo "$response" | jq -r '.result[0].content // ""')
echo "${record_id}|${current_ip}"
}
update_record() {
local domain=$1 target_ip=$2
[[ -z "$domain" ]] && return 2
local info record_id current_ip
local max_retries=3 attempt delay
# ---------- 第一步:获取记录 ID(含重试)----------
for (( attempt=1; attempt<=max_retries; attempt++ )); do
info=$(get_record_info "$domain")
record_id=$(echo "$info" | cut -d'|' -f1)
current_ip=$(echo "$info" | cut -d'|' -f2)
[[ -n "$record_id" ]] && break
delay=$(( attempt * 2 ))
log_warn " | ${domain} -> 获取记录失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
sleep "$delay"
done
if [[ -z "$record_id" ]]; then
log_err " | ${domain} -> 获取记录失败,已重试 ${max_retries} 次,放弃"
return 1
fi
if [[ "$current_ip" == "$target_ip" ]]; then
log_info " | ${domain} -> 已是 ${target_ip},无需更新"
return 0
fi
# ---------- 第二步:更新记录(含重试)----------
local response success
for (( attempt=1; attempt<=max_retries; attempt++ )); do
response=$(curl -s -X PUT \
"https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${record_id}" \
-H "Authorization: Bearer ${CF_API_TOKEN}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"${domain}\",\"content\":\"${target_ip}\",\"ttl\":${DNS_TTL},\"proxied\":${CF_PROXIED}}")
success=$(echo "$response" | jq -r '.success')
if [[ "$success" == "true" ]]; then
log_ok " | ${domain} -> ${current_ip} => ${target_ip}"
return 0
fi
delay=$(( attempt * 2 ))
log_warn " | ${domain} -> 更新失败 (第${attempt}/${max_retries}次),${delay}s 后重试"
sleep "$delay"
done
log_err " | ${domain} -> 更新失败,已重试 ${max_retries} 次,放弃: $(echo "$response" | jq -r '.errors[0].message // "未知错误"')"
return 1
}
switch_all() {
local target_ip=$1 label=$2 ok=0 fail=0
local failed_domains=()
log "========================================"
log "[>>] 批量切换 -> ${target_ip} (${label})"
log "----------------------------------------"
for domain in "${DOMAINS[@]}"; do
update_record "$domain" "$target_ip"
case $? in
0) ((ok++)) ;;
1) ((fail++)); failed_domains+=("$domain") ;;
esac
sleep 0.5
done
# ---------- 对失败域名做一轮补偿重试 ----------
if [[ ${#failed_domains[@]} -gt 0 ]]; then
log "----------------------------------------"
log_warn "对 ${#failed_domains[@]} 个失败域名进行补偿重试 (等待 5s)..."
sleep 5
local retry_ok=0 retry_fail=0
for domain in "${failed_domains[@]}"; do
update_record "$domain" "$target_ip"
case $? in
0) ((retry_ok++)); ((ok++)); ((fail--)) ;;
1) ((retry_fail++)) ;;
esac
sleep 1
done
log_info "补偿结果: 补救成功 ${retry_ok} 个,仍失败 ${retry_fail} 个"
fi
log "----------------------------------------"
if [[ $fail -eq 0 ]]; then
log_ok "完成:全部 ${ok} 个成功"
else
log_warn "完成:成功 ${ok} 个,最终失败 ${fail} 个"
fi
log "========================================"
}
log "========================================"
log "[>>] frpc 监控启动 | 域名数:${#DOMAINS[@]} | VPS1:${VPS1_IP} | VPS2:${VPS2_IP} | 间隔:${CHECK_INTERVAL}s"
log "========================================"
load_state
while true; do
VPS1_STATUS=$(get_vps_status "$FRPC1_URL" "$FRPC1_CONTAINER")
VPS2_STATUS=$(get_vps_status "$FRPC2_URL" "$FRPC2_CONTAINER")
log_status "VPS1:${VPS1_STATUS} VPS2:${VPS2_STATUS} 上次:${LAST_STATE}"
case "${VPS1_STATUS}|${VPS2_STATUS}" in
"OK|OK")
CURRENT_STATE="BOTH_OK"
if [[ "$LAST_STATE" == "VPS1_FAIL" || "$LAST_STATE" == "BOTH_FAIL" ]]; then
log_ok "主线 VPS1 恢复,切回主线"
switch_all "$VPS1_IP" "恢复主线"
else
log_ok "双线正常,DNS 保持主线 VPS1"
fi
;;
"VPS_DOWN|OK")
CURRENT_STATE="VPS1_FAIL"
if [[ "$LAST_STATE" != "VPS1_FAIL" ]]; then
log_warn "主线 VPS1 宕机,切换备线 VPS2"
switch_all "$VPS2_IP" "切换备线"
else
log_warn "主线 VPS1 仍宕机,DNS 保持备线 VPS2"
fi
;;
"OK|VPS_DOWN")
CURRENT_STATE="VPS2_FAIL"
if [[ "$LAST_STATE" != "VPS2_FAIL" ]]; then
log_info "备线 VPS2 宕机,确保 DNS 指向主线 VPS1"
switch_all "$VPS1_IP" "确保主线"
else
log_info "备线 VPS2 仍宕机,DNS 正常指向主线 VPS1"
fi
;;
"VPS_DOWN|VPS_DOWN")
CURRENT_STATE="BOTH_FAIL"
log_err "双线均宕机!DNS 保持不变,等待恢复"
;;
esac
save_state "$CURRENT_STATE"
LAST_STATE="$CURRENT_STATE"
sleep "$CHECK_INTERVAL"
done
SCRIPT_EOF
# ---------- 替换占位符 ----------
# 普通字符串占位符(sed -e 批量处理)
sed -i \
-e "s|CF_TOKEN_PLACEHOLDER|${CF_API_TOKEN}|g" \
-e "s|ZONE_ID_PLACEHOLDER|${CF_ZONE_ID}|g" \
-e "s|VPS1_IP_PLACEHOLDER|${VPS1_IP}|g" \
-e "s|VPS2_IP_PLACEHOLDER|${VPS2_IP}|g" \
-e "s|LOG_FILE_PLACEHOLDER|${LOG_FILE}|g" \
-e "s|STATE_FILE_PLACEHOLDER|${STATE_FILE}|g" \
-e "s|CON1_PLACEHOLDER|${FRPC1_CONTAINER}|g" \
-e "s|CON2_PLACEHOLDER|${FRPC2_CONTAINER}|g" \
"$MONITOR_SCRIPT"
# 数值变量(不带引号)
sed -i \
-e "s|FRPC1_DASH_PORT_PLACEHOLDER|${FRPC1_DASH_PORT}|g" \
-e "s|FRPC2_DASH_PORT_PLACEHOLDER|${FRPC2_DASH_PORT}|g" \
"$MONITOR_SCRIPT"
# URL 静态写入(含端口数字,用 sed 安全)
sed -i \
-e "s|FRPC1_URL_PLACEHOLDER|http://127.0.0.1:${FRPC1_DASH_PORT}/api/status|g" \
-e "s|FRPC2_URL_PLACEHOLDER|http://127.0.0.1:${FRPC2_DASH_PORT}/api/status|g" \
"$MONITOR_SCRIPT"
# 用户名、密码可能含特殊字符,用 perl \Q...\E 逐字面替换
perl -i -pe "s|USER_PLACEHOLDER|\Q${FRPC_USER}\E|g" "$MONITOR_SCRIPT"
perl -i -pe "s|PASS_PLACEHOLDER|\Q${FRPC_PASS}\E|g" "$MONITOR_SCRIPT"
# 域名数组通过环境变量传给 perl,彻底避免域名中特殊字符被解释
local DOMAIN_BLOCK
DOMAIN_BLOCK=$(printf ' "%s"\n' "${DOMAINS[@]}")
DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
my $block = $ENV{"DOMAIN_BLOCK"};
s/DOMAIN_ARRAY_PLACEHOLDER/$block/;
' "$MONITOR_SCRIPT"
chmod +x "$MONITOR_SCRIPT"
echo -e " ${GREEN}监控脚本生成完毕${NC}"
}
# -------- 4. 配置 systemd 和 logrotate --------
setup_services() {
echo -e "${GREEN}[5/5] 配置 Systemd 服务和 Logrotate...${NC}"
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=frpc Monitor with CF DNS Failover
After=network.target docker.service
Requires=docker.service
[Service]
Type=simple
ExecStart=/bin/bash ${MONITOR_SCRIPT}
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
cat > "$LOGROTATE_FILE" <<EOF
${LOG_FILE} {
daily
rotate 3
compress
delaycompress
missingok
notifempty
copytruncate
}
EOF
systemctl daemon-reload
systemctl enable frpc-monitor
systemctl restart frpc-monitor
sleep 2
local STATUS
STATUS=$(systemctl is-active frpc-monitor)
echo -e "\n${CYAN}========================================${NC}"
if [[ "$STATUS" == "active" ]]; then
echo -e "${GREEN}安装配置完成,服务运行正常!${NC}"
else
echo -e "${RED}服务启动异常,请选择「3. 查看实时日志」排查${NC}"
fi
echo -e "${CYAN}========================================${NC}"
echo -e "脚本位置 : ${MONITOR_SCRIPT}"
echo -e "服务状态 : ${STATUS}"
echo -e "实时日志 : tail -f ${LOG_FILE}"
echo ""
read -p "按回车键返回主菜单..."
}
# -------- 安装主流程 --------
install_script() {
install_deps
collect_config || { read -p "按回车键返回主菜单..."; return; }
generate_monitor_script
setup_services
}
# ================================================================
# -------- 修改配置 --------
# ================================================================
# 从监控脚本中读取带双引号字符串变量值(兼容值含任意特殊字符)
_cfg_get() {
local key=$1
KEY="$key" perl -ne '
if (/^\Q$ENV{KEY}\E="(.*)"$/) { print $1; exit }
' "$MONITOR_SCRIPT"
}
# 安全替换带双引号的字符串变量(perl 逐字面匹配,兼容特殊字符)
_cfg_set() {
local key=$1 val=$2 old_val
old_val=$(_cfg_get "$key")
OLD_VAL="$old_val" NEW_VAL="$val" KEY="$key" perl -i -0pe '
my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
' "$MONITOR_SCRIPT"
}
# 替换不带引号的数值变量;若为 Dashboard 端口则同步更新对应 URL
_cfg_set_num() {
local key=$1 val=$2
sed -i "s|^${key}=.*|${key}=${val}|" "$MONITOR_SCRIPT"
local url_key old_url new_url
case "$key" in
FRPC1_DASH_PORT) url_key="FRPC1_URL" ;;
FRPC2_DASH_PORT) url_key="FRPC2_URL" ;;
*) return ;;
esac
old_url=$(_cfg_get "$url_key")
new_url="http://127.0.0.1:${val}/api/status"
OLD_VAL="$old_url" NEW_VAL="$new_url" KEY="$url_key" perl -i -0pe '
my ($k, $o, $n) = ($ENV{KEY}, $ENV{OLD_VAL}, $ENV{NEW_VAL});
s/^\Q$k\E="\Q$o\E"/$k="$n"/m;
' "$MONITOR_SCRIPT"
}
# 修改后重启服务
_restart_service() {
echo -e "${GREEN}正在重启 frpc-monitor 服务以应用配置...${NC}"
systemctl restart frpc-monitor
sleep 2
local status
status=$(systemctl is-active frpc-monitor)
if [[ "$status" == "active" ]]; then
echo -e "${GREEN}服务已重启,配置生效${NC}"
else
echo -e "${RED}服务重启异常,请选择「3. 查看实时日志」排查${NC}"
fi
}
# 获取域名数组到全局变量 DOMAIN_LIST
_load_domains() {
DOMAIN_LIST=()
while IFS= read -r line; do
local d
d=$(echo "$line" | tr -d ' "')
[[ -n "$d" ]] && DOMAIN_LIST+=("$d")
done < <(awk '/^DOMAINS=\(/{found=1; next} found && /^\)/{exit} found{print}' "$MONITOR_SCRIPT")
}
# 将 DOMAIN_LIST 写回监控脚本
_save_domains() {
if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
echo -e "${RED}域名列表为空,不允许保存(至少保留一个域名)${NC}"
return 1
fi
local DOMAIN_BLOCK
DOMAIN_BLOCK=$(printf ' "%s"\n' "${DOMAIN_LIST[@]}")
DOMAIN_BLOCK="$DOMAIN_BLOCK" perl -i -0pe '
my $block = $ENV{"DOMAIN_BLOCK"};
s/DOMAINS=\(.*?\)/DOMAINS=(\n$block\n)/s;
' "$MONITOR_SCRIPT"
}
# 辅助:读取多个域名(支持空格分隔),追加到 DOMAIN_LIST
_read_domains_input() {
echo -e "${YELLOW}每行可输入一个或多个域名(空格分隔),空行回车结束:${NC}"
while true; do
read -p "域名: " domain_line
[[ -z "$domain_line" ]] && break
for d in $domain_line; do
DOMAIN_LIST+=("$d")
echo -e " ${GREEN}已添加: ${d}${NC}"
done
done
}
# 辅助:读取并更新字符串配置项,不允许为空,自动重启服务
_update_cfg() {
local key=$1 prompt=$2 v
read -p "$prompt" v
if [[ -z "$v" ]]; then
echo -e "${RED}输入不能为空,已取消${NC}"
read -p "按回车键继续..."; return 1
fi
_cfg_set "$key" "$v"
echo -e "${GREEN}已更新${NC}"
_restart_service
read -p "按回车键继续..."
}
# 辅助:读取并更新数值配置项,自动重启服务
# 用法: _update_num KEY "提示语" min [max](max=0 表示不限上界)
_update_num() {
local key=$1 prompt=$2 min=${3:-1} max=${4:-0} v valid=false
read -p "$prompt" v
if [[ "$v" =~ ^[0-9]+$ ]]; then
if (( max > 0 )); then
(( v >= min && v <= max )) && valid=true
else
(( v >= min )) && valid=true
fi
fi
if [[ "$valid" == true ]]; then
_cfg_set_num "$key" "$v"
echo -e "${GREEN}已更新${NC}"
_restart_service
else
(( max > 0 )) \
&& echo -e "${RED}请输入 ${min}~${max} 的整数${NC}" \
|| echo -e "${RED}请输入不小于 ${min} 的整数${NC}"
fi
read -p "按回车键继续..."
}
# -------- 子菜单:域名管理 --------
modify_domains() {
while true; do
clear
_load_domains
echo -e "${CYAN}==============================${NC}"
echo -e "${CYAN} 域名管理${NC}"
echo -e "${CYAN}==============================${NC}"
local i=1
for d in "${DOMAIN_LIST[@]}"; do echo " ${i}. ${d}"; ((i++)); done
echo ""
echo " a. 添加域名"
echo " d. 删除域名"
echo " 0. 返回上级菜单"
echo -e "${CYAN}==============================${NC}"
read -p "请输入操作: " op
case $op in
a|A)
local before=${#DOMAIN_LIST[@]}
_read_domains_input
if [[ ${#DOMAIN_LIST[@]} -gt $before ]]; then
_save_domains && _restart_service
else
echo -e "${YELLOW}未输入任何域名,已取消${NC}"
fi
read -p "按回车键继续..."
;;
d|D)
if [[ ${#DOMAIN_LIST[@]} -eq 0 ]]; then
echo -e "${RED}当前没有域名可删除${NC}"
read -p "按回车键继续..."; continue
fi
if [[ ${#DOMAIN_LIST[@]} -eq 1 ]]; then
echo -e "${RED}至少需要保留一个域名,无法删除最后一项${NC}"
read -p "按回车键继续..."; continue
fi
local idx
read -p "请输入要删除的域名序号 (1-${#DOMAIN_LIST[@]}): " idx
if ! [[ "$idx" =~ ^[0-9]+$ ]] || (( idx < 1 || idx > ${#DOMAIN_LIST[@]} )); then
echo -e "${RED}无效序号${NC}"; sleep 2; continue
fi
local removed="${DOMAIN_LIST[$((idx-1))]}"
DOMAIN_LIST=("${DOMAIN_LIST[@]:0:$((idx-1))}" "${DOMAIN_LIST[@]:$idx}")
_save_domains \
&& echo -e "${GREEN}已删除: ${removed}${NC}" \
&& _restart_service
read -p "按回车键继续..."
;;
0) break ;;
*) echo -e "${RED}无效操作${NC}"; sleep 1 ;;
esac
done
}
# -------- 子菜单:运行参数 --------
modify_runtime_params() {
while true; do
clear
local cur_interval cur_ttl cur_proxied cur_rwait
cur_interval=$(grep -m1 "^CHECK_INTERVAL=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_ttl=$(grep -m1 "^DNS_TTL=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_proxied=$(grep -m1 "^CF_PROXIED=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_rwait=$(grep -m1 "^RESTART_WAIT=" "$MONITOR_SCRIPT" | cut -d= -f2)
echo -e "${CYAN}==============================${NC}"
echo -e "${CYAN} 运行参数设置${NC}"
echo -e "${CYAN}==============================${NC}"
echo " 1. 检测间隔 (CHECK_INTERVAL) : ${cur_interval}s"
echo " 2. DNS TTL (DNS_TTL) : ${cur_ttl}s"
echo " 3. CF 代理 (CF_PROXIED) : ${cur_proxied}"
echo " 4. 重启等待 (RESTART_WAIT) : ${cur_rwait}s"
echo " 0. 返回上级菜单"
echo -e "${CYAN}==============================${NC}"
read -p "请选择参数编号: " p
case $p in
1) _update_num "CHECK_INTERVAL" "新的检测间隔(秒,当前 ${cur_interval},建议 ≥10): " 10 ;;
2) _update_num "DNS_TTL" "新的 DNS TTL(秒,当前 ${cur_ttl}): " 1 ;;
3)
read -p "CF_PROXIED (true/false,当前 ${cur_proxied}): " v
if [[ "$v" == "true" || "$v" == "false" ]]; then
_cfg_set_num "CF_PROXIED" "$v"
echo -e "${GREEN}已更新${NC}"; _restart_service
else
echo -e "${RED}只能输入 true 或 false${NC}"
fi
read -p "按回车键继续..." ;;
4) _update_num "RESTART_WAIT" "新的重启等待时间(秒,当前 ${cur_rwait}): " 1 ;;
0) break ;;
*) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
esac
done
}
# -------- 子菜单:单项配置 --------
modify_single_item() {
while true; do
clear
local cur_token cur_zone cur_vps1 cur_vps2 cur_con1 cur_con2
local cur_dash1 cur_dash2 cur_user cur_pass_raw cur_pass stars
cur_token=$(_cfg_get "CF_API_TOKEN")
cur_zone=$(_cfg_get "CF_ZONE_ID")
cur_vps1=$(_cfg_get "VPS1_IP")
cur_vps2=$(_cfg_get "VPS2_IP")
cur_con1=$(_cfg_get "FRPC1_CONTAINER")
cur_con2=$(_cfg_get "FRPC2_CONTAINER")
cur_user=$(_cfg_get "FRPC_USER")
cur_dash1=$(grep -m1 "^FRPC1_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_dash2=$(grep -m1 "^FRPC2_DASH_PORT=" "$MONITOR_SCRIPT" | cut -d= -f2)
cur_pass_raw=$(_cfg_get "FRPC_PASS")
stars="${cur_pass_raw//?/*}"
if [[ -z "$cur_pass_raw" ]]; then
cur_pass="(未设置)"
else
cur_pass="${cur_pass_raw:0:2}${stars:2}"
fi
echo -e "${CYAN}==============================${NC}"
echo -e "${CYAN} 单项配置修改${NC}"
echo -e "${CYAN}==============================${NC}"
echo " 1. CF API Token : ${cur_token:0:8}…(已隐藏)"
echo " 2. CF Zone ID : ${cur_zone}"
echo " 3. 主线 VPS1 IP : ${cur_vps1}"
echo " 4. 备线 VPS2 IP : ${cur_vps2}"
echo " 5. frpc01 容器名 : ${cur_con1}"
echo " 6. frpc02 容器名 : ${cur_con2}"
echo " 7. frpc01 Dashboard 端口 : ${cur_dash1}"
echo " 8. frpc02 Dashboard 端口 : ${cur_dash2}"
echo " 9. Dashboard 用户名 : ${cur_user}"
echo " 10. Dashboard 密码 : ${cur_pass}"
echo " 0. 返回上级菜单"
echo -e "${CYAN}==============================${NC}"
read -p "请选择项目编号: " item
case $item in
1) _update_cfg "CF_API_TOKEN" "新的 CF API Token: " ;;
2) _update_cfg "CF_ZONE_ID" "新的 CF Zone ID: " ;;
3) _update_cfg "VPS1_IP" "新的 VPS1 IP: " ;;
4) _update_cfg "VPS2_IP" "新的 VPS2 IP: " ;;
5) _update_cfg "FRPC1_CONTAINER" "新的 frpc01 容器名: " ;;
6) _update_cfg "FRPC2_CONTAINER" "新的 frpc02 容器名: " ;;
7) _update_num "FRPC1_DASH_PORT" "新的 frpc01 Dashboard 端口(当前 ${cur_dash1}): " 1 65535 ;;
8) _update_num "FRPC2_DASH_PORT" "新的 frpc02 Dashboard 端口(当前 ${cur_dash2}): " 1 65535 ;;
9) _update_cfg "FRPC_USER" "新的 Dashboard 用户名: " ;;
10) _update_cfg "FRPC_PASS" "新的 Dashboard 密码: " ;;
0) break ;;
*) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
esac
done
}
# -------- 修改配置入口 --------
modify_config() {
if [[ ! -f "$MONITOR_SCRIPT" ]]; then
echo -e "${RED}未找到监控脚本 ${MONITOR_SCRIPT},请先执行「1. 安装/配置监控脚本」${NC}"
read -p "按回车键返回主菜单..."; return
fi
while true; do
clear
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} 修改配置${NC}"
echo -e "${CYAN}========================================${NC}"
echo " 1. 修改单项配置 (Token / IP / 容器名 / 端口 / 密码等)"
echo " 2. 管理域名列表 (添加 / 删除域名)"
echo " 3. 修改运行参数 (检测间隔 / TTL / 代理模式等)"
echo " 4. 重新向导配置 (覆盖所有配置,重新完整填写)"
echo " 0. 返回主菜单"
echo -e "${CYAN}========================================${NC}"
read -p "请输入选项编号: " sub
case $sub in
1) modify_single_item ;;
2) modify_domains ;;
3) modify_runtime_params ;;
4)
echo -e "${YELLOW}此操作将覆盖当前所有配置,重新引导填写!${NC}"
read -p "确认继续?(y/n): " confirm
if [[ "$confirm" == "y" ]]; then
if collect_config; then
generate_monitor_script
_restart_service
echo -e "${GREEN}配置已全部更新${NC}"
fi
read -p "按回车键继续..."
fi
;;
0) break ;;
*) echo -e "${RED}无效选项${NC}"; sleep 1 ;;
esac
done
}
# ================================================================
# -------- 查看日志 --------
# ================================================================
view_logs() {
if [[ ! -f "$LOG_FILE" ]]; then
echo -e "${RED}日志文件不存在,服务可能尚未启动${NC}"
read -p "按回车键返回主菜单..."; return
fi
echo -e "${GREEN}实时日志输出中,按 Ctrl+C 退出查看...${NC}"
echo ""
# 使用 sed -u 实时替换英文状态码为中文
# UNKNOWN -> 未知
# BOTH_OK -> 双线正常
# VPS1_FAIL -> 主线故障
# VPS2_FAIL -> 备线故障
# BOTH_FAIL -> 双线故障
# OK -> 正常
# VPS_DOWN -> 连接中断
tail -f "$LOG_FILE" | sed -u \
-e 's/\[OK\]/[成功]/g' \
-e 's/\[!!\]/[警告]/g' \
-e 's/\[XX\]/[失败]/g' \
-e 's/\[--\]/[提示]/g' \
-e 's/\[>>\]/[执行]/g' \
-e 's/\[\.\.\]/[补偿]/g' \
-e 's/\[ST\]/[巡检]/g' \
-e 's/UNKNOWN/未知/g' \
-e 's/BOTH_OK/双线正常/g' \
-e 's/VPS1_FAIL/主线故障/g' \
-e 's/VPS2_FAIL/备线故障/g' \
-e 's/BOTH_FAIL/双线故障/g' \
-e 's/\bOK\b/正常/g' \
-e 's/VPS_DOWN/连接中断/g'
}
# ================================================================
# -------- 卸载 --------
# ================================================================
uninstall_script() {
echo -e "${YELLOW}此操作将停止服务并删除所有相关文件(包括本脚本)!${NC}"
echo ""
read -p "确认卸载?(y/n): " confirm
[[ "$confirm" != "y" ]] && return
echo -e "${RED}正在卸载...${NC}"
systemctl stop frpc-monitor 2>/dev/null
systemctl disable frpc-monitor 2>/dev/null
rm -f "$SERVICE_FILE"
systemctl daemon-reload
systemctl reset-failed 2>/dev/null
rm -f "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "${LOG_FILE}"* "$STATE_FILE" "$LOGROTATE_FILE"
echo -e "\n${CYAN}--- 验证清理结果 ---${NC}"
for f in "$MONITOR_SCRIPT" "$INSTALL_DEPS_SCRIPT" "$SERVICE_FILE" "$LOG_FILE" "$LOGROTATE_FILE" "$STATE_FILE"; do
if [[ ! -e "$f" ]]; then
echo -e " ${GREEN}$f 已删除${NC}"
else
echo -e " ${RED}$f 仍存在${NC}"
fi
done
echo ""
echo -e " 服务状态: $(systemctl is-active frpc-monitor 2>&1)"
echo ""
echo -e "${GREEN}卸载完成${NC}"
echo -e "${YELLOW}本脚本将在退出后自动删除:$0${NC}"
echo ""
read -p "按回车键退出并完成清理..."
# 后台延迟删除脚本自身,留出主进程退出时间
(sleep 1 && rm -f "$0") &
exit 0
}
# ================================================================
# 主循环
# ================================================================
while true; do
print_menu
read -p "请输入选项编号: " choice
case $choice in
1) install_script ;;
2) modify_config ;;
3) view_logs ;;
4) uninstall_script ;;
0) echo -e "退出"; exit 0 ;;
*) echo -e "${RED}无效选项${NC}"; sleep 2 ;;
esac
done
3️⃣ 赋予文件执行权限并执行文件
sudo chmod +x /opt/frpc-failover.sh && sudo bash /opt/frpc-failover.sh
首次配置向导示例
脚本会自动引导完成以下配置:
1️⃣ 检测 frpc 容器
检测到以下 frpc 容器:
- frpc-office
- frpc-backup
2️⃣ Cloudflare 配置
Cloudflare API Token: ************************
Cloudflare Zone ID: 1234567890abcdef
3️⃣ VPS 配置
主线 VPS1 IP: 1.2.3.4
备线 VPS2 IP: 5.6.7.8
FRPS 端口 (默认 7000): 7000
4️⃣ 容器配置
frpc01 容器名: frpc-office
frpc02 容器名: frpc-backup
frpc Dashboard 用户名: admin
frpc Dashboard 密码: ******
5️⃣ 域名配置
需要切换的域名:
- example.com
- api.example.com
- www.example.com
安装完成后,服务会自动启动!🎉
⚙️ 配置管理
主菜单功能
========================================
frpc 双线监控 + CF DNS 自动切换管理器
========================================
1. 安装/配置监控脚本 (首次安装)
2. 修改配置
3. 查看实时日志
4. 卸载并清理所有文件
0. 退出
========================================
配置项详解
注意:脚本中 Dashboard 的默认访问地址分别为:
http://127.0.0.1:7401/api/status和http://127.0.0.1:7402/api/status
需确保两个 frpc 容器映射了不同的主机端口(例:7401、7402)
| 配置类别 | 参数 | 说明 | 默认值 |
|---|---|---|---|
| API 凭证 | CF_API_TOKEN |
Cloudflare API Token | 必填 |
CF_ZONE_ID |
域名所在 Zone ID | 必填 | |
| 网络配置 | VPS1_IP |
主线服务器 IP | 必填 |
VPS2_IP |
备线服务器 IP | 必填 | |
FRPS_PORT |
frps 服务端口 | 7000 | |
| 容器配置 | FRPC1_CONTAINER |
主线 frpc 容器名 | 自动检测 |
FRPC2_CONTAINER |
备线 frpc 容器名 | 自动检测 | |
FRPC_USER |
Dashboard 用户名 | 必填 | |
FRPC_PASS |
Dashboard 密码 | 必填 | |
| 运行参数 | CHECK_INTERVAL |
健康检查间隔 | 30 秒 |
DNS_TTL |
DNS 记录 TTL | 60 秒 | |
RESTART_WAIT |
容器重启等待时间 | 15 秒 | |
CF_PROXIED |
Cloudflare 代理 | false | |
| 域名管理 | DOMAINS |
需要切换的域名列表 | 至少 1 个 |
修改配置示例
添加新域名
[主菜单] → 2 (修改配置) → 2 (管理域名列表) → a (添加域名)
域名: new-service.example.com
✅ 已添加: new-service.example.com
调整检测间隔
[主菜单] → 2 (修改配置) → 3 (修改运行参数) → 1
新的检测间隔(秒,当前 30,建议 ≥10): 15
✅ 服务已重启,配置生效
🗑️ 卸载清理
自动卸载
在主菜单中选择 4. 卸载并清理所有文件,脚本会自动:
- 停止并禁用 systemd 服务
- 删除监控脚本 (
/opt/frpc-monitor.sh) - 删除依赖安装脚本 (
/opt/install-deps.sh) - 删除日志文件及轮转配置
- 删除状态文件
- 自动删除自身脚本
🔐 本站配置信息
此区域为 ✱✱✱ 内容,请输入密码验证身份。