#!/bin/bash # Xray 一键安装与管理脚本 # 路径: /etc/xray # 支持: Debian/Ubuntu/CentOS, Alpine, OpenWrt # 功能: 服务端安装, 多协议配置, 配置修改, 分享链接 # 颜色定义 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # 路径定义 XRAY_DIR="/etc/xray" XRAY_BIN="$XRAY_DIR/xray" CONFIG_FILE="$XRAY_DIR/config.json" GEOIP_FILE="$XRAY_DIR/geoip.dat" GEOSITE_FILE="$XRAY_DIR/geosite.dat" API_SERVER="127.0.0.1:10085" # 检查 Root 权限 if [ "$(id -u)" != "0" ]; then echo -e "${RED}错误: 必须使用 root 权限运行此脚本${NC}" exit 1 fi # 系统检测 detect_system() { if [ -f /etc/alpine-release ]; then SYSTEM_TYPE="Alpine" PKG_MANAGER="apk" elif command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then SYSTEM_TYPE="Systemd" # Debian, Ubuntu, CentOS if command -v apt-get >/dev/null 2>&1; then PKG_MANAGER="apt" elif command -v yum >/dev/null 2>&1; then PKG_MANAGER="yum" else PKG_MANAGER="unknown" fi elif [ -f /etc/openwrt_release ]; then SYSTEM_TYPE="OpenWrt" PKG_MANAGER="opkg" else SYSTEM_TYPE="Generic" # 无服务管理器 (WSL, Docker 等) if command -v apt-get >/dev/null 2>&1; then PKG_MANAGER="apt" elif command -v apk >/dev/null 2>&1; then PKG_MANAGER="apk" else PKG_MANAGER="unknown" fi fi ARCH=$(uname -m) case "$ARCH" in x86_64) XRAY_ARCH="64" ;; aarch64) XRAY_ARCH="arm64-v8a" ;; armv7l) XRAY_ARCH="arm32-v7a" ;; *) echo -e "${RED}不支持的架构: $ARCH${NC}"; exit 1 ;; esac } # 安装依赖 install_dependencies() { # 预检查依赖,避免重复安装 if command -v curl >/dev/null 2>&1 && \ command -v unzip >/dev/null 2>&1 && \ command -v jq >/dev/null 2>&1; then return 0 fi echo -e "${BLUE}正在安装必要依赖...${NC}" case "$PKG_MANAGER" in apt) apt-get update apt-get install -y curl unzip jq ;; yum) yum install -y curl unzip jq ;; apk) apk add curl unzip jq ;; opkg) opkg update opkg install curl unzip jq ;; *) echo -e "${YELLOW}无法自动安装依赖,请确保已安装: curl, unzip, jq${NC}" ;; esac } # 获取 UUID get_uuid() { cat /proc/sys/kernel/random/uuid } # 获取随机端口 get_random_port() { shuf -i 10000-65000 -n 1 } # 安装 Xray install_xray() { install_dependencies echo -e "${BLUE}正在获取 Xray 最新版本...${NC}" LATEST_VERSION=$(curl -s https://api.github.com/repos/XTLS/Xray-core/releases/latest | jq -r .tag_name) if [ -z "$LATEST_VERSION" ] || [ "$LATEST_VERSION" == "null" ]; then echo -e "${RED}获取版本失败,请检查网络连接${NC}" exit 1 fi # 检查当前版本 if [ -f "$XRAY_BIN" ]; then CURRENT_VERSION=$($XRAY_BIN version | head -n 1 | awk '{print $2}') # 移除 v 前缀 LATEST_V_NUM=${LATEST_VERSION#v} CURRENT_V_NUM=${CURRENT_VERSION#v} if [ "$LATEST_V_NUM" == "$CURRENT_V_NUM" ]; then echo -e "${GREEN}当前已是最新版本 ($CURRENT_VERSION)${NC}" return 0 fi echo -e "${YELLOW}发现新版本: $CURRENT_VERSION -> $LATEST_VERSION${NC}" else echo -e "${GREEN}准备安装版本: $LATEST_VERSION${NC}" fi mkdir -p "$XRAY_DIR" DOWNLOAD_URL="https://github.com/XTLS/Xray-core/releases/download/${LATEST_VERSION}/Xray-linux-${XRAY_ARCH}.zip" echo -e "${BLUE}正在下载 Xray...${NC}" curl -L -o /tmp/xray.zip "$DOWNLOAD_URL" echo -e "${BLUE}正在解压...${NC}" unzip -o /tmp/xray.zip -d /tmp/xray_extract mv /tmp/xray_extract/xray "$XRAY_BIN" mv /tmp/xray_extract/geoip.dat "$GEOIP_FILE" mv /tmp/xray_extract/geosite.dat "$GEOSITE_FILE" chmod +x "$XRAY_BIN" ln -sf "$XRAY_BIN" /usr/bin/xray rm -rf /tmp/xray.zip /tmp/xray_extract echo -e "${GREEN}Xray 安装完成${NC}" } # 配置服务 setup_service() { if [ "$SYSTEM_TYPE" == "Generic" ]; then echo -e "${YELLOW}未检测到 Systemd/OpenRC/Procd,将使用后台进程运行。${NC}" return fi echo -e "${BLUE}正在配置系统服务...${NC}" if [ "$SYSTEM_TYPE" == "Systemd" ]; then cat > /etc/systemd/system/xray.service < /etc/init.d/xray < /etc/init.d/xray < "$CONFIG_FILE" < "$CONFIG_FILE" < "$CONFIG_FILE" </dev/null 2>&1 return $? } # 重启日志 (API) restart_logger() { if ! check_api; then echo -e "${RED}API 服务未运行${NC}" return 1 fi $XRAY_BIN api restartlogger -s "$API_SERVER" if [ $? -eq 0 ]; then echo -e "${GREEN}日志服务已重启${NC}" else echo -e "${RED}日志服务重启失败${NC}" fi } # 查看日志菜单 view_log_menu() { while true; do echo -e "${YELLOW}日志管理${NC}" echo "1. 查看访问日志 (Access Log)" echo "2. 查看错误日志 (Error Log)" echo "3. 重启日志服务 (Reopen Logs)" echo "0. 返回上级" read -p "请选择: " LOG_CHOICE case "$LOG_CHOICE" in 1) ACCESS_LOG=$(jq -r .log.access "$CONFIG_FILE") if [ -f "$ACCESS_LOG" ]; then echo -e "${GREEN}正在查看访问日志 (Ctrl+C 退出)...${NC}" tail -f "$ACCESS_LOG" else echo -e "${RED}访问日志文件不存在: $ACCESS_LOG${NC}" fi ;; 2) ERROR_LOG=$(jq -r .log.error "$CONFIG_FILE") if [ -f "$ERROR_LOG" ]; then echo -e "${GREEN}正在查看错误日志 (Ctrl+C 退出)...${NC}" tail -f "$ERROR_LOG" else echo -e "${RED}错误日志文件不存在: $ERROR_LOG${NC}" fi ;; 3) restart_logger read -n 1 -s -r -p "按任意键继续..." ;; 0) break ;; *) echo "无效选择" ;; esac done } # 重载入站 (使用 API) reload_inbound() { local INDEX=$1 local OLD_TAG=$2 # 检查 API 状态 if ! check_api; then # API 不可用,回退到重启 restart_service return fi # 获取当前 Tag (如果未提供旧 Tag,则假设 Tag 未变,从文件读取) if [ -z "$OLD_TAG" ]; then OLD_TAG=$(jq -r ".inbounds[$INDEX].tag" "$CONFIG_FILE") fi # 提取新入站配置 local TMP_JSON=$(mktemp) jq "{inbounds: [.inbounds[$INDEX]]}" "$CONFIG_FILE" > "$TMP_JSON" # 移除旧入站 "$XRAY_BIN" api rmi -s "$API_SERVER" "$OLD_TAG" >/dev/null 2>&1 # 添加新入站 if "$XRAY_BIN" api adi -s "$API_SERVER" "$TMP_JSON" >/dev/null 2>&1; then echo -e "${GREEN}配置已热更新 (API)${NC}" else echo -e "${RED}热更新失败,尝试重启服务...${NC}" restart_service fi rm -f "$TMP_JSON" } # 修改配置 modify_config() { if [ ! -f "$CONFIG_FILE" ]; then echo -e "${RED}配置文件不存在!${NC}" return fi echo -e "${BLUE}=== 入站列表 ===${NC}" # 获取入站数量 INBOUND_COUNT=$(jq '.inbounds | length' "$CONFIG_FILE") if [ "$INBOUND_COUNT" -eq 0 ]; then echo -e "${RED}未找到入站配置${NC}" return fi # 映射显示索引到真实索引 declare -a REAL_INDICES local DISPLAY_INDEX=1 # 列出所有入站 for ((i=0; i "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}端口已修改为 $NEW_PORT${NC}" reload_inbound "$INDEX" "$OLD_TAG" else echo -e "${RED}无效端口${NC}" fi ;; 2) read -p "请输入新备注: " NEW_TAG if [ ! -z "$NEW_TAG" ]; then tmp=$(mktemp) jq --argjson idx "$INDEX" --arg tag "$NEW_TAG" '.inbounds[$idx].tag = $tag' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}备注已修改为 $NEW_TAG${NC}" reload_inbound "$INDEX" "$OLD_TAG" fi ;; 3) if [[ "$PROTOCOL" == "vless" || "$PROTOCOL" == "vmess" ]]; then NEW_UUID=$(get_uuid) echo -e "新 UUID: $NEW_UUID" tmp=$(mktemp) jq --argjson idx "$INDEX" --arg uuid "$NEW_UUID" '.inbounds[$idx].settings.clients[0].id = $uuid' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}UUID 已更新${NC}" reload_inbound "$INDEX" "$OLD_TAG" fi ;; 4) if [[ "$SECURITY" == "reality" ]]; then read -p "请输入新的 SNI (例如 www.microsoft.com): " NEW_SNI if [ ! -z "$NEW_SNI" ]; then tmp=$(mktemp) # 更新 dest (SNI:443) 和 serverNames jq --argjson idx "$INDEX" --arg sni "$NEW_SNI" \ '.inbounds[$idx].streamSettings.realitySettings.serverNames = [$sni] | .inbounds[$idx].streamSettings.realitySettings.dest = ($sni + ":443")' \ "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}SNI 已更新为 $NEW_SNI${NC}" reload_inbound "$INDEX" "$OLD_TAG" fi fi ;; 5) if [[ "$SECURITY" == "reality" ]]; then echo "正在生成新密钥对..." KEYS=$($XRAY_BIN x25519) NEW_PRIVATE_KEY=$(echo "$KEYS" | grep "PrivateKey:" | awk -F ':' '{print $NF}' | tr -d '[:space:]') NEW_PUBLIC_KEY=$(echo "$KEYS" | grep "Password:" | awk -F ':' '{print $NF}' | tr -d '[:space:]') if [ ! -z "$NEW_PRIVATE_KEY" ]; then tmp=$(mktemp) jq --argjson idx "$INDEX" --arg key "$NEW_PRIVATE_KEY" '.inbounds[$idx].streamSettings.realitySettings.privateKey = $key' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}PrivateKey 已更新${NC}" echo -e "新的 Public Key (Password): ${YELLOW}$NEW_PUBLIC_KEY${NC} (请更新客户端)" reload_inbound "$INDEX" "$OLD_TAG" else echo -e "${RED}生成密钥失败${NC}" fi fi ;; 6) if [[ "$SECURITY" == "reality" ]]; then NEW_SHORT_ID=$(cat /proc/sys/kernel/random/uuid | tr -d '-' | head -c 8) echo -e "新 ShortId: $NEW_SHORT_ID" tmp=$(mktemp) jq --argjson idx "$INDEX" --arg sid "$NEW_SHORT_ID" '.inbounds[$idx].streamSettings.realitySettings.shortIds = [$sid]' "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}ShortId 已更新${NC}" reload_inbound "$INDEX" "$OLD_TAG" fi ;; 9) configure_xray restart_service ;; *) return ;; esac } # 服务操作 start_service() { # 启动前检查配置有效性 if [ -f "$XRAY_BIN" ] && [ -f "$CONFIG_FILE" ]; then if ! "$XRAY_BIN" run -test -c "$CONFIG_FILE" >/dev/null 2>&1; then echo -e "${RED}错误: 配置文件无效,无法启动服务。${NC}" echo -e "${YELLOW}请尝试在菜单中选择 '2. 修改配置' -> '3. 重新生成完整配置'${NC}" "$XRAY_BIN" run -test -c "$CONFIG_FILE" # 输出具体错误 return 1 fi fi if [ "$SYSTEM_TYPE" == "Systemd" ]; then systemctl start xray elif [ "$SYSTEM_TYPE" == "Alpine" ] || [ "$SYSTEM_TYPE" == "OpenWrt" ]; then /etc/init.d/xray start elif [ "$SYSTEM_TYPE" == "Generic" ]; then if pgrep -f "$XRAY_BIN" >/dev/null; then echo -e "${YELLOW}Xray 已经在运行中${NC}" else nohup "$XRAY_BIN" run -c "$CONFIG_FILE" >/dev/null 2>&1 & echo -e "${GREEN}Xray 已在后台启动${NC}" fi fi # 检查是否启动成功 sleep 1 if pgrep -f "$XRAY_BIN" >/dev/null; then echo -e "${GREEN}服务状态: 运行中${NC}" else echo -e "${RED}服务启动失败,请检查日志${NC}" fi } stop_service() { if [ "$SYSTEM_TYPE" == "Systemd" ]; then systemctl stop xray elif [ "$SYSTEM_TYPE" == "Alpine" ] || [ "$SYSTEM_TYPE" == "OpenWrt" ]; then /etc/init.d/xray stop elif [ "$SYSTEM_TYPE" == "Generic" ]; then pkill -f "$XRAY_BIN" echo -e "${GREEN}Xray 进程已停止${NC}" fi } restart_service() { stop_service sleep 1 start_service } # 查看链接 view_link() { if [ ! -f "$CONFIG_FILE" ]; then echo -e "${RED}配置文件不存在${NC}" return fi IP=$(curl -s4 ifconfig.me) INBOUND_COUNT=$(jq '.inbounds | length' "$CONFIG_FILE") echo -e "${BLUE}=== 连接信息 ===${NC}" echo -e "地址 (IP): $IP" for ((i=0; i1024 ){ $1/=1024; s++ } printf "%.2f %s", $1, v[s] }' } # 查看统计 view_stats() { if ! check_api; then echo -e "${RED}API 服务未运行或端口未监听${NC}" read -n 1 -s -r -p "按任意键返回..." return fi while true; do clear echo -e "${BLUE}=== 流量统计 ===${NC}" # 获取系统统计 SYS_STATS=$("$XRAY_BIN" api statssys -s "$API_SERVER") UPTIME=$(echo "$SYS_STATS" | jq -r .Uptime) # 简单的 uptime 格式化 UPTIME_H=$(awk -v s="$UPTIME" 'BEGIN {printf "%d天 %02d:%02d:%02d", s/86400, (s%86400)/3600, (s%3600)/60, s%60}') echo -e "运行时间: ${GREEN}$UPTIME_H${NC}" echo "------------------------" STATS_JSON=$("$XRAY_BIN" api statsquery -s "$API_SERVER" "") echo -e "${YELLOW}--- 入站流量 ---${NC}" printf "%-20s %-15s %-15s\n" "Tag" "上行 (Uplink)" "下行 (Downlink)" # 提取所有入站 Tag TAGS=$(echo "$STATS_JSON" | jq -r '.stat[] | select(.name | startswith("inbound>>>")) | .name | split(">>>")[1]' | sort | uniq) if [ -z "$TAGS" ]; then echo "暂无数据" else for TAG in $TAGS; do if [ "$TAG" == "api" ]; then continue; fi UP=$(echo "$STATS_JSON" | jq -r ".stat[] | select(.name == \"inbound>>>$TAG>>>traffic>>>uplink\") | .value // 0") DOWN=$(echo "$STATS_JSON" | jq -r ".stat[] | select(.name == \"inbound>>>$TAG>>>traffic>>>downlink\") | .value // 0") UP_H=$(format_bytes "$UP") DOWN_H=$(format_bytes "$DOWN") printf "%-20s %-15s %-15s\n" "$TAG" "$UP_H" "$DOWN_H" done fi echo "------------------------" echo "r. 重置统计数据" echo "0. 返回主菜单" read -p "请选择: " OP case "$OP" in r|R) "$XRAY_BIN" api statsquery -s "$API_SERVER" "" true >/dev/null echo -e "${GREEN}统计已重置${NC}" sleep 1 ;; 0) return ;; *) ;; esac done } # 添加新入站 add_inbound() { echo -e "${YELLOW}添加新入站配置${NC}" echo "1. VLESS + Reality + Vision [推荐]" echo "2. VMess + TCP" echo "3. VLESS + TCP" read -p "请选择 [1-3] (默认1): " TYPE TYPE=${TYPE:-1} UUID=$(get_uuid) PORT=$(get_random_port) TAG="inbound-$(date +%s)" if [ "$TYPE" == "1" ]; then # Reality Logic KEYS=$($XRAY_BIN x25519) PRIVATE_KEY=$(echo "$KEYS" | grep "PrivateKey:" | awk -F ':' '{print $NF}' | tr -d '[:space:]') PUBLIC_KEY=$(echo "$KEYS" | grep "Password:" | awk -F ':' '{print $NF}' | tr -d '[:space:]') SHORT_ID=$(cat /proc/sys/kernel/random/uuid | tr -d '-' | head -c 8) SNI="www.microsoft.com" INBOUND_JSON=$(cat < "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}配置文件已更新${NC}" echo -e "端口: $PORT" echo -e "UUID: $UUID" if [ "$TYPE" == "1" ]; then echo -e "Public Key: $PUBLIC_KEY" echo -e "Short ID: $SHORT_ID" fi else echo -e "${RED}添加失败 (API)${NC}" fi } # 删除入站 delete_inbound() { echo -e "${YELLOW}删除入站配置${NC}" COUNT=$(jq '.inbounds | length' "$CONFIG_FILE") if [ "$COUNT" -eq 0 ]; then echo "没有入站配置" return fi echo "现有入站:" jq -r '.inbounds[] | "\(.tag) (Port: \(.port), Protocol: \(.protocol))"' "$CONFIG_FILE" | nl -w2 -s". " read -p "请输入要删除的序号 (0 取消): " IDX if [ "$IDX" == "0" ] || [ -z "$IDX" ]; then return; fi IDX=$((IDX-1)) TAG=$(jq -r ".inbounds[$IDX].tag" "$CONFIG_FILE") if [ "$TAG" == "null" ]; then echo "无效序号" return fi $XRAY_BIN api rmi -s "$API_SERVER" "$TAG" if [ $? -eq 0 ]; then echo -e "${GREEN}删除成功 (API)${NC}" tmp=$(mktemp) jq "del(.inbounds[$IDX])" "$CONFIG_FILE" > "$tmp" && mv "$tmp" "$CONFIG_FILE" echo -e "${GREEN}配置文件已更新${NC}" else echo -e "${RED}删除失败 (API) - 可能该 Tag 不存在于运行中的实例${NC}" fi } # 服务管理菜单 service_menu() { while true; do echo -e "${YELLOW}服务管理${NC}" echo "1. 启动服务" echo "2. 停止服务" echo "3. 重启服务" echo "0. 返回上级" read -p "请选择: " SVC_CHOICE case "$SVC_CHOICE" in 1) start_service ;; 2) stop_service ;; 3) restart_service ;; 0) break ;; *) echo "无效选择" ;; esac done } # 主菜单 show_menu() { check_api while true; do clear echo -e "${BLUE}Xray 管理脚本${NC}" echo "------------------------" echo "1. 添加入站" echo "2. 修改配置" echo "3. 查看配置" echo "4. 删除配置" echo "5. 服务管理" echo "6. 查看日志" echo "7. 流量统计" echo "8. 更新" echo "0. 退出" echo "------------------------" read -p "请选择: " CHOICE case "$CHOICE" in 1) add_inbound read -n 1 -s -r -p "按任意键继续..." ;; 2) modify_config read -n 1 -s -r -p "按任意键继续..." ;; 3) view_link read -n 1 -s -r -p "按任意键继续..." ;; 4) delete_inbound read -n 1 -s -r -p "按任意键继续..." ;; 5) service_menu ;; 6) view_log_menu ;; 7) view_stats ;; 8) install_xray read -n 1 -s -r -p "按任意键继续..." ;; 0) exit 0 ;; *) echo "无效选择" sleep 1 ;; esac done } # 入口 if [ $# -gt 0 ]; then case "$1" in install) detect_system install_xray configure_xray setup_service start_service ;; start) start_service ;; stop) stop_service ;; restart) restart_service ;; *) echo "Usage: $0 {install|start|stop|restart}" ;; esac else detect_system show_menu fi