Files
vps-management-bot/scripts/default-src-manager.sh

530 lines
14 KiB
Bash
Raw Normal View History

2026-03-21 00:58:35 +08:00
#!/usr/bin/env bash
#
# 默认出站源地址管理器 Pro
# 功能:智能管理服务器多 IP 出站策略
# 作者:顶尖定制版
# 版本v2.0
#
set -euo pipefail
# ==================== 配置 ====================
STATE_DIR="/var/lib/default-src-manager"
STATE_FILE="$STATE_DIR/config.json"
BACKUP_DIR="$STATE_DIR/backups"
LOG_FILE="$STATE_DIR/manager.log"
SERVICE_NAME="default-src-manager.service"
SERVICE_FILE="/etc/systemd/system/$SERVICE_NAME"
APPLY_SCRIPT="/usr/local/sbin/default-src-apply"
# ==================== 颜色 ====================
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
fi
# ==================== 工具函数 ====================
log() {
local level="$1"
shift
local msg="$*"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$timestamp] [$level] $msg" >> "$LOG_FILE"
}
info() {
echo -e "${CYAN}[INFO]${RESET} $*"
log "INFO" "$*"
}
success() {
echo -e "${GREEN}[OK]${RESET} $*"
log "OK" "$*"
}
warn() {
echo -e "${YELLOW}[WARN]${RESET} $*"
log "WARN" "$*"
}
error() {
echo -e "${RED}[ERROR]${RESET} $*" >&2
log "ERROR" "$*"
}
die() {
error "$*"
exit 1
}
section() {
echo
echo -e "${BOLD}${BLUE}━━━━ $* ━━━━${RESET}"
echo
}
kv() {
printf " ${DIM}%-18s${RESET} %s\n" "$1:" "$2"
}
check_root() {
[[ $EUID -eq 0 ]] || die "需要 root 权限运行"
}
check_deps() {
local missing=()
for cmd in ip jq curl ping systemctl; do
command -v "$cmd" >/dev/null 2>&1 || missing+=("$cmd")
done
if [[ ${#missing[@]} -gt 0 ]]; then
die "缺少依赖: ${missing[*]}"
fi
}
init_dirs() {
mkdir -p "$STATE_DIR" "$BACKUP_DIR"
touch "$LOG_FILE"
}
# ==================== IP 工具 ====================
is_private_ipv4() {
local ip="$1"
[[ "$ip" =~ ^10\. ]] && return 0
[[ "$ip" =~ ^192\.168\. ]] && return 0
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]] && return 0
return 1
}
get_ip_score() {
local ip="$1"
# 优先级172.16-31 > 10 > 192.168
if [[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]]; then
echo 300
elif [[ "$ip" =~ ^10\. ]]; then
echo 200
elif [[ "$ip" =~ ^192\.168\. ]]; then
echo 100
else
echo 0
fi
}
# ==================== 环境检测 ====================
detect_environment() {
info "正在检测网络环境..."
local iface gw public_ips=() private_ips=()
# 检测默认网卡和网关
iface="$(ip -4 route show default | awk 'NR==1{print $5}')"
gw="$(ip -4 route show default | awk 'NR==1{print $3}')"
[[ -n "$iface" ]] || die "未找到默认网卡"
[[ -n "$gw" ]] || die "未找到默认网关"
# 收集所有 IP
while IFS= read -r line; do
local ip cidr
cidr="$(echo "$line" | awk '{print $4}')"
ip="${cidr%/*}"
if is_private_ipv4 "$ip"; then
private_ips+=("$ip")
else
public_ips+=("$ip")
fi
done < <(ip -o -4 addr show dev "$iface" scope global 2>/dev/null)
[[ ${#public_ips[@]} -gt 0 ]] || die "未找到公网 IP"
[[ ${#private_ips[@]} -gt 0 ]] || die "未找到内网 IP"
# 选择最优内网 IP优先 172.16-31
local best_private="${private_ips[0]}"
local best_score=0
for ip in "${private_ips[@]}"; do
local score
score="$(get_ip_score "$ip")"
if [[ $score -gt $best_score ]]; then
best_score=$score
best_private="$ip"
fi
done
# 保存配置
jq -n \
--arg iface "$iface" \
--arg gw "$gw" \
--arg public "${public_ips[0]}" \
--arg private "$best_private" \
'{
interface: $iface,
gateway: $gw,
public_ip: $public,
private_ip: $private,
detected_at: (now | strftime("%Y-%m-%d %H:%M:%S"))
}' > "$STATE_FILE"
success "环境检测完成"
kv "网卡" "$iface"
kv "网关" "$gw"
kv "公网 IP" "${public_ips[0]}"
kv "内网 IP" "$best_private"
if [[ ${#private_ips[@]} -gt 1 ]]; then
info "检测到多个内网 IP已自动选择优先级最高的"
fi
}
load_config() {
[[ -f "$STATE_FILE" ]] || die "配置文件不存在,请先运行检测"
IFACE="$(jq -r '.interface' "$STATE_FILE")"
GATEWAY="$(jq -r '.gateway' "$STATE_FILE")"
PUBLIC_IP="$(jq -r '.public_ip' "$STATE_FILE")"
PRIVATE_IP="$(jq -r '.private_ip' "$STATE_FILE")"
}
# ==================== 策略应用 ====================
backup_routes() {
local backup_file="$BACKUP_DIR/routes-$(date +%Y%m%d-%H%M%S).txt"
{
echo "=== Default Route ==="
ip route show default
echo
echo "=== Policy Rules ==="
ip rule show
echo
echo "=== Table 100 ==="
ip route show table 100 2>/dev/null || echo "empty"
echo
echo "=== Table 200 ==="
ip route show table 200 2>/dev/null || echo "empty"
} > "$backup_file"
info "已备份当前路由配置到 $backup_file"
}
clean_policy() {
info "清理现有策略..."
ip rule del pref 100 2>/dev/null || true
ip rule del pref 110 2>/dev/null || true
ip route flush table 100 2>/dev/null || true
ip route flush table 200 2>/dev/null || true
ip route flush cache 2>/dev/null || true
}
apply_private_default() {
load_config
backup_routes
section "应用内网 IP 默认出站"
info "配置主路由表..."
ip route replace default via "$GATEWAY" dev "$IFACE" src "$PRIVATE_IP"
info "配置策略路由表..."
clean_policy
# 表 100: 内网 IP 源
ip route replace default via "$GATEWAY" dev "$IFACE" src "$PRIVATE_IP" table 100
ip rule add pref 100 from "$PRIVATE_IP/32" table 100
# 表 200: 公网 IP 源
ip route replace default via "$GATEWAY" dev "$IFACE" src "$PUBLIC_IP" table 200
ip rule add pref 110 from "$PUBLIC_IP/32" table 200
ip route flush cache 2>/dev/null || true
success "策略应用完成"
verify_routes
}
rollback_public_default() {
load_config
backup_routes
section "恢复公网 IP 默认出站"
clean_policy
ip route replace default via "$GATEWAY" dev "$IFACE" src "$PUBLIC_IP"
ip route flush cache 2>/dev/null || true
success "已恢复为公网 IP 默认出站"
verify_routes
}
# ==================== 验证测试 ====================
verify_routes() {
echo
info "当前路由状态:"
echo
echo " 主默认路由:"
ip route show default | sed 's/^/ /'
echo
echo " 策略规则:"
ip rule show | grep -E "100|110" | sed 's/^/ /' || echo " (无)"
}
test_connectivity() {
load_config
section "连通性测试"
# 测试 1: 默认路由选路
info "测试默认新连接选路..."
ip route get 1.1.1.1 | sed 's/^/ /'
echo
# 测试 2: 内网 IP 出站
info "测试内网 IP 出站..."
if ping -I "$PRIVATE_IP" -c 2 -W 3 1.1.1.1 >/dev/null 2>&1; then
success "内网 IP 出站正常"
else
warn "内网 IP 出站失败"
fi
# 测试 3: 公网 IP 查询
info "查询实际出口 IP..."
local exit_ip
exit_ip="$(curl -4 --interface "$PRIVATE_IP" --connect-timeout 5 --max-time 10 -s https://api.ipify.org 2>/dev/null || echo "查询失败")"
kv "出口 IP" "$exit_ip"
# 测试 4: DNS 解析
info "测试 DNS 解析..."
if host -W 3 google.com >/dev/null 2>&1; then
success "DNS 解析正常"
else
warn "DNS 解析异常"
fi
}
# ==================== 开机自启 ====================
install_service() {
load_config
section "安装开机自启"
# 创建应用脚本
cat > "$APPLY_SCRIPT" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
STATE_FILE="/var/lib/default-src-manager/config.json"
[[ -f "$STATE_FILE" ]] || exit 1
IFACE="$(jq -r '.interface' "$STATE_FILE")"
GATEWAY="$(jq -r '.gateway' "$STATE_FILE")"
PUBLIC_IP="$(jq -r '.public_ip' "$STATE_FILE")"
PRIVATE_IP="$(jq -r '.private_ip' "$STATE_FILE")"
# 清理
ip rule del pref 100 2>/dev/null || true
ip rule del pref 110 2>/dev/null || true
ip route flush table 100 2>/dev/null || true
ip route flush table 200 2>/dev/null || true
# 应用
ip route replace default via "$GATEWAY" dev "$IFACE" src "$PRIVATE_IP"
ip route replace default via "$GATEWAY" dev "$IFACE" src "$PRIVATE_IP" table 100
ip route replace default via "$GATEWAY" dev "$IFACE" src "$PUBLIC_IP" table 200
ip rule add pref 100 from "$PRIVATE_IP/32" table 100
ip rule add pref 110 from "$PUBLIC_IP/32" table 200
ip route flush cache 2>/dev/null || true
EOF
chmod +x "$APPLY_SCRIPT"
# 创建 systemd 服务
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=Default Source IP Manager
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=$APPLY_SCRIPT
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now "$SERVICE_NAME"
success "开机自启已安装"
systemctl status "$SERVICE_NAME" --no-pager || true
}
remove_service() {
section "移除开机自启"
systemctl disable --now "$SERVICE_NAME" 2>/dev/null || true
rm -f "$SERVICE_FILE" "$APPLY_SCRIPT"
systemctl daemon-reload
success "开机自启已移除"
}
# ==================== 状态显示 ====================
show_status() {
if [[ ! -f "$STATE_FILE" ]]; then
warn "尚未检测环境,请先运行选项 1"
return
fi
load_config
section "当前状态"
kv "网卡" "$IFACE"
kv "网关" "$GATEWAY"
kv "公网 IP" "$PUBLIC_IP"
kv "内网 IP" "$PRIVATE_IP"
local detected_at
detected_at="$(jq -r '.detected_at' "$STATE_FILE")"
kv "检测时间" "$detected_at"
echo
info "当前默认路由:"
ip route show default | sed 's/^/ /'
if ip route show default | grep -q "src $PRIVATE_IP"; then
echo
success "当前模式:内网 IP 默认出站"
elif ip route show default | grep -q "src $PUBLIC_IP"; then
echo
info "当前模式:公网 IP 默认出站"
else
echo
warn "当前模式:未识别"
fi
}
# ==================== 菜单 ====================
show_menu() {
clear
echo -e "${BOLD}${CYAN}"
echo "╔════════════════════════════════════════════╗"
echo "║ 默认出站源地址管理器 Pro v2.0 ║"
echo "╚════════════════════════════════════════════╝"
echo -e "${RESET}"
show_status 2>/dev/null || echo
echo
echo -e "${BOLD}功能菜单${RESET}"
echo
echo " ${GREEN}1${RESET}) 检测网络环境"
echo " ${GREEN}2${RESET}) 应用内网 IP 默认出站"
echo " ${GREEN}3${RESET}) 恢复公网 IP 默认出站"
echo " ${GREEN}4${RESET}) 测试连通性"
echo " ${GREEN}5${RESET}) 查看详细状态"
echo " ${GREEN}6${RESET}) 安装开机自启"
echo " ${GREEN}7${RESET}) 移除开机自启"
echo " ${GREEN}8${RESET}) 清理策略规则"
echo " ${GREEN}9${RESET}) 查看操作日志"
echo " ${RED}0${RESET}) 退出"
echo
}
# ==================== 主函数 ====================
main() {
check_root
check_deps
init_dirs
while true; do
show_menu
read -rp "请选择 [0-9]: " choice
echo
case "$choice" in
1)
detect_environment
read -rp "按回车继续..."
;;
2)
apply_private_default
read -rp "按回车继续..."
;;
3)
rollback_public_default
read -rp "按回车继续..."
;;
4)
test_connectivity
read -rp "按回车继续..."
;;
5)
section "详细路由状态"
echo "主路由表:"
ip route show | sed 's/^/ /'
echo
echo "策略规则:"
ip rule show | sed 's/^/ /'
echo
echo "表 100内网 IP 源):"
ip route show table 100 2>/dev/null | sed 's/^/ /' || echo " (空)"
echo
echo "表 200公网 IP 源):"
ip route show table 200 2>/dev/null | sed 's/^/ /' || echo " (空)"
read -rp "按回车继续..."
;;
6)
install_service
read -rp "按回车继续..."
;;
7)
remove_service
read -rp "按回车继续..."
;;
8)
clean_policy
success "策略规则已清理"
read -rp "按回车继续..."
;;
9)
section "最近 20 条日志"
tail -20 "$LOG_FILE" 2>/dev/null || echo "日志为空"
read -rp "按回车继续..."
;;
0)
echo "再见!"
exit 0
;;
*)
warn "无效选项"
sleep 1
;;
esac
done
}
main "$@"