Files
vps-management-bot/scripts/hkt.sh
2026-03-21 01:14:00 +08:00

540 lines
13 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_exit_ip() {
section "测试出口 IP"
info "查询当前出口 IP..."
local exit_ip
exit_ip="$(curl -4 --connect-timeout 5 --max-time 10 -s https://api.ipify.org 2>/dev/null || echo "查询失败")"
echo
success "当前出口 IP: $exit_ip"
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 "================================"
echo " 默认出站源地址管理器 v2.0"
echo "================================"
echo
show_status 2>/dev/null || echo
echo
echo "功能菜单"
echo "--------"
echo " 1) 检测网络环境"
echo " 2) 应用内网 IP 默认出站"
echo " 3) 恢复公网 IP 默认出站"
echo " 4) 测试出口 IP"
echo " 5) 测试连通性"
echo " 6) 查看详细状态"
echo " 7) 安装开机自启"
echo " 8) 移除开机自启"
echo " 9) 清理策略规则"
echo " 0) 退出"
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_exit_ip
read -rp "按回车继续..."
;;
5)
test_connectivity
read -rp "按回车继续..."
;;
6)
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 "按回车继续..."
;;
7)
install_service
read -rp "按回车继续..."
;;
8)
remove_service
read -rp "按回车继续..."
;;
9)
clean_policy
success "策略规则已清理"
read -rp "按回车继续..."
;;
0)
echo "再见!"
exit 0
;;
*)
warn "无效选项"
sleep 1
;;
esac
done
}
main "$@"