diff --git a/scripts/default-src-manager.sh b/scripts/default-src-manager.sh new file mode 100644 index 0000000..a2a7fdc --- /dev/null +++ b/scripts/default-src-manager.sh @@ -0,0 +1,529 @@ +#!/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" </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 "$@"