mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 07:56:37 +08:00
Compare commits
22 Commits
2.0.0-beta
...
2.0.9
| Author | SHA1 | Date | |
|---|---|---|---|
| dd58ac67de | |||
| e6226d8069 | |||
| 6e25e1838d | |||
| c0c88a8466 | |||
| 4edbb66224 | |||
| 84e01c7851 | |||
| 761db1844d | |||
| ca76cc29de | |||
| c20a78a196 | |||
| 2053cbec32 | |||
| 687e9c78ad | |||
| 07105309fb | |||
| 31290137fe | |||
| 4a23f3b45a | |||
| 0b2974726c | |||
| 43aebdf8f4 | |||
| 37ea338a35 | |||
| 4063d39d6a | |||
| f6dd3ad657 | |||
| 252c16de29 | |||
| 4fef8eeb32 | |||
| 9146eb0375 |
@@ -1,118 +1,167 @@
|
|||||||
name: Build and Push Images Based on Version
|
name: Build and Push Images
|
||||||
|
|
||||||
# 在这里定义统一版本号
|
|
||||||
env:
|
env:
|
||||||
VERSION: "2.0.0-beta"
|
VERSION: "2.0.7-beta" # 分支推送时使用的默认版本
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- beta
|
- beta
|
||||||
|
tags:
|
||||||
|
- '[0-9]*' # 匹配 2.0.8, 2.0.8-beta 等格式
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-version:
|
check-version:
|
||||||
name: Check Version and Decide Build
|
name: Check Version and Decide Build
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ env.VERSION }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
should_build: ${{ steps.check-tag.outputs.should_build }}
|
should_build: ${{ steps.version.outputs.should_build }}
|
||||||
|
should_build_gost: ${{ steps.version.outputs.should_build_gost }}
|
||||||
|
is_tag: ${{ steps.version.outputs.is_tag }}
|
||||||
|
image_owner: ${{ steps.version.outputs.image_owner }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0 # 获取所有历史和 tags
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Display version
|
- name: Determine version and build strategy
|
||||||
|
id: version
|
||||||
run: |
|
run: |
|
||||||
echo "Current version: ${{ env.VERSION }}"
|
# 镜像 owner 需要小写
|
||||||
|
IMAGE_OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "image_owner=$IMAGE_OWNER" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Check if tag exists
|
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||||
id: check-tag
|
# Tag 触发:直接使用 tag 名作为版本,全量构建
|
||||||
run: |
|
VERSION="${{ github.ref_name }}"
|
||||||
if git rev-parse "${{ env.VERSION }}" >/dev/null 2>&1; then
|
echo "🏷️ Tag trigger detected: $VERSION"
|
||||||
echo "Tag ${{ env.VERSION }} already exists, skip build"
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
echo "is_tag=true" >> $GITHUB_OUTPUT
|
||||||
else
|
|
||||||
echo "Tag ${{ env.VERSION }} does not exist, will build all components"
|
|
||||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "should_build_gost=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
# 分支触发:使用 env.VERSION,检查是否需要构建
|
||||||
|
VERSION="${{ env.VERSION }}"
|
||||||
|
echo "🌿 Branch trigger detected, using version: $VERSION"
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "is_tag=false" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# 检查 tag 是否已存在
|
||||||
|
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
||||||
|
echo "Tag $VERSION already exists"
|
||||||
|
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# 检查 go-gost 目录是否有变化
|
||||||
|
TAG_COMMIT=$(git rev-list -n 1 "$VERSION")
|
||||||
|
if git diff --quiet --ignore-all-space --ignore-blank-lines $TAG_COMMIT HEAD -- go-gost/ 2>/dev/null; then
|
||||||
|
echo "✅ GOST files unchanged since tag"
|
||||||
|
echo "should_build_gost=false" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "🔄 Detected changes in go-gost directory"
|
||||||
|
git diff --stat $TAG_COMMIT HEAD -- go-gost/ || true
|
||||||
|
echo "should_build_gost=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Tag $VERSION does not exist, will build all components"
|
||||||
|
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "should_build_gost=true" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build-gost:
|
build-gost:
|
||||||
name: Build & Compress GOST Binary
|
name: Build & Compress GOST Binary
|
||||||
needs: check-version
|
needs: check-version
|
||||||
if: needs.check-version.outputs.should_build == 'true'
|
if: needs.check-version.outputs.should_build_gost == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.23'
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('go-gost/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
- name: Install UPX
|
- name: Install UPX
|
||||||
run: |
|
run: |
|
||||||
wget https://github.com/upx/upx/releases/download/v4.2.1/upx-4.2.1-amd64_linux.tar.xz
|
wget -q https://github.com/upx/upx/releases/download/v4.2.1/upx-4.2.1-amd64_linux.tar.xz
|
||||||
tar -xf upx-4.2.1-amd64_linux.tar.xz
|
tar -xf upx-4.2.1-amd64_linux.tar.xz
|
||||||
sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin/
|
sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin/
|
||||||
rm -rf upx-4.2.1-amd64_linux*
|
rm -rf upx-4.2.1-amd64_linux*
|
||||||
|
|
||||||
- name: Build GOST binary
|
- name: Build GOST binary (AMD64)
|
||||||
working-directory: ./go-gost
|
working-directory: ./go-gost
|
||||||
run: |
|
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost-amd64
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
|
||||||
|
- name: Build GOST binary (ARM64)
|
||||||
|
working-directory: ./go-gost
|
||||||
|
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o gost-arm64
|
||||||
|
|
||||||
- name: Compress with UPX
|
- name: Compress with UPX
|
||||||
working-directory: ./go-gost
|
working-directory: ./go-gost
|
||||||
run: |
|
run: |
|
||||||
upx --best --lzma gost
|
upx --best --lzma gost-amd64
|
||||||
|
upx --best --lzma gost-arm64
|
||||||
|
|
||||||
- name: Upload GOST artifact
|
- name: Upload GOST AMD64 artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: gost-binary
|
name: gost-binary-amd64
|
||||||
path: ./go-gost/gost
|
path: ./go-gost/gost-amd64
|
||||||
|
|
||||||
|
- name: Upload GOST ARM64 artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gost-binary-arm64
|
||||||
|
path: ./go-gost/gost-arm64
|
||||||
|
|
||||||
build-vite:
|
build-vite:
|
||||||
name: Build & Push Vite Frontend
|
name: Build & Push Vite Frontend
|
||||||
needs: check-version
|
needs: check-version
|
||||||
if: needs.check-version.outputs.should_build == 'true'
|
if: needs.check-version.outputs.should_build == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to DockerHub
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
registry: ${{ env.REGISTRY }}
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Prepare build args
|
||||||
|
run: |
|
||||||
|
echo "VITE_GITHUB_REPO=https://github.com/${{ github.repository }}" > ./vite-frontend/.env.production
|
||||||
|
|
||||||
- name: Build and push Vite Docker images
|
- name: Build and push Vite Docker images
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ needs.check-version.outputs.version }}"
|
VERSION="${{ needs.check-version.outputs.version }}"
|
||||||
|
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||||
|
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--push \
|
--push \
|
||||||
-t bqlpfy/vite-frontend:latest \
|
-t ${{ env.REGISTRY }}/${OWNER}/vite-frontend:latest \
|
||||||
-t bqlpfy/vite-frontend:${VERSION} \
|
-t ${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION} \
|
||||||
./vite-frontend
|
./vite-frontend
|
||||||
|
|
||||||
build-java:
|
build-java:
|
||||||
@@ -120,17 +169,20 @@ jobs:
|
|||||||
needs: check-version
|
needs: check-version
|
||||||
if: needs.check-version.outputs.should_build == 'true'
|
if: needs.check-version.outputs.should_build == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up JDK and Maven
|
- name: Set up JDK and Maven
|
||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
java-version: 21
|
java-version: 21
|
||||||
distribution: 'temurin'
|
distribution: 'temurin'
|
||||||
|
|
||||||
- name: Cache Maven dependencies
|
- name: Cache Maven dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.m2
|
path: ~/.m2
|
||||||
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}
|
||||||
@@ -141,92 +193,176 @@ jobs:
|
|||||||
run: mvn clean package -DskipTests
|
run: mvn clean package -DskipTests
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to DockerHub
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
registry: ${{ env.REGISTRY }}
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Java Docker images
|
- name: Build and push Java Docker images
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ needs.check-version.outputs.version }}"
|
VERSION="${{ needs.check-version.outputs.version }}"
|
||||||
|
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||||
|
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
--push \
|
--push \
|
||||||
-t bqlpfy/springboot-backend:latest \
|
-t ${{ env.REGISTRY }}/${OWNER}/springboot-backend:latest \
|
||||||
-t bqlpfy/springboot-backend:${VERSION} \
|
-t ${{ env.REGISTRY }}/${OWNER}/springboot-backend:${VERSION} \
|
||||||
./springboot-backend
|
./springboot-backend
|
||||||
|
|
||||||
create-release:
|
create-release:
|
||||||
name: Create Release and Tag
|
name: Create Release (Tag Only)
|
||||||
needs: [check-version, build-gost, build-vite, build-java]
|
needs: [check-version, build-gost, build-vite, build-java]
|
||||||
if: needs.check-version.outputs.should_build == 'true'
|
if: needs.check-version.outputs.is_tag == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download GOST binary
|
- name: Download GOST AMD64 binary
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: gost-binary
|
name: gost-binary-amd64
|
||||||
path: ./artifacts
|
path: ./artifacts/amd64
|
||||||
|
|
||||||
|
- name: Download GOST ARM64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gost-binary-arm64
|
||||||
|
path: ./artifacts/arm64
|
||||||
|
|
||||||
|
- name: Prepare release files
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.check-version.outputs.version }}"
|
||||||
|
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||||
|
REPO="${{ github.repository }}"
|
||||||
|
|
||||||
|
# 移动二进制文件
|
||||||
|
mv ./artifacts/amd64/gost-amd64 ./artifacts/gost-amd64
|
||||||
|
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
|
||||||
|
|
||||||
|
# 复制并修改 docker-compose 文件
|
||||||
|
cp docker-compose-v4.yml ./artifacts/docker-compose-v4.yml
|
||||||
|
cp docker-compose-v6.yml ./artifacts/docker-compose-v6.yml
|
||||||
|
|
||||||
|
# 替换镜像地址为 GHCR
|
||||||
|
sed -i "s|bqlpfy/springboot-backend:[^[:space:]]*|${{ env.REGISTRY }}/${OWNER}/springboot-backend:${VERSION}|g" ./artifacts/docker-compose-v4.yml
|
||||||
|
sed -i "s|bqlpfy/vite-frontend:[^[:space:]]*|${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION}|g" ./artifacts/docker-compose-v4.yml
|
||||||
|
sed -i "s|bqlpfy/springboot-backend:[^[:space:]]*|${{ env.REGISTRY }}/${OWNER}/springboot-backend:${VERSION}|g" ./artifacts/docker-compose-v6.yml
|
||||||
|
sed -i "s|bqlpfy/vite-frontend:[^[:space:]]*|${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION}|g" ./artifacts/docker-compose-v6.yml
|
||||||
|
|
||||||
|
# 复制并修改安装脚本
|
||||||
|
cp install.sh ./artifacts/install.sh
|
||||||
|
cp panel_install.sh ./artifacts/panel_install.sh
|
||||||
|
|
||||||
|
# 替换仓库地址和版本号
|
||||||
|
sed -i "s|bqlpfy/flux-panel|${REPO}|g" ./artifacts/install.sh
|
||||||
|
sed -i "s|bqlpfy/flux-panel|${REPO}|g" ./artifacts/panel_install.sh
|
||||||
|
sed -i "s|2.0.7-beta|${VERSION}|g" ./artifacts/install.sh
|
||||||
|
sed -i "s|2.0.7-beta|${VERSION}|g" ./artifacts/panel_install.sh
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ needs.check-version.outputs.version }}"
|
VERSION="${{ needs.check-version.outputs.version }}"
|
||||||
|
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||||
# 创建并推送 tag
|
|
||||||
git tag "${VERSION}" ${{ github.sha }}
|
|
||||||
git push origin "${VERSION}"
|
|
||||||
|
|
||||||
# 获取 commit 信息
|
# 获取 commit 信息
|
||||||
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
|
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
|
||||||
COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an")
|
COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an")
|
||||||
COMMIT_DATE=$(git log -1 --pretty=format:"%ai")
|
COMMIT_DATE=$(git log -1 --pretty=format:"%ai")
|
||||||
|
|
||||||
# 创建 release
|
# 创建 release
|
||||||
gh release create "${VERSION}" \
|
gh release create "${VERSION}" \
|
||||||
--title "Release ${VERSION}" \
|
--title "Release ${VERSION}" \
|
||||||
--notes "Version ${VERSION}
|
--notes "## 📝 Release Information
|
||||||
|
|
||||||
## 📝 Commit Information
|
- **Version**: ${VERSION}
|
||||||
- **Message**: ${COMMIT_MSG}
|
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
|
||||||
- **Author**: ${COMMIT_AUTHOR}
|
- **Author**: ${COMMIT_AUTHOR}
|
||||||
- **Date**: ${COMMIT_DATE}
|
- **Date**: ${COMMIT_DATE}
|
||||||
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
|
- **Message**: ${COMMIT_MSG}
|
||||||
|
|
||||||
## 📦 Components
|
## 📦 Docker Images
|
||||||
- **GOST Binary** (Linux AMD64, compressed with UPX)
|
|
||||||
- **Vite Frontend** Docker Image: \`bqlpfy/vite-frontend:${VERSION}\` / \`bqlpfy/vite-frontend:latest\`
|
\`\`\`bash
|
||||||
- **Spring Boot Backend** Docker Image: \`bqlpfy/springboot-backend:${VERSION}\` / \`bqlpfy/springboot-backend:latest\`
|
# Backend
|
||||||
|
docker pull ${{ env.REGISTRY }}/${OWNER}/springboot-backend:${VERSION}
|
||||||
## 📥 Installation Files
|
|
||||||
- \`install.sh\` - GOST 客户端安装脚本
|
# Frontend
|
||||||
- \`panel_install.sh\` - 面板安装脚本
|
docker pull ${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION}
|
||||||
- \`docker-compose-v4.yml\` - Docker Compose 配置 (IPv4)
|
\`\`\`
|
||||||
- \`docker-compose-v6.yml\` - Docker Compose 配置 (IPv6)
|
|
||||||
|
## 🚀 Quick Install
|
||||||
## ℹ️ Build Details
|
|
||||||
- Build date: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
**Panel:**
|
||||||
- Branch: ${{ github.ref_name }}" \
|
\`\`\`bash
|
||||||
|
curl -L https://github.com/${{ github.repository }}/releases/download/${VERSION}/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**Node:**
|
||||||
|
\`\`\`bash
|
||||||
|
curl -L https://github.com/${{ github.repository }}/releases/download/${VERSION}/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||||
|
\`\`\`" \
|
||||||
--repo ${{ github.repository }}
|
--repo ${{ github.repository }}
|
||||||
|
|
||||||
# 上传所有文件到 release
|
# 上传所有文件到 release
|
||||||
echo "📤 上传 GOST 二进制..."
|
echo "📤 上传 GOST 二进制文件..."
|
||||||
gh release upload "${VERSION}" ./artifacts/gost --clobber
|
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
|
||||||
|
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
|
||||||
|
|
||||||
echo "📤 上传安装脚本..."
|
echo "📤 上传安装脚本..."
|
||||||
gh release upload "${VERSION}" ./install.sh --clobber
|
gh release upload "${VERSION}" ./artifacts/install.sh --clobber
|
||||||
gh release upload "${VERSION}" ./panel_install.sh --clobber
|
gh release upload "${VERSION}" ./artifacts/panel_install.sh --clobber
|
||||||
|
|
||||||
echo "📤 上传 Docker Compose 配置文件..."
|
echo "📤 上传 Docker Compose 配置文件..."
|
||||||
gh release upload "${VERSION}" ./docker-compose-v4.yml --clobber
|
gh release upload "${VERSION}" ./artifacts/docker-compose-v4.yml --clobber
|
||||||
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
|
gh release upload "${VERSION}" ./artifacts/docker-compose-v6.yml --clobber
|
||||||
|
|
||||||
echo "✅ 所有文件已上传到 Release ${VERSION}"
|
echo "✅ Release ${VERSION} 创建完成"
|
||||||
|
|
||||||
|
update-release-gost:
|
||||||
|
name: Update GOST Binaries in Release
|
||||||
|
needs: [check-version, build-gost]
|
||||||
|
if: needs.check-version.outputs.is_tag == 'false' && needs.check-version.outputs.should_build == 'false' && needs.check-version.outputs.should_build_gost == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download GOST AMD64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gost-binary-amd64
|
||||||
|
path: ./artifacts/amd64
|
||||||
|
|
||||||
|
- name: Download GOST ARM64 binary
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: gost-binary-arm64
|
||||||
|
path: ./artifacts/arm64
|
||||||
|
|
||||||
|
- name: Rename binaries
|
||||||
|
run: |
|
||||||
|
mv ./artifacts/amd64/gost-amd64 ./artifacts/gost-amd64
|
||||||
|
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
|
||||||
|
|
||||||
|
- name: Update GOST binaries in Release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.check-version.outputs.version }}"
|
||||||
|
|
||||||
|
echo "🔄 更新 Release ${VERSION} 中的 GOST 二进制文件..."
|
||||||
|
|
||||||
|
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
|
||||||
|
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
|
||||||
|
|
||||||
|
echo "✅ GOST 二进制文件更新完成"
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Plan: 搭建开发环境
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
为 Flux Panel 项目安装所有缺失的开发依赖,使 3 个子项目都能本地开发和构建。
|
||||||
|
|
||||||
|
## 当前状态
|
||||||
|
|
||||||
|
### ✅ 已安装
|
||||||
|
| 工具 | 版本 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| Node.js | v20.19.2 | vite-frontend |
|
||||||
|
| npm | 9.2.0 | vite-frontend |
|
||||||
|
| Go | 1.24.4 | go-gost |
|
||||||
|
| Docker | 29.1.4 | 容器化部署 |
|
||||||
|
|
||||||
|
### ❌ 缺失
|
||||||
|
| 工具 | 需求版本 | 用途 |
|
||||||
|
|------|----------|------|
|
||||||
|
| Java | 21 | springboot-backend |
|
||||||
|
| Maven | 3.x | 构建后端 |
|
||||||
|
| Docker Compose | v2 | 容器编排 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 执行任务
|
||||||
|
|
||||||
|
### Task 1: 安装 Java 21
|
||||||
|
```bash
|
||||||
|
apt-get update && apt-get install -y openjdk-21-jdk
|
||||||
|
```
|
||||||
|
**验证**: `java -version` 应显示 openjdk 21
|
||||||
|
|
||||||
|
### Task 2: 安装 Maven
|
||||||
|
```bash
|
||||||
|
apt-get install -y maven
|
||||||
|
```
|
||||||
|
**验证**: `mvn -v` 应显示 Maven 3.x
|
||||||
|
|
||||||
|
### Task 3: 安装 Docker Compose Plugin
|
||||||
|
```bash
|
||||||
|
apt-get install -y docker-compose-plugin
|
||||||
|
```
|
||||||
|
**验证**: `docker compose version` 应显示版本号
|
||||||
|
|
||||||
|
### Task 4: 安装前端依赖
|
||||||
|
```bash
|
||||||
|
cd /root/flux-panel/vite-frontend && npm install
|
||||||
|
```
|
||||||
|
**验证**: `node_modules/` 目录存在
|
||||||
|
|
||||||
|
### Task 5: 验证后端可构建
|
||||||
|
```bash
|
||||||
|
cd /root/flux-panel/springboot-backend && mvn clean compile -q
|
||||||
|
```
|
||||||
|
**验证**: 编译成功无错误
|
||||||
|
|
||||||
|
### Task 6: 验证 Go 模块
|
||||||
|
```bash
|
||||||
|
cd /root/flux-panel/go-gost && go mod download
|
||||||
|
```
|
||||||
|
**验证**: 依赖下载成功
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完成标准
|
||||||
|
- [ ] `java -version` → openjdk 21
|
||||||
|
- [ ] `mvn -v` → Maven 3.x
|
||||||
|
- [ ] `docker compose version` → v2.x
|
||||||
|
- [ ] 前端: `npm run dev` 可启动
|
||||||
|
- [ ] 后端: `mvn compile` 成功
|
||||||
|
- [ ] Go: `go build .` 成功
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# PROJECT KNOWLEDGE BASE
|
||||||
|
|
||||||
|
**Generated:** Sat Jan 24 2026
|
||||||
|
**Context:** Monorepo for Flux Panel (Traffic Forwarding)
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
Flux Panel is a traffic forwarding management system based on [go-gost](https://github.com/go-gost/gost). It manages tunnels, port forwarding, and user quotas.
|
||||||
|
**Stack:** Monorepo (Java/Spring Boot Backend + React/Vite Frontend + Go/GOST Service).
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
/root/flux-panel/
|
||||||
|
├── springboot-backend/ # Java 21 + Spring Boot 2.7 Admin API
|
||||||
|
├── vite-frontend/ # React 18 + Vite + HeroUI/NextUI
|
||||||
|
├── go-gost/ # Go 1.23 + GOST Extensions (Core logic)
|
||||||
|
├── docker-compose*.yml # Deployment configs (v4/v6)
|
||||||
|
└── *.sh # Install scripts (panel_install.sh, install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
| Task | Location | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| **Admin API** | `springboot-backend/` | Users, quotas, billing logic |
|
||||||
|
| **UI/Dashboard** | `vite-frontend/` | Management console |
|
||||||
|
| **Core Forwarding** | `go-gost/` | GOST implementation & extensions |
|
||||||
|
| **Deploy** | `docker-compose-v4.yml` | Container orchestration |
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
- **Monorepo**: 3 distinct languages/stacks. Treat each subdir as a separate project.
|
||||||
|
- **Docker**: Primary deployment method.
|
||||||
|
- **Scripts**: `panel_install.sh` for panel, `install.sh` for nodes.
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
```bash
|
||||||
|
# Quick Deploy (Panel)
|
||||||
|
./panel_install.sh
|
||||||
|
|
||||||
|
# Quick Deploy (Node)
|
||||||
|
./install.sh
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker-compose -f docker-compose-v4.yml up -d
|
||||||
|
```
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
# flux-panel转发面板 哆啦A梦转发面板
|
# flux-panel转发面板 哆啦A梦转发面板
|
||||||
|
|
||||||
|
|
||||||
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
|
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
|
||||||
---
|
---
|
||||||
## 特性
|
## 特性
|
||||||
@@ -18,13 +16,23 @@
|
|||||||
---
|
---
|
||||||
### Docker Compose部署
|
### Docker Compose部署
|
||||||
#### 快速部署
|
#### 快速部署
|
||||||
面板端:
|
面板端(稳定版):
|
||||||
```bash
|
```bash
|
||||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||||
```
|
```
|
||||||
节点端:
|
节点端(稳定版):
|
||||||
```bash
|
```bash
|
||||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
面板端(开发版):
|
||||||
|
```bash
|
||||||
|
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/beta/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||||
|
```
|
||||||
|
节点端(开发版):
|
||||||
|
```bash
|
||||||
|
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/beta/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -58,17 +66,4 @@ curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/inst
|
|||||||
作者对因使用本项目所造成的任何直接或间接损失概不负责,亦不提供任何形式的担保、承诺或技术支持。
|
作者对因使用本项目所造成的任何直接或间接损失概不负责,亦不提供任何形式的担保、承诺或技术支持。
|
||||||
|
|
||||||
|
|
||||||
请务必在合法、合规、安全的前提下使用本项目。
|
请务必在合法、合规、安全的前提下使用本项目。
|
||||||
|
|
||||||
---
|
|
||||||
## ⭐ 喝杯咖啡!(USDT)
|
|
||||||
|
|
||||||
| 网络 | 地址 |
|
|
||||||
|------------|----------------------------------------------------------------------|
|
|
||||||
| BNB(BEP20) | `0x755492c03728851bbf855daa28a1e089f9aca4d1` |
|
|
||||||
| TRC20 | `TYh2L3xxXpuJhAcBWnt3yiiADiCSJLgUm7` |
|
|
||||||
| Aptos | `0xf2f9fb14749457748506a8281628d556e8540d1eb586d202cd8b02b99d369ef8` |
|
|
||||||
|
|
||||||
[](https://www.star-history.com/#bqlpfy/flux-panel&Date)
|
|
||||||
|
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: bqlpfy/springboot-backend:2.0.0-beta
|
image: bqlpfy/springboot-backend:2.0.7-beta
|
||||||
container_name: springboot-backend
|
container_name: springboot-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -15,6 +15,8 @@ services:
|
|||||||
- sqlite_data:/app/data
|
- sqlite_data:/app/data
|
||||||
networks:
|
networks:
|
||||||
- gost-network
|
- gost-network
|
||||||
|
stop_grace_period: 30s
|
||||||
|
stop_signal: SIGTERM
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -23,7 +25,7 @@ services:
|
|||||||
start_period: 60s
|
start_period: 60s
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: bqlpfy/vite-frontend:2.0.0-beta
|
image: bqlpfy/vite-frontend:2.0.7-beta
|
||||||
container_name: vite-frontend
|
container_name: vite-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
backend:
|
||||||
image: bqlpfy/springboot-backend:2.0.0-beta
|
image: bqlpfy/springboot-backend:2.0.7-beta
|
||||||
container_name: springboot-backend
|
container_name: springboot-backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -15,6 +15,8 @@ services:
|
|||||||
- sqlite_data:/app/data
|
- sqlite_data:/app/data
|
||||||
networks:
|
networks:
|
||||||
- gost-network
|
- gost-network
|
||||||
|
stop_grace_period: 30s
|
||||||
|
stop_signal: SIGTERM
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -23,7 +25,7 @@ services:
|
|||||||
start_period: 60s
|
start_period: 60s
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: bqlpfy/vite-frontend:2.0.0-beta
|
image: bqlpfy/vite-frontend:2.0.7-beta
|
||||||
container_name: vite-frontend
|
container_name: vite-frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# GO-GOST SERVICE KNOWLEDGE BASE
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
Core forwarding service based on GOST v3.
|
||||||
|
**Stack:** Go 1.23, GOST Core v0.3.1, GOST x (Extensions).
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
go-gost/
|
||||||
|
├── main.go # Entry point
|
||||||
|
├── x/ # Local extensions (REPLACES github.com/go-gost/x)
|
||||||
|
│ ├── api/ # Management API
|
||||||
|
│ ├── registry/ # Service registry
|
||||||
|
│ ├── handler/ # Protocol handlers (socks, tunnel, relay)
|
||||||
|
│ └── listener/ # Network listeners (tcp, udp, tun/tap)
|
||||||
|
└── go.mod # Defines local replacement
|
||||||
|
```
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
- **Local Replace**: `go.mod` uses `replace github.com/go-gost/x => ./x`.
|
||||||
|
- **Extensions**: Custom logic lives in `x/`. This is the primary place for modifications.
|
||||||
|
- **Handlers**: Implements SOCKS5, Tunnel, Relay, etc.
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
```bash
|
||||||
|
# Run
|
||||||
|
go run .
|
||||||
|
|
||||||
|
# Build
|
||||||
|
go build .
|
||||||
|
```
|
||||||
+1
-1
@@ -119,7 +119,7 @@ func main() {
|
|||||||
log := xlogger.NewLogger()
|
log := xlogger.NewLogger()
|
||||||
logger.SetDefault(log)
|
logger.SetDefault(log)
|
||||||
|
|
||||||
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "2.0.0")
|
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "2.0.2")
|
||||||
defer wsReporter.Stop()
|
defer wsReporter.Stop()
|
||||||
service.SetHTTPReportURL(config.Addr, config.Secret)
|
service.SetHTTPReportURL(config.Addr, config.Secret)
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-gost/x/config"
|
"github.com/go-gost/x/config"
|
||||||
"github.com/go-gost/x/service"
|
|
||||||
"github.com/go-gost/x/internal/util/crypto"
|
"github.com/go-gost/x/internal/util/crypto"
|
||||||
|
"github.com/go-gost/x/service"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
"github.com/shirou/gopsutil/v3/host"
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
@@ -87,6 +87,9 @@ type TcpPingResponse struct {
|
|||||||
|
|
||||||
type WebSocketReporter struct {
|
type WebSocketReporter struct {
|
||||||
url string
|
url string
|
||||||
|
addr string // 保存服务器地址
|
||||||
|
secret string // 保存密钥
|
||||||
|
version string // 保存版本号
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
reconnectTime time.Duration
|
reconnectTime time.Duration
|
||||||
pingInterval time.Duration
|
pingInterval time.Duration
|
||||||
@@ -195,7 +198,25 @@ func (w *WebSocketReporter) connect() error {
|
|||||||
w.connecting = false
|
w.connecting = false
|
||||||
}()
|
}()
|
||||||
|
|
||||||
u, err := url.Parse(w.url)
|
// 重新读取 config.json 获取最新的协议配置
|
||||||
|
type LocalConfig struct {
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
Http int `json:"http"`
|
||||||
|
Tls int `json:"tls"`
|
||||||
|
Socks int `json:"socks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg LocalConfig
|
||||||
|
if b, err := os.ReadFile("config.json"); err == nil {
|
||||||
|
json.Unmarshal(b, &cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用最新的配置重新构建 URL
|
||||||
|
currentURL := "ws://" + w.addr + "/system-info?type=1&secret=" + w.secret + "&version=" + w.version +
|
||||||
|
"&http=" + strconv.Itoa(cfg.Http) + "&tls=" + strconv.Itoa(cfg.Tls) + "&socks=" + strconv.Itoa(cfg.Socks)
|
||||||
|
|
||||||
|
u, err := url.Parse(currentURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("解析URL失败: %v", err)
|
return fmt.Errorf("解析URL失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -225,7 +246,7 @@ func (w *WebSocketReporter) connect() error {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Printf("✅ WebSocket连接建立成功\n")
|
fmt.Printf("✅ WebSocket连接建立成功 (http=%d, tls=%d, socks=%d)\n", cfg.Http, cfg.Tls, cfg.Socks)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,81 +796,81 @@ func (w *WebSocketReporter) handleDeleteLimiter(data interface{}) error {
|
|||||||
|
|
||||||
// handleSetProtocol 处理设置屏蔽协议的命令
|
// handleSetProtocol 处理设置屏蔽协议的命令
|
||||||
func (w *WebSocketReporter) handleSetProtocol(data interface{}) error {
|
func (w *WebSocketReporter) handleSetProtocol(data interface{}) error {
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("序列化协议设置失败: %v", err)
|
return fmt.Errorf("序列化协议设置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 支持 {"http":0/1, "tls":0/1, "socks":0/1}
|
// 支持 {"http":0/1, "tls":0/1, "socks":0/1}
|
||||||
var req struct {
|
var req struct {
|
||||||
HTTP *int `json:"http"`
|
HTTP *int `json:"http"`
|
||||||
TLS *int `json:"tls"`
|
TLS *int `json:"tls"`
|
||||||
SOCKS *int `json:"socks"`
|
SOCKS *int `json:"socks"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(jsonData, &req); err != nil {
|
if err := json.Unmarshal(jsonData, &req); err != nil {
|
||||||
return fmt.Errorf("解析协议设置失败: %v", err)
|
return fmt.Errorf("解析协议设置失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取当前值作为默认
|
// 读取当前值作为默认
|
||||||
httpVal, tlsVal, socksVal := 0, 0, 0
|
httpVal, tlsVal, socksVal := 0, 0, 0
|
||||||
|
|
||||||
if req.HTTP != nil {
|
if req.HTTP != nil {
|
||||||
if *req.HTTP != 0 && *req.HTTP != 1 {
|
if *req.HTTP != 0 && *req.HTTP != 1 {
|
||||||
return fmt.Errorf("http 取值必须为0或1")
|
return fmt.Errorf("http 取值必须为0或1")
|
||||||
}
|
}
|
||||||
httpVal = *req.HTTP
|
httpVal = *req.HTTP
|
||||||
}
|
}
|
||||||
if req.TLS != nil {
|
if req.TLS != nil {
|
||||||
if *req.TLS != 0 && *req.TLS != 1 {
|
if *req.TLS != 0 && *req.TLS != 1 {
|
||||||
return fmt.Errorf("tls 取值必须为0或1")
|
return fmt.Errorf("tls 取值必须为0或1")
|
||||||
}
|
}
|
||||||
tlsVal = *req.TLS
|
tlsVal = *req.TLS
|
||||||
}
|
}
|
||||||
if req.SOCKS != nil {
|
if req.SOCKS != nil {
|
||||||
if *req.SOCKS != 0 && *req.SOCKS != 1 {
|
if *req.SOCKS != 0 && *req.SOCKS != 1 {
|
||||||
return fmt.Errorf("socks 取值必须为0或1")
|
return fmt.Errorf("socks 取值必须为0或1")
|
||||||
}
|
}
|
||||||
socksVal = *req.SOCKS
|
socksVal = *req.SOCKS
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置至 service,全量传递(未提供的值沿用0)
|
// 设置至 service,全量传递(未提供的值沿用0)
|
||||||
service.SetProtocolBlock(httpVal, tlsVal, socksVal)
|
service.SetProtocolBlock(httpVal, tlsVal, socksVal)
|
||||||
|
|
||||||
// 同步写入本地 config.json
|
// 同步写入本地 config.json
|
||||||
if err := updateLocalConfigJSON(httpVal, tlsVal, socksVal); err != nil {
|
if err := updateLocalConfigJSON(httpVal, tlsVal, socksVal); err != nil {
|
||||||
return fmt.Errorf("写入config.json失败: %v", err)
|
return fmt.Errorf("写入config.json失败: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
|
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
|
||||||
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
|
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
|
||||||
path := "config.json"
|
path := "config.json"
|
||||||
|
|
||||||
// 读取现有配置
|
// 读取现有配置
|
||||||
type LocalConfig struct {
|
type LocalConfig struct {
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
Http int `json:"http"`
|
Http int `json:"http"`
|
||||||
Tls int `json:"tls"`
|
Tls int `json:"tls"`
|
||||||
Socks int `json:"socks"`
|
Socks int `json:"socks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var cfg LocalConfig
|
var cfg LocalConfig
|
||||||
if b, err := os.ReadFile(path); err == nil {
|
if b, err := os.ReadFile(path); err == nil {
|
||||||
_ = json.Unmarshal(b, &cfg)
|
_ = json.Unmarshal(b, &cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.Http = httpVal
|
cfg.Http = httpVal
|
||||||
cfg.Tls = tlsVal
|
cfg.Tls = tlsVal
|
||||||
cfg.Socks = socksVal
|
cfg.Socks = socksVal
|
||||||
|
|
||||||
// 写回
|
// 写回
|
||||||
data, err := json.MarshalIndent(cfg, "", " ")
|
data, err := json.MarshalIndent(cfg, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(path, data, 0644)
|
return os.WriteFile(path, data, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleCall 处理服务端的call回调消息
|
// handleCall 处理服务端的call回调消息
|
||||||
@@ -1021,12 +1042,16 @@ func getMemoryInfo() MemoryInfo {
|
|||||||
// StartWebSocketReporterWithConfig 使用配置字段启动WebSocket报告器
|
// StartWebSocketReporterWithConfig 使用配置字段启动WebSocket报告器
|
||||||
func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls int, socks int, version string) *WebSocketReporter {
|
func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls int, socks int, version string) *WebSocketReporter {
|
||||||
|
|
||||||
// 构建包含本机IP的WebSocket URL
|
// 构建初始 WebSocket URL
|
||||||
fullURL := "ws://" + addr + "/system-info?type=1&secret=" + secret + "&version=" + version + "&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
|
fullURL := "ws://" + addr + "/system-info?type=1&secret=" + secret + "&version=" + version + "&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
|
||||||
|
|
||||||
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
|
||||||
|
|
||||||
reporter := NewWebSocketReporter(fullURL, secret)
|
reporter := NewWebSocketReporter(fullURL, secret)
|
||||||
|
// 保存 addr, secret, version 供重连时使用
|
||||||
|
reporter.addr = addr
|
||||||
|
reporter.secret = secret
|
||||||
|
reporter.version = version
|
||||||
reporter.Start()
|
reporter.Start()
|
||||||
return reporter
|
return reporter
|
||||||
}
|
}
|
||||||
@@ -1107,6 +1132,32 @@ func tcpPingHost(ip string, port int, count int, timeoutMs int) (float64, float6
|
|||||||
|
|
||||||
fmt.Printf("🔍 开始TCP ping测试: %s,次数: %d,超时: %dms\n", target, count, timeoutMs)
|
fmt.Printf("🔍 开始TCP ping测试: %s,次数: %d,超时: %dms\n", target, count, timeoutMs)
|
||||||
|
|
||||||
|
// 如果是域名,先解析一次DNS,避免每次连接都重新解析导致延迟累加
|
||||||
|
if net.ParseIP(ip) == nil {
|
||||||
|
// 是域名,需要解析
|
||||||
|
fmt.Printf("🔍 检测到域名,正在解析DNS...\n")
|
||||||
|
dnsStart := time.Now()
|
||||||
|
|
||||||
|
addrs, err := net.LookupHost(ip)
|
||||||
|
dnsDuration := time.Since(dnsStart)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, 100.0, fmt.Errorf("DNS解析失败: %v", err)
|
||||||
|
}
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
return 0, 100.0, fmt.Errorf("DNS解析未返回任何IP地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ DNS解析完成 (%.2fms),解析到 %d 个IP: %v\n",
|
||||||
|
dnsDuration.Seconds()*1000, len(addrs), addrs)
|
||||||
|
|
||||||
|
// 使用第一个解析到的IP进行测试
|
||||||
|
target = net.JoinHostPort(addrs[0], fmt.Sprintf("%d", port))
|
||||||
|
fmt.Printf("🎯 使用IP地址进行测试: %s\n", target)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("🎯 使用IP地址进行测试: %s\n", target)
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < count; i++ {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -1,7 +1,30 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
|
# 获取系统架构
|
||||||
|
get_architecture() {
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case $ARCH in
|
||||||
|
x86_64)
|
||||||
|
echo "amd64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
echo "arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "amd64" # 默认使用 amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建下载地址
|
||||||
|
build_download_url() {
|
||||||
|
local ARCH=$(get_architecture)
|
||||||
|
echo "https://github.com/Sagit-chu/flux-panel/releases/download/2.0.8/gost-${ARCH}"
|
||||||
|
}
|
||||||
|
|
||||||
# 下载地址
|
# 下载地址
|
||||||
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/gost"
|
DOWNLOAD_URL=$(build_download_url)
|
||||||
INSTALL_DIR="/etc/gost"
|
INSTALL_DIR="/etc/flux_agent"
|
||||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||||
if [ "$COUNTRY" = "CN" ]; then
|
if [ "$COUNTRY" = "CN" ]; then
|
||||||
# 拼接 URL
|
# 拼接 URL
|
||||||
|
|||||||
+11
-3
@@ -8,8 +8,8 @@ export LC_ALL=C
|
|||||||
|
|
||||||
|
|
||||||
# 全局下载地址配置
|
# 全局下载地址配置
|
||||||
DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/docker-compose-v4.yml"
|
DOCKER_COMPOSEV4_URL="https://github.com/Sagit-chu/flux-panel/releases/download/2.0.8/docker-compose-v4.yml"
|
||||||
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/docker-compose-v6.yml"
|
DOCKER_COMPOSEV6_URL="https://github.com/Sagit-chu/flux-panel/releases/download/2.0.8/docker-compose-v6.yml"
|
||||||
|
|
||||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||||
if [ "$COUNTRY" = "CN" ]; then
|
if [ "$COUNTRY" = "CN" ]; then
|
||||||
@@ -234,7 +234,15 @@ update_panel() {
|
|||||||
configure_docker_ipv6
|
configure_docker_ipv6
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🛑 停止当前服务..."
|
# 先发送 SIGTERM 信号,让应用优雅关闭
|
||||||
|
docker stop -t 30 springboot-backend 2>/dev/null || true
|
||||||
|
docker stop -t 10 vite-frontend 2>/dev/null || true
|
||||||
|
|
||||||
|
# 等待 WAL 文件同步
|
||||||
|
echo "⏳ 等待数据同步..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# 然后再完全停止
|
||||||
$DOCKER_CMD down
|
$DOCKER_CMD down
|
||||||
|
|
||||||
echo "⬇️ 拉取最新镜像..."
|
echo "⬇️ 拉取最新镜像..."
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# SPRINGBOOT BACKEND KNOWLEDGE BASE
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
Admin API for Flux Panel. Manages users, licenses, and traffic rules.
|
||||||
|
**Stack:** Java 21, Spring Boot 2.7.18, SQLite, MyBatis Plus.
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
springboot-backend/
|
||||||
|
├── src/main/java/com/admin/
|
||||||
|
│ ├── controller/ # API Endpoints
|
||||||
|
│ ├── entity/ # DB Models (MyBatis Plus)
|
||||||
|
│ ├── mapper/ # Data Access
|
||||||
|
│ ├── service/ # Business Logic
|
||||||
|
│ └── common/ # Utils, DTOs
|
||||||
|
└── src/main/resources/
|
||||||
|
├── application.yml # Config
|
||||||
|
├── mapper/ # XML Mappers
|
||||||
|
├── data.sql # Init data
|
||||||
|
└── bgimages/ # Static resources
|
||||||
|
```
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
- **DB**: SQLite used via `sqlite-jdbc`.
|
||||||
|
- **ORM**: MyBatis Plus + MyBatis Plus Join.
|
||||||
|
- **JSON**: FastJSON2 used for serialization.
|
||||||
|
- **Utils**: Hutool used extensively.
|
||||||
|
- **Auth**: Likely custom or token-based (see `controller` logic).
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
mvn clean package
|
||||||
|
|
||||||
|
# Run
|
||||||
|
java -jar target/admin-0.0.1-SNAPSHOT.jar
|
||||||
|
```
|
||||||
@@ -16,7 +16,7 @@ ENV LC_ALL=en_US.UTF-8
|
|||||||
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||||
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu \
|
&& apt-get install -y --no-install-recommends fontconfig fonts-dejavu sqlite3 \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|||||||
@@ -21,4 +21,6 @@ public class ForwardDto {
|
|||||||
private String remoteAddr;
|
private String remoteAddr;
|
||||||
|
|
||||||
private String strategy;
|
private String strategy;
|
||||||
|
|
||||||
|
private Integer inPort;
|
||||||
}
|
}
|
||||||
@@ -20,9 +20,13 @@ public class ForwardUpdateDto {
|
|||||||
@NotBlank(message = "转发名称不能为空")
|
@NotBlank(message = "转发名称不能为空")
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
private Integer tunnelId;
|
||||||
|
|
||||||
@NotBlank(message = "远程地址不能为空")
|
@NotBlank(message = "远程地址不能为空")
|
||||||
private String remoteAddr;
|
private String remoteAddr;
|
||||||
|
|
||||||
private String strategy;
|
private String strategy;
|
||||||
|
|
||||||
|
private Integer inPort;
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -30,4 +30,13 @@ public class TunnelUpdateDto {
|
|||||||
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
@DecimalMin(value = "0.0", inclusive = false, message = "流量倍率必须大于0.0")
|
||||||
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
@DecimalMax(value = "100.0", message = "流量倍率不能大于100.0")
|
||||||
private BigDecimal trafficRatio;
|
private BigDecimal trafficRatio;
|
||||||
|
|
||||||
|
// 入口节点配置(可选,为空时不更新节点配置)
|
||||||
|
private List<ChainTunnel> inNodeId;
|
||||||
|
|
||||||
|
// 转发链节点配置(二维数组,每一跳可有多个节点)
|
||||||
|
private List<List<ChainTunnel>> chainNodes;
|
||||||
|
|
||||||
|
// 出口节点配置
|
||||||
|
private List<ChainTunnel> outNodeId;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.admin.common.task;
|
package com.admin.common.task;
|
||||||
|
|
||||||
|
import com.admin.common.dto.GostDto;
|
||||||
import com.admin.common.utils.GostUtil;
|
import com.admin.common.utils.GostUtil;
|
||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
import com.admin.service.*;
|
import com.admin.service.*;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.admin.common.utils;
|
package com.admin.common.utils;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.admin.common.dto.GostDto;
|
||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
@@ -12,26 +14,34 @@ import java.util.Objects;
|
|||||||
public class GostUtil {
|
public class GostUtil {
|
||||||
|
|
||||||
|
|
||||||
public static void AddLimiters(Long node_id, Long name, String speed) {
|
public static GostDto AddLimiters(Long node_id, Long name, String speed) {
|
||||||
JSONObject data = createLimiterData(name, speed);
|
JSONObject data = createLimiterData(name, speed);
|
||||||
WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddLimiters");
|
||||||
|
if (gostDto.getMsg().contains("exists")){
|
||||||
|
gostDto.setMsg("OK");
|
||||||
|
}
|
||||||
|
return gostDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UpdateLimiters(Long node_id, Long name, String speed) {
|
public static GostDto UpdateLimiters(Long node_id, Long name, String speed) {
|
||||||
JSONObject data = createLimiterData(name, speed);
|
JSONObject data = createLimiterData(name, speed);
|
||||||
JSONObject req = new JSONObject();
|
JSONObject req = new JSONObject();
|
||||||
req.put("limiter", name + "");
|
req.put("limiter", name + "");
|
||||||
req.put("data", data);
|
req.put("data", data);
|
||||||
WebSocketServer.send_msg(node_id, req, "UpdateLimiters");
|
return WebSocketServer.send_msg(node_id, req, "UpdateLimiters");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteLimiters(Long node_id, Long name) {
|
public static GostDto DeleteLimiters(Long node_id, Long name) {
|
||||||
JSONObject req = new JSONObject();
|
JSONObject req = new JSONObject();
|
||||||
req.put("limiter", name + "");
|
req.put("limiter", name + "");
|
||||||
WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
GostDto gostDto = WebSocketServer.send_msg(node_id, req, "DeleteLimiters");
|
||||||
|
if (gostDto.getMsg().contains("not found")){
|
||||||
|
gostDto.setMsg("OK");
|
||||||
|
}
|
||||||
|
return gostDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddChains(Long node_id, List<ChainTunnel> chainTunnels, Map<Long, Node> node_s) {
|
public static GostDto AddChains(Long node_id, List<ChainTunnel> chainTunnels, Map<Long, Node> node_s) {
|
||||||
JSONArray nodes = new JSONArray();
|
JSONArray nodes = new JSONArray();
|
||||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||||
JSONObject dialer = new JSONObject();
|
JSONObject dialer = new JSONObject();
|
||||||
@@ -43,19 +53,23 @@ public class GostUtil {
|
|||||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||||
JSONObject node = new JSONObject();
|
JSONObject node = new JSONObject();
|
||||||
node.put("name", "node_" + chainTunnel.getInx());
|
node.put("name", "node_" + chainTunnel.getInx());
|
||||||
node.put("addr", node_info.getServerIp() + ":" + chainTunnel.getPort());
|
node.put("addr", processServerAddress(node_info.getServerIp() + ":" + chainTunnel.getPort()));
|
||||||
node.put("connector", connector);
|
node.put("connector", connector);
|
||||||
node.put("dialer", dialer);
|
node.put("dialer", dialer);
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(node_info.getInterfaceName())) {
|
|
||||||
node.put("interface", node_info.getInterfaceName());
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes.add(node);
|
nodes.add(node);
|
||||||
}
|
}
|
||||||
JSONObject hop = new JSONObject();
|
JSONObject hop = new JSONObject();
|
||||||
hop.put("name", "hop_" + chainTunnels.getFirst().getTunnelId());
|
hop.put("name", "hop_" + chainTunnels.getFirst().getTunnelId());
|
||||||
|
|
||||||
|
// interface设置在转发链
|
||||||
|
if (StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||||
|
hop.put("interface", node_s.get(node_id).getInterfaceName());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
JSONObject selector = new JSONObject();
|
JSONObject selector = new JSONObject();
|
||||||
selector.put("strategy", chainTunnels.getFirst().getStrategy());
|
selector.put("strategy", chainTunnels.getFirst().getStrategy());
|
||||||
selector.put("maxFails", 1);
|
selector.put("maxFails", 1);
|
||||||
@@ -72,24 +86,34 @@ public class GostUtil {
|
|||||||
data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId());
|
data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId());
|
||||||
data.put("hops", hops);
|
data.put("hops", hops);
|
||||||
|
|
||||||
WebSocketServer.send_msg(node_id, data, "AddChains");
|
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "AddChains");
|
||||||
|
if (gostDto.getMsg().contains("exists")){
|
||||||
|
gostDto.setMsg("OK");
|
||||||
|
}
|
||||||
|
return gostDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteChains(Long node_id, String name) {
|
public static GostDto DeleteChains(Long node_id, String name) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
data.put("chain", name);
|
data.put("chain", name);
|
||||||
WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteChains");
|
||||||
|
if (gostDto.getMsg().contains("not found")){
|
||||||
|
gostDto.setMsg("OK");
|
||||||
|
}
|
||||||
|
return gostDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddChainService(Long node_id, ChainTunnel chainTunnel, Map<Long, Node> node_s) {
|
public static GostDto AddChainService(Long node_id, ChainTunnel chainTunnel, Map<Long, Node> node_s) {
|
||||||
JSONArray services = new JSONArray();
|
JSONArray services = new JSONArray();
|
||||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||||
JSONObject service_item = new JSONObject();
|
JSONObject service_item = new JSONObject();
|
||||||
service_item.put("name", chainTunnel.getTunnelId() + "_tls");
|
service_item.put("name", chainTunnel.getTunnelId() + "_tls");
|
||||||
service_item.put("addr", node_info.getTcpListenAddr() + ":" + chainTunnel.getPort());
|
service_item.put("addr", node_info.getTcpListenAddr() + ":" + chainTunnel.getPort());
|
||||||
if (StringUtils.isNotBlank(node_info.getInterfaceName())) {
|
|
||||||
|
// 只为出口节点(chainType=3)设置 interface
|
||||||
|
if (chainTunnel.getChainType() == 3 && StringUtils.isNotBlank(node_s.get(node_id).getInterfaceName())) {
|
||||||
JSONObject metadata = new JSONObject();
|
JSONObject metadata = new JSONObject();
|
||||||
metadata.put("interface", node_info.getInterfaceName());
|
metadata.put("interface", node_s.get(node_id).getInterfaceName());
|
||||||
service_item.put("metadata", metadata);
|
service_item.put("metadata", metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,16 +130,14 @@ public class GostUtil {
|
|||||||
|
|
||||||
services.add(service_item);
|
services.add(service_item);
|
||||||
|
|
||||||
WebSocketServer.send_msg(node_id, services, "AddService");
|
GostDto gostDto = WebSocketServer.send_msg(node_id, services, "AddService");
|
||||||
|
if (gostDto.getMsg().contains("exists")){
|
||||||
|
gostDto.setMsg("OK");
|
||||||
|
}
|
||||||
|
return gostDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteChainService(Long node_id, JSONArray services) {
|
public static GostDto AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) {
|
||||||
JSONObject data = new JSONObject();
|
|
||||||
data.put("services", services);
|
|
||||||
WebSocketServer.send_msg(node_id, data, "DeleteService");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) {
|
|
||||||
JSONArray services = new JSONArray();
|
JSONArray services = new JSONArray();
|
||||||
String[] protocols = {"tcp", "udp"};
|
String[] protocols = {"tcp", "udp"};
|
||||||
for (String protocol : protocols) {
|
for (String protocol : protocols) {
|
||||||
@@ -127,7 +149,8 @@ public class GostUtil {
|
|||||||
service.put("addr", node.getUdpListenAddr() + ":" + forwardPort.getPort());
|
service.put("addr", node.getUdpListenAddr() + ":" + forwardPort.getPort());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(node.getInterfaceName())) {
|
// 只在端口转发时设置 interface(隧道转发时 interface 在转发链的节点上设置)
|
||||||
|
if (tunnel.getType() == 1 && StringUtils.isNotBlank(node.getInterfaceName())) {
|
||||||
JSONObject metadata = new JSONObject();
|
JSONObject metadata = new JSONObject();
|
||||||
metadata.put("interface", node.getInterfaceName());
|
metadata.put("interface", node.getInterfaceName());
|
||||||
service.put("metadata", metadata);
|
service.put("metadata", metadata);
|
||||||
@@ -155,22 +178,30 @@ public class GostUtil {
|
|||||||
|
|
||||||
services.add(service);
|
services.add(service);
|
||||||
}
|
}
|
||||||
WebSocketServer.send_msg(node.getId(), services, meth);
|
GostDto gostDto = WebSocketServer.send_msg(node.getId(), services, meth);
|
||||||
|
if (gostDto.getMsg().contains("exists")){
|
||||||
|
gostDto.setMsg("OK");
|
||||||
|
}
|
||||||
|
return gostDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void DeleteService(Long node_id, JSONArray services) {
|
public static GostDto DeleteService(Long node_id, JSONArray services) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
data.put("services", services);
|
data.put("services", services);
|
||||||
WebSocketServer.send_msg(node_id, data, "DeleteService");
|
GostDto gostDto = WebSocketServer.send_msg(node_id, data, "DeleteService");
|
||||||
|
if (gostDto.getMsg().contains("not found")){
|
||||||
|
gostDto.setMsg("OK");
|
||||||
|
}
|
||||||
|
return gostDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void PauseAndResumeService(Long node_id, String name, String meth) {
|
public static GostDto PauseAndResumeService(Long node_id, String name, String meth) {
|
||||||
JSONObject data = new JSONObject();
|
JSONObject data = new JSONObject();
|
||||||
JSONArray services = new JSONArray();
|
JSONArray services = new JSONArray();
|
||||||
services.add(name + "_tcp");
|
services.add(name + "_tcp");
|
||||||
services.add(name + "_udp");
|
services.add(name + "_udp");
|
||||||
data.put("services", services);
|
data.put("services", services);
|
||||||
WebSocketServer.send_msg(node_id, data, meth);
|
return WebSocketServer.send_msg(node_id, data, meth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -208,7 +239,7 @@ public class GostUtil {
|
|||||||
num++;
|
num++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strategy == null || strategy.equals("")) {
|
if (strategy == null || strategy.isEmpty()) {
|
||||||
strategy = "fifo";
|
strategy = "fifo";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,5 +253,42 @@ public class GostUtil {
|
|||||||
return forwarder;
|
return forwarder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String processServerAddress(String serverAddr) {
|
||||||
|
if (StrUtil.isBlank(serverAddr)) {
|
||||||
|
return serverAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经被方括号包裹,直接返回
|
||||||
|
if (serverAddr.startsWith("[")) {
|
||||||
|
return serverAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找最后一个冒号,分离主机和端口
|
||||||
|
int lastColonIndex = serverAddr.lastIndexOf(':');
|
||||||
|
if (lastColonIndex == -1) {
|
||||||
|
// 没有端口号,直接检查是否需要包裹
|
||||||
|
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
String host = serverAddr.substring(0, lastColonIndex);
|
||||||
|
String port = serverAddr.substring(lastColonIndex);
|
||||||
|
|
||||||
|
// 检查主机部分是否为IPv6地址
|
||||||
|
if (isIPv6Address(host)) {
|
||||||
|
return "[" + host + "]" + port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isIPv6Address(String address) {
|
||||||
|
// IPv6地址包含多个冒号,至少2个
|
||||||
|
if (!address.contains(":")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算冒号数量,IPv6地址至少有2个冒号
|
||||||
|
long colonCount = address.chars().filter(ch -> ch == ':').count();
|
||||||
|
return colonCount >= 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ public class WebSocketServer extends TextWebSocketHandler {
|
|||||||
String socks = (String) session.getAttributes().get("socks");
|
String socks = (String) session.getAttributes().get("socks");
|
||||||
|
|
||||||
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
|
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
|
||||||
|
log.info("屏蔽协议数据:http->{}, tls->{}, socks->{}", http, tls, socks);
|
||||||
|
|
||||||
// 检查是否已有该节点的连接,如果有则记录日志但直接覆盖
|
// 检查是否已有该节点的连接,如果有则记录日志但直接覆盖
|
||||||
WebSocketSession existingSession = nodeSessions.get(nodeId);
|
WebSocketSession existingSession = nodeSessions.get(nodeId);
|
||||||
|
|||||||
@@ -3,8 +3,11 @@ package com.admin.config;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.ApplicationArguments;
|
import org.springframework.boot.ApplicationArguments;
|
||||||
import org.springframework.boot.ApplicationRunner;
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import java.sql.Statement;
|
import java.sql.Statement;
|
||||||
@@ -12,9 +15,11 @@ import java.sql.Statement;
|
|||||||
/**
|
/**
|
||||||
* SQLite 数据库配置
|
* SQLite 数据库配置
|
||||||
* 启用 WAL (Write-Ahead Logging) 模式以提高并发性能
|
* 启用 WAL (Write-Ahead Logging) 模式以提高并发性能
|
||||||
|
* 添加定期 checkpoint 和优雅关闭处理
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@EnableScheduling
|
||||||
public class SQLiteConfig implements ApplicationRunner {
|
public class SQLiteConfig implements ApplicationRunner {
|
||||||
|
|
||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
@@ -33,10 +38,46 @@ public class SQLiteConfig implements ApplicationRunner {
|
|||||||
statement.execute("PRAGMA cache_size=-64000;"); // 64MB 缓存
|
statement.execute("PRAGMA cache_size=-64000;"); // 64MB 缓存
|
||||||
statement.execute("PRAGMA temp_store=MEMORY;");
|
statement.execute("PRAGMA temp_store=MEMORY;");
|
||||||
statement.execute("PRAGMA busy_timeout=5000;"); // 5秒超时
|
statement.execute("PRAGMA busy_timeout=5000;"); // 5秒超时
|
||||||
|
statement.execute("PRAGMA wal_autocheckpoint=1000;"); // 每1000页自动checkpoint
|
||||||
|
|
||||||
|
log.info("SQLite WAL mode configured successfully");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Failed to configure SQLite database", e);
|
log.error("Failed to configure SQLite database", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定期执行 checkpoint,确保 WAL 文件内容写入主数据库
|
||||||
|
* 每5分钟执行一次
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 300000, initialDelay = 300000)
|
||||||
|
public void performCheckpoint() {
|
||||||
|
try (Connection connection = dataSource.getConnection();
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
|
||||||
|
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||||
|
log.debug("SQLite WAL checkpoint completed");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to perform SQLite checkpoint", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用关闭前执行最终的 checkpoint,确保所有数据都写入主数据库文件
|
||||||
|
*/
|
||||||
|
@PreDestroy
|
||||||
|
public void onShutdown() {
|
||||||
|
log.info("Performing final SQLite checkpoint before shutdown...");
|
||||||
|
try (Connection connection = dataSource.getConnection();
|
||||||
|
Statement statement = connection.createStatement()) {
|
||||||
|
|
||||||
|
// 强制执行 checkpoint,将所有 WAL 内容写入主数据库
|
||||||
|
statement.execute("PRAGMA wal_checkpoint(TRUNCATE);");
|
||||||
|
log.info("Final SQLite checkpoint completed successfully");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to perform final SQLite checkpoint", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import com.admin.common.task.CheckGostConfigAsync;
|
|||||||
import com.admin.common.utils.AESCrypto;
|
import com.admin.common.utils.AESCrypto;
|
||||||
import com.admin.common.utils.GostUtil;
|
import com.admin.common.utils.GostUtil;
|
||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
|
import com.admin.service.ChainTunnelService;
|
||||||
import com.alibaba.fastjson.JSON;
|
import com.alibaba.fastjson.JSON;
|
||||||
import com.alibaba.fastjson.JSONArray;
|
import com.alibaba.fastjson.JSONArray;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@@ -64,6 +66,10 @@ public class FlowController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
CheckGostConfigAsync checkGostConfigAsync;
|
CheckGostConfigAsync checkGostConfigAsync;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
@Lazy
|
||||||
|
ChainTunnelService chainTunnelService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加密消息包装器
|
* 加密消息包装器
|
||||||
*/
|
*/
|
||||||
@@ -306,17 +312,14 @@ public class FlowController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void pauseService(List<Forward> forwardList, String name) {
|
public void pauseService(List<Forward> forwardList, String name) {
|
||||||
// for (Forward forward : forwardList) {
|
for (Forward forward : forwardList) {
|
||||||
// Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", forward.getTunnelId()).eq("chain_type", 1));
|
||||||
// if (tunnel != null) {
|
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||||
// GostUtil.PauseService(tunnel.getInNodeId(), name);
|
GostUtil.PauseAndResumeService(chainTunnel.getNodeId(), name, "PauseService");
|
||||||
// if (tunnel.getType() == 2) {
|
}
|
||||||
// GostUtil.PauseRemoteService(tunnel.getOutNodeId(), name);
|
forward.setStatus(0);
|
||||||
// }
|
forwardService.updateById(forward);
|
||||||
// }
|
}
|
||||||
// forward.setStatus(0);
|
|
||||||
// forwardService.updateById(forward);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateForwardFlow(String forwardId, FlowDto flowStats) {
|
private void updateForwardFlow(String forwardId, FlowDto flowStats) {
|
||||||
|
|||||||
@@ -56,6 +56,4 @@ public interface TunnelService extends IService<Tunnel> {
|
|||||||
* @return 诊断结果
|
* @return 诊断结果
|
||||||
*/
|
*/
|
||||||
R diagnoseTunnel(Long tunnelId);
|
R diagnoseTunnel(Long tunnelId);
|
||||||
|
|
||||||
Integer getNodePort(Long nodeId, Integer type);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.springframework.beans.BeanUtils;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -82,53 +83,60 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
// 判断是否使用隧道的inIp
|
// 判断是否使用隧道的inIp
|
||||||
boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty();
|
boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty();
|
||||||
|
|
||||||
// 收集所有的IP列表
|
Set<String> ipPortSet = new LinkedHashSet<>();
|
||||||
List<String> ipList = new ArrayList<>();
|
|
||||||
// 收集所有的端口列表
|
|
||||||
List<Integer> portList = new ArrayList<>();
|
|
||||||
|
|
||||||
if (useTunnelInIp) {
|
if (useTunnelInIp) {
|
||||||
// 使用隧道的inIp
|
// 使用隧道的inIp(求笛卡尔积)
|
||||||
|
List<String> ipList = new ArrayList<>();
|
||||||
|
List<Integer> portList = new ArrayList<>();
|
||||||
|
|
||||||
String[] tunnelInIps = tunnel.getInIp().split(",");
|
String[] tunnelInIps = tunnel.getInIp().split(",");
|
||||||
for (String ip : tunnelInIps) {
|
for (String ip : tunnelInIps) {
|
||||||
if (ip != null && !ip.trim().isEmpty()) {
|
if (ip != null && !ip.trim().isEmpty()) {
|
||||||
ipList.add(ip.trim());
|
ipList.add(ip.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 使用节点的serverIp
|
// 收集所有端口
|
||||||
for (ForwardPort forwardPort : forwardPorts) {
|
for (ForwardPort forwardPort : forwardPorts) {
|
||||||
Node node = nodeService.getById(forwardPort.getNodeId());
|
if (forwardPort.getPort() != null) {
|
||||||
if (node != null && node.getServerIp() != null) {
|
portList.add(forwardPort.getPort());
|
||||||
ipList.add(node.getServerIp());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 去重
|
||||||
// 收集所有端口
|
List<String> uniqueIps = ipList.stream().distinct().toList();
|
||||||
for (ForwardPort forwardPort : forwardPorts) {
|
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
||||||
if (forwardPort.getPort() != null) {
|
|
||||||
portList.add(forwardPort.getPort());
|
// 组合 IP:Port(笛卡尔积)
|
||||||
|
for (String ip : uniqueIps) {
|
||||||
|
for (Integer port : uniquePorts) {
|
||||||
|
ipPortSet.add(ip + ":" + port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inPort设置为第一个端口(用于向后兼容)
|
||||||
|
if (!uniquePorts.isEmpty()) {
|
||||||
|
forward.setInPort(uniquePorts.getFirst());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用节点的serverIp(一对一,不求笛卡尔积)
|
||||||
|
for (ForwardPort forwardPort : forwardPorts) {
|
||||||
|
Node node = nodeService.getById(forwardPort.getNodeId());
|
||||||
|
if (node != null && node.getServerIp() != null && forwardPort.getPort() != null) {
|
||||||
|
ipPortSet.add(node.getServerIp() + ":" + forwardPort.getPort());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// inPort设置为第一个端口(用于向后兼容)
|
||||||
|
if (!forwardPorts.isEmpty() && forwardPorts.getFirst().getPort() != null) {
|
||||||
|
forward.setInPort(forwardPorts.getFirst().getPort());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 去重
|
// 设置入口IP
|
||||||
List<String> uniqueIps = ipList.stream().distinct().toList();
|
|
||||||
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
|
||||||
|
|
||||||
// 组合 IP:Port(笛卡尔积)
|
|
||||||
Set<String> ipPortSet = new LinkedHashSet<>();
|
|
||||||
for (String ip : uniqueIps) {
|
|
||||||
for (Integer port : uniquePorts) {
|
|
||||||
ipPortSet.add(ip + ":" + port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置入口IP和端口
|
|
||||||
if (!ipPortSet.isEmpty()) {
|
if (!ipPortSet.isEmpty()) {
|
||||||
forward.setInIp(String.join(",", ipPortSet));
|
forward.setInIp(String.join(",", ipPortSet));
|
||||||
// inPort设置为第一个端口(用于向后兼容)
|
|
||||||
forward.setInPort(uniquePorts.getFirst());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,89 +167,229 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
forward.setUserName(currentUser.getUserName());
|
forward.setUserName(currentUser.getUserName());
|
||||||
forward.setCreatedTime(System.currentTimeMillis());
|
forward.setCreatedTime(System.currentTimeMillis());
|
||||||
forward.setUpdatedTime(System.currentTimeMillis());
|
forward.setUpdatedTime(System.currentTimeMillis());
|
||||||
|
List<JSONObject> success = new ArrayList<>();
|
||||||
|
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1));
|
||||||
|
chainTunnels = get_port(chainTunnels, forwardDto.getInPort(), 0L);
|
||||||
this.save(forward);
|
this.save(forward);
|
||||||
|
|
||||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1));
|
|
||||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||||
Integer nodePort = tunnelService.getNodePort(chainTunnel.getNodeId(), 2);
|
|
||||||
|
|
||||||
ForwardPort forwardPort = new ForwardPort();
|
ForwardPort forwardPort = new ForwardPort();
|
||||||
forwardPort.setForwardId(forward.getId());
|
forwardPort.setForwardId(forward.getId());
|
||||||
forwardPort.setNodeId(chainTunnel.getNodeId());
|
forwardPort.setNodeId(chainTunnel.getNodeId());
|
||||||
forwardPort.setPort(nodePort);
|
forwardPort.setPort(chainTunnel.getPort());
|
||||||
forwardPortService.save(forwardPort);
|
forwardPortService.save(forwardPort);
|
||||||
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), permissionResult.getUserTunnel());
|
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), permissionResult.getUserTunnel());
|
||||||
Integer limiter = permissionResult.getLimiter();
|
Integer limiter = permissionResult.getLimiter();
|
||||||
|
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node == null){
|
if (node == null) {
|
||||||
return R.err("部分节点不存在");
|
return R.err("部分节点不存在");
|
||||||
}
|
}
|
||||||
GostUtil.AddAndUpdateService(serviceName, limiter, node, forward, forwardPort, tunnel, "AddService");
|
GostDto gostDto = GostUtil.AddAndUpdateService(serviceName, limiter, node, forward, forwardPort, tunnel, "AddService");
|
||||||
|
if (Objects.equals(gostDto.getMsg(), "OK")) {
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", node.getId());
|
||||||
|
data.put("name", serviceName);
|
||||||
|
success.add(data);
|
||||||
|
} else {
|
||||||
|
this.removeById(forward.getId());
|
||||||
|
forwardPortService.remove(new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId()));
|
||||||
|
for (JSONObject jsonObject : success) {
|
||||||
|
JSONArray se = new JSONArray();
|
||||||
|
se.add(jsonObject.getString("name") + "_tcp");
|
||||||
|
se.add(jsonObject.getString("name") + "_udp");
|
||||||
|
GostUtil.DeleteService(jsonObject.getLong("node_id"), se);
|
||||||
|
return R.err(gostDto.getMsg());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public R updateForward(ForwardUpdateDto forwardUpdateDto) {
|
public R updateForward(ForwardUpdateDto forwardUpdateDto) {
|
||||||
// 1. 获取当前用户信息
|
|
||||||
UserInfo currentUser = getCurrentUserInfo();
|
UserInfo currentUser = getCurrentUserInfo();
|
||||||
|
|
||||||
|
|
||||||
// 2. 检查转发是否存在
|
|
||||||
Forward existForward = validateForwardExists(forwardUpdateDto.getId(), currentUser);
|
Forward existForward = validateForwardExists(forwardUpdateDto.getId(), currentUser);
|
||||||
if (existForward == null) {
|
if (existForward == null) {
|
||||||
return R.err("转发不存在");
|
return R.err("转发不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Integer oldTunnelId = existForward.getTunnelId();
|
||||||
|
Integer newTunnelId = forwardUpdateDto.getTunnelId() != null ? forwardUpdateDto.getTunnelId() : oldTunnelId;
|
||||||
|
boolean tunnelChanged = !Objects.equals(oldTunnelId, newTunnelId);
|
||||||
|
|
||||||
Tunnel tunnel = validateTunnel(existForward.getTunnelId());
|
Tunnel oldTunnel = validateTunnel(oldTunnelId);
|
||||||
if (tunnel == null) {
|
if (oldTunnel == null) {
|
||||||
return R.err("隧道不存在");
|
return R.err("原隧道不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
UserPermissionResult permissionResult = checkUserPermissions(currentUser, tunnel, null);
|
UserTunnel oldUserTunnel;
|
||||||
if (permissionResult.isHasError()) {
|
|
||||||
return R.err(permissionResult.getErrorMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
UserTunnel userTunnel = null;
|
|
||||||
if (currentUser.getRoleId() != 0) {
|
if (currentUser.getRoleId() != 0) {
|
||||||
userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue());
|
oldUserTunnel = getUserTunnel(currentUser.getUserId(), oldTunnelId);
|
||||||
if (userTunnel == null) {
|
if (oldUserTunnel == null) {
|
||||||
return R.err("你没有该隧道权限");
|
return R.err("你没有原隧道权限");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 管理员用户也需要获取UserTunnel(如果存在的话),用于构建正确的服务名称
|
oldUserTunnel = getUserTunnel(existForward.getUserId(), oldTunnelId);
|
||||||
// 通过forward记录获取原始的用户ID
|
}
|
||||||
userTunnel = getUserTunnel(existForward.getUserId(), tunnel.getId().intValue());
|
|
||||||
|
if (tunnelChanged) {
|
||||||
|
Tunnel newTunnel = validateTunnel(newTunnelId);
|
||||||
|
if (newTunnel == null) {
|
||||||
|
return R.err("新隧道不存在");
|
||||||
|
}
|
||||||
|
if (newTunnel.getStatus() != 1) {
|
||||||
|
return R.err("新隧道已禁用,无法切换");
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPermissionResult newPermResult = checkUserPermissions(currentUser, newTunnel, null);
|
||||||
|
if (newPermResult.isHasError()) {
|
||||||
|
return R.err(newPermResult.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
UserTunnel newUserTunnel;
|
||||||
|
if (currentUser.getRoleId() != 0) {
|
||||||
|
newUserTunnel = getUserTunnel(currentUser.getUserId(), newTunnelId);
|
||||||
|
if (newUserTunnel == null) {
|
||||||
|
return R.err("你没有新隧道权限");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newUserTunnel = getUserTunnel(existForward.getUserId(), newTunnelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseOldTunnelResources(existForward, oldTunnel, oldUserTunnel);
|
||||||
|
|
||||||
|
existForward.setTunnelId(newTunnelId);
|
||||||
|
existForward.setRemoteAddr(forwardUpdateDto.getRemoteAddr());
|
||||||
|
existForward.setName(forwardUpdateDto.getName());
|
||||||
|
existForward.setStrategy(forwardUpdateDto.getStrategy());
|
||||||
|
existForward.setStatus(1);
|
||||||
|
existForward.setUpdatedTime(System.currentTimeMillis());
|
||||||
|
this.updateById(existForward);
|
||||||
|
|
||||||
|
R allocResult = allocateNewTunnelResources(existForward, newTunnel, forwardUpdateDto.getInPort(), newPermResult, newUserTunnel);
|
||||||
|
if (allocResult.getCode() != 0) {
|
||||||
|
throw new RuntimeException(allocResult.getMsg());
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPermissionResult permissionResult = checkUserPermissions(currentUser, oldTunnel, null);
|
||||||
|
if (permissionResult.isHasError()) {
|
||||||
|
return R.err(permissionResult.getErrorMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
existForward.setRemoteAddr(forwardUpdateDto.getRemoteAddr());
|
existForward.setRemoteAddr(forwardUpdateDto.getRemoteAddr());
|
||||||
existForward.setName(forwardUpdateDto.getName());
|
existForward.setName(forwardUpdateDto.getName());
|
||||||
existForward.setStrategy(forwardUpdateDto.getStrategy());
|
existForward.setStrategy(forwardUpdateDto.getStrategy());
|
||||||
existForward.setStatus(1);
|
existForward.setStatus(1);
|
||||||
|
existForward.setUpdatedTime(System.currentTimeMillis());
|
||||||
this.updateById(existForward);
|
this.updateById(existForward);
|
||||||
|
|
||||||
|
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", oldTunnel.getId()).eq("chain_type", 1));
|
||||||
|
chainTunnels = get_port(chainTunnels, forwardUpdateDto.getInPort(), existForward.getId());
|
||||||
|
|
||||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1));
|
|
||||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||||
String serviceName = buildServiceName(existForward.getId(), existForward.getUserId(), userTunnel);
|
String serviceName = buildServiceName(existForward.getId(), existForward.getUserId(), oldUserTunnel);
|
||||||
Integer limiter = permissionResult.getLimiter();
|
Integer limiter = permissionResult.getLimiter();
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node == null){
|
if (node == null) {
|
||||||
return R.err("部分节点不存在");
|
return R.err("部分节点不存在");
|
||||||
}
|
}
|
||||||
ForwardPort forwardPort = forwardPortService.getOne(new QueryWrapper<ForwardPort>().eq("forward_id", existForward.getId()).eq("node_id", node.getId()));
|
ForwardPort forwardPort = forwardPortService.getOne(new QueryWrapper<ForwardPort>().eq("forward_id", existForward.getId()).eq("node_id", node.getId()));
|
||||||
if (forwardPort == null){
|
if (forwardPort == null) {
|
||||||
return R.err("部分节点不存在1");
|
return R.err("部分节点不存在1");
|
||||||
}
|
}
|
||||||
GostUtil.AddAndUpdateService(serviceName, limiter, node, existForward, forwardPort, tunnel, "UpdateService");
|
forwardPort.setPort(chainTunnel.getPort());
|
||||||
|
forwardPortService.updateById(forwardPort);
|
||||||
|
GostDto gostDto = GostUtil.AddAndUpdateService(serviceName, limiter, node, existForward, forwardPort, oldTunnel, "UpdateService");
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK")) return R.err(gostDto.getMsg());
|
||||||
}
|
}
|
||||||
|
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void releaseOldTunnelResources(Forward forward, Tunnel oldTunnel, UserTunnel oldUserTunnel) {
|
||||||
|
List<ChainTunnel> oldInNodes = chainTunnelService.list(
|
||||||
|
new QueryWrapper<ChainTunnel>()
|
||||||
|
.eq("tunnel_id", oldTunnel.getId())
|
||||||
|
.eq("chain_type", 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (ChainTunnel chainTunnel : oldInNodes) {
|
||||||
|
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), oldUserTunnel);
|
||||||
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
|
if (node != null) {
|
||||||
|
JSONArray services = new JSONArray();
|
||||||
|
services.add(serviceName + "_tcp");
|
||||||
|
services.add(serviceName + "_udp");
|
||||||
|
GostUtil.DeleteService(node.getId(), services);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forwardPortService.remove(new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private R allocateNewTunnelResources(Forward forward, Tunnel newTunnel, Integer requestedPort,
|
||||||
|
UserPermissionResult permResult, UserTunnel newUserTunnel) {
|
||||||
|
List<ChainTunnel> newInNodes = chainTunnelService.list(
|
||||||
|
new QueryWrapper<ChainTunnel>()
|
||||||
|
.eq("tunnel_id", newTunnel.getId())
|
||||||
|
.eq("chain_type", 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
newInNodes = get_port(newInNodes, requestedPort, forward.getId());
|
||||||
|
|
||||||
|
List<JSONObject> successServices = new ArrayList<>();
|
||||||
|
|
||||||
|
for (ChainTunnel chainTunnel : newInNodes) {
|
||||||
|
ForwardPort forwardPort = new ForwardPort();
|
||||||
|
forwardPort.setForwardId(forward.getId());
|
||||||
|
forwardPort.setNodeId(chainTunnel.getNodeId());
|
||||||
|
forwardPort.setPort(chainTunnel.getPort());
|
||||||
|
forwardPortService.save(forwardPort);
|
||||||
|
|
||||||
|
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), newUserTunnel);
|
||||||
|
Integer limiter = permResult.getLimiter();
|
||||||
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
|
|
||||||
|
if (node == null) {
|
||||||
|
rollbackCreatedServices(successServices);
|
||||||
|
forwardPortService.remove(new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId()));
|
||||||
|
return R.err("新隧道部分节点不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
GostDto gostDto = GostUtil.AddAndUpdateService(serviceName, limiter, node, forward, forwardPort, newTunnel, "AddService");
|
||||||
|
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK")) {
|
||||||
|
rollbackCreatedServices(successServices);
|
||||||
|
forwardPortService.remove(new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId()));
|
||||||
|
return R.err("在新隧道创建服务失败: " + gostDto.getMsg());
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", node.getId());
|
||||||
|
data.put("name", serviceName);
|
||||||
|
successServices.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rollbackCreatedServices(List<JSONObject> created) {
|
||||||
|
for (JSONObject jsonObject : created) {
|
||||||
|
JSONArray se = new JSONArray();
|
||||||
|
se.add(jsonObject.getString("name") + "_tcp");
|
||||||
|
se.add(jsonObject.getString("name") + "_udp");
|
||||||
|
GostUtil.DeleteService(jsonObject.getLong("node_id"), se);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R deleteForward(Long id) {
|
public R deleteForward(Long id) {
|
||||||
|
|
||||||
@@ -282,7 +430,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
|
|
||||||
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel);
|
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel);
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node == null){
|
if (node == null) {
|
||||||
return R.err("部分节点不存在");
|
return R.err("部分节点不存在");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +446,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public R pauseForward(Long id) {
|
public R pauseForward(Long id) {
|
||||||
return changeForwardStatus(id, 0, "PauseService");
|
return changeForwardStatus(id, 0, "PauseService");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -393,7 +541,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
// 1. 入口->第一跳(或出口)
|
// 1. 入口->第一跳(或出口)
|
||||||
for (ChainTunnel inNode : inNodes) {
|
for (ChainTunnel inNode : inNodes) {
|
||||||
Node fromNode = nodeService.getById(inNode.getNodeId());
|
Node fromNode = nodeService.getById(inNode.getNodeId());
|
||||||
|
|
||||||
if (fromNode != null) {
|
if (fromNode != null) {
|
||||||
if (!chainNodesList.isEmpty()) {
|
if (!chainNodesList.isEmpty()) {
|
||||||
for (ChainTunnel firstChainNode : chainNodesList.getFirst()) {
|
for (ChainTunnel firstChainNode : chainNodesList.getFirst()) {
|
||||||
@@ -429,10 +577,10 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
// 2. 链路测试
|
// 2. 链路测试
|
||||||
for (int i = 0; i < chainNodesList.size(); i++) {
|
for (int i = 0; i < chainNodesList.size(); i++) {
|
||||||
List<ChainTunnel> currentHop = chainNodesList.get(i);
|
List<ChainTunnel> currentHop = chainNodesList.get(i);
|
||||||
|
|
||||||
for (ChainTunnel currentNode : currentHop) {
|
for (ChainTunnel currentNode : currentHop) {
|
||||||
Node fromNode = nodeService.getById(currentNode.getNodeId());
|
Node fromNode = nodeService.getById(currentNode.getNodeId());
|
||||||
|
|
||||||
if (fromNode != null) {
|
if (fromNode != null) {
|
||||||
if (i + 1 < chainNodesList.size()) {
|
if (i + 1 < chainNodesList.size()) {
|
||||||
for (ChainTunnel nextNode : chainNodesList.get(i + 1)) {
|
for (ChainTunnel nextNode : chainNodesList.get(i + 1)) {
|
||||||
@@ -500,65 +648,57 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
public R updateForwardOrder(Map<String, Object> params) {
|
public R updateForwardOrder(Map<String, Object> params) {
|
||||||
try {
|
// 1. 获取当前用户信息
|
||||||
// 1. 获取当前用户信息
|
UserInfo currentUser = getCurrentUserInfo();
|
||||||
UserInfo currentUser = getCurrentUserInfo();
|
|
||||||
|
|
||||||
// 2. 验证参数
|
// 2. 验证参数
|
||||||
if (!params.containsKey("forwards")) {
|
if (!params.containsKey("forwards")) {
|
||||||
return R.err("缺少forwards参数");
|
return R.err("缺少forwards参数");
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
List<Map<String, Object>> forwardsList = (List<Map<String, Object>>) params.get("forwards");
|
|
||||||
if (forwardsList == null || forwardsList.isEmpty()) {
|
|
||||||
return R.err("forwards参数不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 验证用户权限(只能更新自己的转发)
|
|
||||||
if (currentUser.getRoleId() != 0) {
|
|
||||||
// 普通用户只能更新自己的转发
|
|
||||||
List<Long> forwardIds = forwardsList.stream()
|
|
||||||
.map(item -> Long.valueOf(item.get("id").toString()))
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 检查所有转发是否属于当前用户
|
|
||||||
QueryWrapper<Forward> queryWrapper = new QueryWrapper<>();
|
|
||||||
queryWrapper.in("id", forwardIds);
|
|
||||||
queryWrapper.eq("user_id", currentUser.getUserId());
|
|
||||||
|
|
||||||
long count = this.count(queryWrapper);
|
|
||||||
if (count != forwardIds.size()) {
|
|
||||||
return R.err("只能更新自己的转发排序");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 批量更新排序
|
|
||||||
List<Forward> forwardsToUpdate = new ArrayList<>();
|
|
||||||
for (Map<String, Object> forwardData : forwardsList) {
|
|
||||||
Long id = Long.valueOf(forwardData.get("id").toString());
|
|
||||||
Integer inx = Integer.valueOf(forwardData.get("inx").toString());
|
|
||||||
|
|
||||||
Forward forward = new Forward();
|
|
||||||
forward.setId(id);
|
|
||||||
forward.setInx(inx);
|
|
||||||
forwardsToUpdate.add(forward);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 执行批量更新
|
|
||||||
boolean success = this.updateBatchById(forwardsToUpdate);
|
|
||||||
if (success) {
|
|
||||||
log.info("用户 {} 更新了 {} 个转发的排序", currentUser.getUserName(), forwardsToUpdate.size());
|
|
||||||
return R.ok("排序更新成功");
|
|
||||||
} else {
|
|
||||||
return R.err("排序更新失败");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("更新转发排序失败", e);
|
|
||||||
return R.err("更新排序时发生错误: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<Map<String, Object>> forwardsList = (List<Map<String, Object>>) params.get("forwards");
|
||||||
|
if (forwardsList == null || forwardsList.isEmpty()) {
|
||||||
|
return R.err("forwards参数不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 验证用户权限(只能更新自己的转发)
|
||||||
|
if (currentUser.getRoleId() != 0) {
|
||||||
|
// 普通用户只能更新自己的转发
|
||||||
|
List<Long> forwardIds = forwardsList.stream()
|
||||||
|
.map(item -> Long.valueOf(item.get("id").toString()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 检查所有转发是否属于当前用户
|
||||||
|
QueryWrapper<Forward> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.in("id", forwardIds);
|
||||||
|
queryWrapper.eq("user_id", currentUser.getUserId());
|
||||||
|
|
||||||
|
long count = this.count(queryWrapper);
|
||||||
|
if (count != forwardIds.size()) {
|
||||||
|
return R.err("只能更新自己的转发排序");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 批量更新排序
|
||||||
|
List<Forward> forwardsToUpdate = new ArrayList<>();
|
||||||
|
for (Map<String, Object> forwardData : forwardsList) {
|
||||||
|
Long id = Long.valueOf(forwardData.get("id").toString());
|
||||||
|
Integer inx = Integer.valueOf(forwardData.get("inx").toString());
|
||||||
|
|
||||||
|
Forward forward = new Forward();
|
||||||
|
forward.setId(id);
|
||||||
|
forward.setInx(inx);
|
||||||
|
forwardsToUpdate.add(forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 执行批量更新
|
||||||
|
this.updateBatchById(forwardsToUpdate);
|
||||||
|
return R.ok();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -614,10 +754,11 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||||
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel);
|
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel);
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node == null){
|
if (node == null) {
|
||||||
return R.err("部分节点不存在");
|
return R.err("部分节点不存在");
|
||||||
}
|
}
|
||||||
GostUtil.PauseAndResumeService(node.getId(), serviceName, gostMethod);
|
GostDto gostDto = GostUtil.PauseAndResumeService(node.getId(), serviceName, gostMethod);
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK")) return R.err(gostDto.getMsg());
|
||||||
}
|
}
|
||||||
forward.setStatus(targetStatus);
|
forward.setStatus(targetStatus);
|
||||||
forward.setUpdatedTime(System.currentTimeMillis());
|
forward.setUpdatedTime(System.currentTimeMillis());
|
||||||
@@ -916,6 +1057,94 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
return forwardId + "_" + userId + "_" + userTunnelId;
|
return forwardId + "_" + userId + "_" + userTunnelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public List<ChainTunnel> get_port(List<ChainTunnel> chainTunnelList, Integer in_port, Long forward_id) {
|
||||||
|
List<List<Integer>> list = new ArrayList<>();
|
||||||
|
|
||||||
|
// 获取每个节点的端口列表
|
||||||
|
for (ChainTunnel tunnel : chainTunnelList) {
|
||||||
|
List<Integer> nodePort = getNodePort(tunnel.getNodeId(), forward_id);
|
||||||
|
if (nodePort.isEmpty()) {
|
||||||
|
throw new RuntimeException("暂无可用端口");
|
||||||
|
}
|
||||||
|
list.add(nodePort);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 如果指定了 in_port,优先检查公有 ==========
|
||||||
|
if (in_port != null) {
|
||||||
|
for (List<Integer> ports : list) {
|
||||||
|
if (!ports.contains(in_port)) {
|
||||||
|
throw new RuntimeException("指定端口 " + in_port + " 不可用(并非所有节点都有此端口)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有节点都有该端口 设置回 ChainTunnel
|
||||||
|
for (ChainTunnel tunnel : chainTunnelList) {
|
||||||
|
tunnel.setPort(in_port);
|
||||||
|
}
|
||||||
|
return chainTunnelList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 未指定 in_port 查找最小的共同端口 ==========
|
||||||
|
Set<Integer> intersection = new HashSet<>(list.getFirst());
|
||||||
|
for (int i = 1; i < list.size(); i++) {
|
||||||
|
intersection.retainAll(list.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!intersection.isEmpty()) {
|
||||||
|
// 找最小端口
|
||||||
|
Integer commonMin = intersection.stream().min(Integer::compareTo).orElseThrow();
|
||||||
|
|
||||||
|
// 设置到所有节点
|
||||||
|
for (ChainTunnel tunnel : chainTunnelList) {
|
||||||
|
tunnel.setPort(commonMin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chainTunnelList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 没有共同端口取各自第一个可用端口 ==========
|
||||||
|
for (int i = 0; i < chainTunnelList.size(); i++) {
|
||||||
|
List<Integer> ports = list.get(i);
|
||||||
|
Integer first = ports.getFirst();
|
||||||
|
chainTunnelList.get(i).setPort(first);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chainTunnelList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Integer> getNodePort(Long nodeId, Long forward_id) {
|
||||||
|
|
||||||
|
Node node = nodeService.getById(nodeId);
|
||||||
|
if (node == null) {
|
||||||
|
throw new RuntimeException("节点不存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 查询隧道转发链占用的端口
|
||||||
|
List<ChainTunnel> chainTunnels = chainTunnelService.list(
|
||||||
|
new QueryWrapper<ChainTunnel>().eq("node_id", nodeId)
|
||||||
|
);
|
||||||
|
Set<Integer> usedPorts = chainTunnels.stream()
|
||||||
|
.map(ChainTunnel::getPort)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
|
||||||
|
List<ForwardPort> list = forwardPortService.list(new QueryWrapper<ForwardPort>().eq("node_id", nodeId).ne("forward_id", forward_id));
|
||||||
|
Set<Integer> forwardUsedPorts = new HashSet<>();
|
||||||
|
for (ForwardPort forwardPort : list) {
|
||||||
|
forwardUsedPorts.add(forwardPort.getPort());
|
||||||
|
}
|
||||||
|
usedPorts.addAll(forwardUsedPorts);
|
||||||
|
|
||||||
|
List<Integer> parsedPorts = TunnelServiceImpl.parsePorts(node.getPort());
|
||||||
|
return parsedPorts.stream()
|
||||||
|
.filter(p -> !usedPorts.contains(p))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ========== 内部数据类 ==========
|
// ========== 内部数据类 ==========
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -966,7 +1195,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
|||||||
private double averageTime;
|
private double averageTime;
|
||||||
private double packetLoss;
|
private double packetLoss;
|
||||||
private long timestamp;
|
private long timestamp;
|
||||||
|
|
||||||
// 链路类型相关字段
|
// 链路类型相关字段
|
||||||
private Integer fromChainType; // 1: 入口, 2: 链, 3: 出口
|
private Integer fromChainType; // 1: 入口, 2: 链, 3: 出口
|
||||||
private Integer fromInx;
|
private Integer fromInx;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.admin.common.dto.GostDto;
|
|||||||
import com.admin.common.dto.NodeDto;
|
import com.admin.common.dto.NodeDto;
|
||||||
import com.admin.common.dto.NodeUpdateDto;
|
import com.admin.common.dto.NodeUpdateDto;
|
||||||
import com.admin.common.lang.R;
|
import com.admin.common.lang.R;
|
||||||
|
import com.admin.common.utils.GostUtil;
|
||||||
import com.admin.common.utils.WebSocketServer;
|
import com.admin.common.utils.WebSocketServer;
|
||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
import com.admin.mapper.NodeMapper;
|
import com.admin.mapper.NodeMapper;
|
||||||
@@ -125,9 +126,9 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
|||||||
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "ip"));
|
ViteConfig viteConfig = viteConfigService.getOne(new QueryWrapper<ViteConfig>().eq("name", "ip"));
|
||||||
if (viteConfig == null) return R.err("请先前往网站配置中设置ip");
|
if (viteConfig == null) return R.err("请先前往网站配置中设置ip");
|
||||||
StringBuilder command = new StringBuilder();
|
StringBuilder command = new StringBuilder();
|
||||||
command.append("curl -L https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/install.sh")
|
command.append("curl -L https://github.com/bqlpfy/flux-panel/releases/download/2.0.7-beta/install.sh")
|
||||||
.append(" -o ./install.sh && chmod +x ./install.sh && ");
|
.append(" -o ./install.sh && chmod +x ./install.sh && ");
|
||||||
String processedServerAddr = processServerAddress(viteConfig.getValue());
|
String processedServerAddr = GostUtil.processServerAddress(viteConfig.getValue());
|
||||||
command.append("./install.sh")
|
command.append("./install.sh")
|
||||||
.append(" -a ").append(processedServerAddr) // 服务器地址
|
.append(" -a ").append(processedServerAddr) // 服务器地址
|
||||||
.append(" -s ").append(node.getSecret()); // 节点密钥
|
.append(" -s ").append(node.getSecret()); // 节点密钥
|
||||||
@@ -182,44 +183,6 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private String processServerAddress(String serverAddr) {
|
|
||||||
if (StrUtil.isBlank(serverAddr)) {
|
|
||||||
return serverAddr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已经被方括号包裹,直接返回
|
|
||||||
if (serverAddr.startsWith("[")) {
|
|
||||||
return serverAddr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查找最后一个冒号,分离主机和端口
|
|
||||||
int lastColonIndex = serverAddr.lastIndexOf(':');
|
|
||||||
if (lastColonIndex == -1) {
|
|
||||||
// 没有端口号,直接检查是否需要包裹
|
|
||||||
return isIPv6Address(serverAddr) ? "[" + serverAddr + "]" : serverAddr;
|
|
||||||
}
|
|
||||||
|
|
||||||
String host = serverAddr.substring(0, lastColonIndex);
|
|
||||||
String port = serverAddr.substring(lastColonIndex);
|
|
||||||
|
|
||||||
// 检查主机部分是否为IPv6地址
|
|
||||||
if (isIPv6Address(host)) {
|
|
||||||
return "[" + host + "]" + port;
|
|
||||||
}
|
|
||||||
|
|
||||||
return serverAddr;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean isIPv6Address(String address) {
|
|
||||||
// IPv6地址包含多个冒号,至少2个
|
|
||||||
if (!address.contains(":")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算冒号数量,IPv6地址至少有2个冒号
|
|
||||||
long colonCount = address.chars().filter(ch -> ch == ':').count();
|
|
||||||
return colonCount >= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.admin.common.utils.GostUtil;
|
|||||||
import com.admin.entity.*;
|
import com.admin.entity.*;
|
||||||
import com.admin.mapper.SpeedLimitMapper;
|
import com.admin.mapper.SpeedLimitMapper;
|
||||||
import com.admin.service.*;
|
import com.admin.service.*;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -19,6 +20,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -65,11 +67,23 @@ public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLi
|
|||||||
|
|
||||||
String speedInMBps = convertBitsToMBps(speedLimit.getSpeed());
|
String speedInMBps = convertBitsToMBps(speedLimit.getSpeed());
|
||||||
|
|
||||||
|
List<Long> limit_success = new ArrayList<>();
|
||||||
|
|
||||||
List<ChainTunnel> tunnelList = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", speedLimit.getTunnelId()));
|
List<ChainTunnel> tunnelList = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", speedLimit.getTunnelId()));
|
||||||
for (ChainTunnel chainTunnel : tunnelList) {
|
for (ChainTunnel chainTunnel : tunnelList) {
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
GostUtil.AddLimiters(node.getId(),speedLimit.getId(),speedInMBps);
|
GostDto gostDto = GostUtil.AddLimiters(node.getId(), speedLimit.getId(), speedInMBps);
|
||||||
|
if (Objects.equals(gostDto.getMsg(), "OK")){
|
||||||
|
limit_success.add(node.getId());
|
||||||
|
}else {
|
||||||
|
this.removeById(speedLimit.getId());
|
||||||
|
for (Long node_id : limit_success) {
|
||||||
|
GostDto deleteLimiters = GostUtil.DeleteLimiters(node_id, speedLimit.getId());
|
||||||
|
System.out.println(deleteLimiters);
|
||||||
|
}
|
||||||
|
return R.err(gostDto.getMsg());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return R.ok();
|
return R.ok();
|
||||||
@@ -94,7 +108,8 @@ public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLi
|
|||||||
for (ChainTunnel chainTunnel : tunnelList) {
|
for (ChainTunnel chainTunnel : tunnelList) {
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
GostUtil.UpdateLimiters(node.getId(),speedLimit.getId(),speedInMBps);
|
GostDto gostDto = GostUtil.UpdateLimiters(node.getId(), speedLimit.getId(), speedInMBps);
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK")) return R.err(gostDto.getMsg());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.updateById(speedLimit);
|
this.updateById(speedLimit);
|
||||||
@@ -116,7 +131,8 @@ public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLi
|
|||||||
for (ChainTunnel chainTunnel : tunnelList) {
|
for (ChainTunnel chainTunnel : tunnelList) {
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node != null) {
|
if (node != null) {
|
||||||
GostUtil.DeleteLimiters(node.getId(),speedLimit.getId());
|
GostDto gostDto = GostUtil.DeleteLimiters(node.getId(), speedLimit.getId());
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK"))return R.err(gostDto.getMsg());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.removeById(id);
|
this.removeById(id);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
Node node = nodeService.getById(chain_node.getNodeId());
|
Node node = nodeService.getById(chain_node.getNodeId());
|
||||||
if (node == null) return R.err("节点不存在");
|
if (node == null) return R.err("节点不存在");
|
||||||
nodes.put(node.getId(), node);
|
nodes.put(node.getId(), node);
|
||||||
Integer nodePort = getNodePort(chain_node.getNodeId(), 1);
|
Integer nodePort = getNodePort(chain_node.getNodeId());
|
||||||
chain_node.setPort(nodePort);
|
chain_node.setPort(nodePort);
|
||||||
chain_node.setInx(inx); // 设置转发链序号
|
chain_node.setInx(inx); // 设置转发链序号
|
||||||
chainTunnels.add(chain_node);
|
chainTunnels.add(chain_node);
|
||||||
@@ -93,7 +93,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
Node node = nodeService.getById(out_node.getNodeId());
|
Node node = nodeService.getById(out_node.getNodeId());
|
||||||
if (node == null) return R.err("节点不存在");
|
if (node == null) return R.err("节点不存在");
|
||||||
nodes.put(node.getId(), node);
|
nodes.put(node.getId(), node);
|
||||||
Integer nodePort = getNodePort(out_node.getNodeId(), 1);
|
Integer nodePort = getNodePort(out_node.getNodeId());
|
||||||
out_node.setPort(nodePort);
|
out_node.setPort(nodePort);
|
||||||
chainTunnels.add(out_node);
|
chainTunnels.add(out_node);
|
||||||
}
|
}
|
||||||
@@ -132,14 +132,35 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
}
|
}
|
||||||
chainTunnelService.saveBatch(chainTunnels);
|
chainTunnelService.saveBatch(chainTunnels);
|
||||||
|
|
||||||
|
List<JSONObject> chain_success = new ArrayList<>();
|
||||||
|
List<JSONObject> service_success = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (tunnel.getType() == 2) {
|
if (tunnel.getType() == 2) {
|
||||||
|
|
||||||
for (ChainTunnel in_node : tunnelDto.getInNodeId()) {
|
for (ChainTunnel in_node : tunnelDto.getInNodeId()) {
|
||||||
// 创建Chain, 指向chainNode的第一跳。如果chainNode为空就是指向出口
|
// 创建Chain, 指向chainNode的第一跳。如果chainNode为空就是指向出口
|
||||||
if (tunnelDto.getChainNodes().isEmpty()) { // 指向出口
|
if (tunnelDto.getChainNodes().isEmpty()) { // 指向出口
|
||||||
GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getOutNodeId(), nodes);
|
GostDto gostDto = GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getOutNodeId(), nodes);
|
||||||
|
isError(gostDto);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getChainNodes().getFirst(), nodes);// 指向第一跳
|
GostDto gostDto = GostUtil.AddChains(in_node.getNodeId(), tunnelDto.getChainNodes().getFirst(), nodes);// 指向第一跳
|
||||||
|
if (Objects.equals(gostDto.getMsg(), "OK")){
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", in_node.getNodeId());
|
||||||
|
data.put("name", "chains_" + tunnel.getId());
|
||||||
|
chain_success.add(data);
|
||||||
|
}else {
|
||||||
|
this.removeById(tunnel.getId());
|
||||||
|
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()));
|
||||||
|
for (JSONObject chainSuccess : chain_success) {
|
||||||
|
GostDto deleteChains = GostUtil.DeleteChains(chainSuccess.getLong("node_id"), chainSuccess.getString("name"));
|
||||||
|
System.out.println(deleteChains);
|
||||||
|
}
|
||||||
|
return R.err(gostDto.getMsg());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,19 +170,79 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
for (ChainTunnel chainTunnel : chainTunnels1) {
|
for (ChainTunnel chainTunnel : chainTunnels1) {
|
||||||
int inx = i+1;
|
int inx = i+1;
|
||||||
if (inx >= tunnelDto.getChainNodes().size()) { // 指向出口
|
if (inx >= tunnelDto.getChainNodes().size()) { // 指向出口
|
||||||
GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getOutNodeId(), nodes);
|
GostDto gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getOutNodeId(), nodes);
|
||||||
|
if (Objects.equals(gostDto.getMsg(), "OK")){
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", chainTunnel.getNodeId());
|
||||||
|
data.put("name", "chains_" + tunnel.getId());
|
||||||
|
chain_success.add(data);
|
||||||
|
}else {
|
||||||
|
this.removeById(tunnel.getId());
|
||||||
|
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()));
|
||||||
|
for (JSONObject chainSuccess : chain_success) {
|
||||||
|
GostDto deleteChains = GostUtil.DeleteChains(chainSuccess.getLong("node_id"), chainSuccess.getString("name"));
|
||||||
|
System.out.println(deleteChains);
|
||||||
|
}
|
||||||
|
return R.err(gostDto.getMsg());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getChainNodes().get(inx), nodes);
|
GostDto gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), tunnelDto.getChainNodes().get(inx), nodes);
|
||||||
|
if (Objects.equals(gostDto.getMsg(), "OK")){
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", chainTunnel.getNodeId());
|
||||||
|
data.put("name", "chains_" + tunnel.getId());
|
||||||
|
chain_success.add(data);
|
||||||
|
}else {
|
||||||
|
this.removeById(tunnel.getId());
|
||||||
|
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()));
|
||||||
|
for (JSONObject chainSuccess : chain_success) {
|
||||||
|
GostDto deleteChains = GostUtil.DeleteChains(chainSuccess.getLong("node_id"), chainSuccess.getString("name"));
|
||||||
|
System.out.println(deleteChains);
|
||||||
|
}
|
||||||
|
return R.err(gostDto.getMsg());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GostUtil.AddChainService(chainTunnel.getNodeId(), chainTunnel, nodes);
|
GostDto gostDto = GostUtil.AddChainService(chainTunnel.getNodeId(), chainTunnel, nodes);
|
||||||
|
if (Objects.equals(gostDto.getMsg(), "OK")){
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", chainTunnel.getNodeId());
|
||||||
|
data.put("name", tunnel.getId() + "_tls");
|
||||||
|
service_success.add(data);
|
||||||
|
}else {
|
||||||
|
this.removeById(tunnel.getId());
|
||||||
|
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()));
|
||||||
|
for (JSONObject serviceSuccess : service_success) {
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
jsonArray.add(serviceSuccess.getString("name"));
|
||||||
|
GostDto deleteService = GostUtil.DeleteService(serviceSuccess.getLong("node_id"), jsonArray);
|
||||||
|
System.out.println(deleteService);
|
||||||
|
}
|
||||||
|
return R.err(gostDto.getMsg());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
for (ChainTunnel out_node : tunnelDto.getOutNodeId()) {
|
for (ChainTunnel out_node : tunnelDto.getOutNodeId()) {
|
||||||
GostUtil.AddChainService(out_node.getNodeId(), out_node, nodes);
|
GostDto gostDto = GostUtil.AddChainService(out_node.getNodeId(), out_node, nodes);
|
||||||
|
if (Objects.equals(gostDto.getMsg(), "OK")){
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", out_node.getNodeId());
|
||||||
|
data.put("name", tunnel.getId() + "_tls");
|
||||||
|
service_success.add(data);
|
||||||
|
}else {
|
||||||
|
this.removeById(tunnel.getId());
|
||||||
|
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()));
|
||||||
|
for (JSONObject serviceSuccess : service_success) {
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
jsonArray.add(serviceSuccess.getString("name"));
|
||||||
|
GostDto deleteService = GostUtil.DeleteService(serviceSuccess.getLong("node_id"), jsonArray);
|
||||||
|
System.out.println(deleteService);
|
||||||
|
}
|
||||||
|
return R.err(gostDto.getMsg());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -241,6 +322,104 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
public R updateTunnel(TunnelUpdateDto tunnelUpdateDto) {
|
public R updateTunnel(TunnelUpdateDto tunnelUpdateDto) {
|
||||||
Tunnel existingTunnel = this.getById(tunnelUpdateDto.getId());
|
Tunnel existingTunnel = this.getById(tunnelUpdateDto.getId());
|
||||||
if (existingTunnel == null) return R.err("隧道不存在");
|
if (existingTunnel == null) return R.err("隧道不存在");
|
||||||
|
|
||||||
|
List<ChainTunnel> oldChainTunnels = chainTunnelService.list(
|
||||||
|
new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnelUpdateDto.getId())
|
||||||
|
);
|
||||||
|
|
||||||
|
boolean hasNodeChanges = detectNodeChanges(oldChainTunnels, tunnelUpdateDto);
|
||||||
|
|
||||||
|
if (hasNodeChanges && tunnelUpdateDto.getInNodeId() != null) {
|
||||||
|
List<ChainTunnel> backupChains = deepCopyChainTunnels(oldChainTunnels);
|
||||||
|
|
||||||
|
List<Long> nodeIds = new ArrayList<>();
|
||||||
|
Map<Long, Node> nodes = new HashMap<>();
|
||||||
|
|
||||||
|
for (ChainTunnel inNode : tunnelUpdateDto.getInNodeId()) {
|
||||||
|
nodeIds.add(inNode.getNodeId());
|
||||||
|
Node node = nodeService.getById(inNode.getNodeId());
|
||||||
|
if (node == null) return R.err("入口节点不存在: " + inNode.getNodeId());
|
||||||
|
if (node.getStatus() != 1) return R.err("入口节点不在线: " + node.getName());
|
||||||
|
nodes.put(node.getId(), node);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChainTunnel> newChainTunnels = new ArrayList<>();
|
||||||
|
for (ChainTunnel inNode : tunnelUpdateDto.getInNodeId()) {
|
||||||
|
inNode.setTunnelId(existingTunnel.getId());
|
||||||
|
inNode.setChainType(1);
|
||||||
|
newChainTunnels.add(inNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingTunnel.getType() == 2) {
|
||||||
|
if (tunnelUpdateDto.getOutNodeId() == null || tunnelUpdateDto.getOutNodeId().isEmpty()) {
|
||||||
|
return R.err("隧道转发类型必须配置出口节点");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<ChainTunnel>> chainNodes = tunnelUpdateDto.getChainNodes() == null ?
|
||||||
|
new ArrayList<>() : tunnelUpdateDto.getChainNodes();
|
||||||
|
|
||||||
|
int inx = 1;
|
||||||
|
for (List<ChainTunnel> hop : chainNodes) {
|
||||||
|
for (ChainTunnel chainNode : hop) {
|
||||||
|
nodeIds.add(chainNode.getNodeId());
|
||||||
|
Node node = nodeService.getById(chainNode.getNodeId());
|
||||||
|
if (node == null) return R.err("转发链节点不存在: " + chainNode.getNodeId());
|
||||||
|
if (node.getStatus() != 1) return R.err("转发链节点不在线: " + node.getName());
|
||||||
|
nodes.put(node.getId(), node);
|
||||||
|
|
||||||
|
Integer port = getNodePort(chainNode.getNodeId());
|
||||||
|
chainNode.setPort(port);
|
||||||
|
chainNode.setInx(inx);
|
||||||
|
chainNode.setChainType(2);
|
||||||
|
chainNode.setTunnelId(existingTunnel.getId());
|
||||||
|
newChainTunnels.add(chainNode);
|
||||||
|
}
|
||||||
|
inx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ChainTunnel outNode : tunnelUpdateDto.getOutNodeId()) {
|
||||||
|
nodeIds.add(outNode.getNodeId());
|
||||||
|
Node node = nodeService.getById(outNode.getNodeId());
|
||||||
|
if (node == null) return R.err("出口节点不存在: " + outNode.getNodeId());
|
||||||
|
if (node.getStatus() != 1) return R.err("出口节点不在线: " + node.getName());
|
||||||
|
nodes.put(node.getId(), node);
|
||||||
|
|
||||||
|
Integer port = getNodePort(outNode.getNodeId());
|
||||||
|
outNode.setPort(port);
|
||||||
|
outNode.setChainType(3);
|
||||||
|
outNode.setTunnelId(existingTunnel.getId());
|
||||||
|
newChainTunnels.add(outNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> nodeIdSet = new HashSet<>(nodeIds);
|
||||||
|
if (nodeIdSet.size() != nodeIds.size()) {
|
||||||
|
return R.err("节点配置重复");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cleanupGostConfig(oldChainTunnels, existingTunnel.getId());
|
||||||
|
|
||||||
|
chainTunnelService.remove(
|
||||||
|
new QueryWrapper<ChainTunnel>().eq("tunnel_id", existingTunnel.getId())
|
||||||
|
);
|
||||||
|
|
||||||
|
R applyResult = applyNewGostConfig(tunnelUpdateDto, existingTunnel, nodes);
|
||||||
|
if (applyResult.getCode() != 0) {
|
||||||
|
chainTunnelService.saveBatch(backupChains);
|
||||||
|
rebuildGostConfig(backupChains, existingTunnel);
|
||||||
|
return R.err("更新失败,已回滚: " + applyResult.getMsg());
|
||||||
|
}
|
||||||
|
|
||||||
|
chainTunnelService.saveBatch(newChainTunnels);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
chainTunnelService.saveBatch(backupChains);
|
||||||
|
rebuildGostConfig(backupChains, existingTunnel);
|
||||||
|
return R.err("更新失败,已回滚: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Tunnel tunnel = new Tunnel();
|
Tunnel tunnel = new Tunnel();
|
||||||
tunnel.setId(tunnelUpdateDto.getId());
|
tunnel.setId(tunnelUpdateDto.getId());
|
||||||
tunnel.setName(tunnelUpdateDto.getName());
|
tunnel.setName(tunnelUpdateDto.getName());
|
||||||
@@ -248,18 +427,23 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
tunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio());
|
tunnel.setTrafficRatio(tunnelUpdateDto.getTrafficRatio());
|
||||||
tunnel.setInIp(tunnelUpdateDto.getInIp());
|
tunnel.setInIp(tunnelUpdateDto.getInIp());
|
||||||
|
|
||||||
if (StringUtils.isEmpty(tunnel.getInIp())){
|
if (StringUtils.isEmpty(tunnel.getInIp())) {
|
||||||
StringBuilder in_ip = new StringBuilder();
|
StringBuilder inIp = new StringBuilder();
|
||||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1));
|
List<ChainTunnel> chainTunnels = chainTunnelService.list(
|
||||||
|
new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1)
|
||||||
|
);
|
||||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||||
if (node == null)return R.err("隧道节点数据错误,部分节点不存在");
|
if (node == null) return R.err("隧道节点数据错误,部分节点不存在");
|
||||||
in_ip.append(node.getServerIp()).append(",");
|
inIp.append(node.getServerIp()).append(",");
|
||||||
}
|
}
|
||||||
in_ip.deleteCharAt(in_ip.length() - 1);
|
if (inIp.length() > 0) {
|
||||||
tunnel.setInIp(in_ip.toString());
|
inIp.deleteCharAt(inIp.length() - 1);
|
||||||
|
}
|
||||||
|
tunnel.setInIp(inIp.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tunnel.setUpdatedTime(System.currentTimeMillis());
|
||||||
this.updateById(tunnel);
|
this.updateById(tunnel);
|
||||||
return R.ok();
|
return R.ok();
|
||||||
}
|
}
|
||||||
@@ -286,12 +470,12 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
GostUtil.DeleteChains(chainTunnel.getNodeId(), "chains_" + chainTunnel.getTunnelId());
|
GostUtil.DeleteChains(chainTunnel.getNodeId(), "chains_" + chainTunnel.getTunnelId());
|
||||||
JSONArray services = new JSONArray();
|
JSONArray services = new JSONArray();
|
||||||
services.add(chainTunnel.getTunnelId() + "_tls");
|
services.add(chainTunnel.getTunnelId() + "_tls");
|
||||||
GostUtil.DeleteChainService(chainTunnel.getNodeId(), services);
|
GostUtil.DeleteService(chainTunnel.getNodeId(), services);
|
||||||
}
|
}
|
||||||
else { // 出口
|
else { // 出口
|
||||||
JSONArray services = new JSONArray();
|
JSONArray services = new JSONArray();
|
||||||
services.add(chainTunnel.getTunnelId() + "_tls");
|
services.add(chainTunnel.getTunnelId() + "_tls");
|
||||||
GostUtil.DeleteChainService(chainTunnel.getNodeId(), services);
|
GostUtil.DeleteService(chainTunnel.getNodeId(), services);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", id));
|
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", id));
|
||||||
@@ -471,8 +655,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
return R.ok(diagnosisReport);
|
return R.ok(diagnosisReport);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public Integer getNodePort(Long nodeId) {
|
||||||
public Integer getNodePort(Long nodeId,Integer type) {
|
|
||||||
|
|
||||||
Node node = nodeService.getById(nodeId);
|
Node node = nodeService.getById(nodeId);
|
||||||
if (node == null){
|
if (node == null){
|
||||||
@@ -505,11 +688,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
if (availablePorts.isEmpty()) {
|
if (availablePorts.isEmpty()) {
|
||||||
throw new RuntimeException("节点端口已满,无可用端口");
|
throw new RuntimeException("节点端口已满,无可用端口");
|
||||||
}
|
}
|
||||||
if (type == 1) {
|
return availablePorts.getFirst();
|
||||||
return availablePorts.getLast();
|
|
||||||
}else {
|
|
||||||
return availablePorts.getFirst();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<Integer> parsePorts(String input) {
|
public static List<Integer> parsePorts(String input) {
|
||||||
@@ -531,6 +710,10 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
return set.stream().sorted().collect(Collectors.toList());
|
return set.stream().sorted().collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void isError(GostDto gostDto){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) {
|
private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) {
|
||||||
try {
|
try {
|
||||||
// 构建TCP ping请求数据
|
// 构建TCP ping请求数据
|
||||||
@@ -626,5 +809,231 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean detectNodeChanges(List<ChainTunnel> oldChains, TunnelUpdateDto dto) {
|
||||||
|
if (dto.getInNodeId() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChainTunnel> oldInNodes = oldChains.stream()
|
||||||
|
.filter(ct -> ct.getChainType() != null && ct.getChainType() == 1)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<ChainTunnel> oldChainNodes = oldChains.stream()
|
||||||
|
.filter(ct -> ct.getChainType() != null && ct.getChainType() == 2)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<ChainTunnel> oldOutNodes = oldChains.stream()
|
||||||
|
.filter(ct -> ct.getChainType() != null && ct.getChainType() == 3)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
Set<Long> oldInNodeIds = oldInNodes.stream().map(ChainTunnel::getNodeId).collect(Collectors.toSet());
|
||||||
|
Set<Long> newInNodeIds = dto.getInNodeId().stream().map(ChainTunnel::getNodeId).collect(Collectors.toSet());
|
||||||
|
if (!oldInNodeIds.equals(newInNodeIds)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChainTunnel> flatNewChainNodes = (dto.getChainNodes() == null) ? new ArrayList<>() :
|
||||||
|
dto.getChainNodes().stream().flatMap(List::stream).collect(Collectors.toList());
|
||||||
|
if (oldChainNodes.size() != flatNewChainNodes.size()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < oldChainNodes.size(); i++) {
|
||||||
|
ChainTunnel oldCt = oldChainNodes.get(i);
|
||||||
|
boolean found = flatNewChainNodes.stream().anyMatch(newCt ->
|
||||||
|
Objects.equals(oldCt.getNodeId(), newCt.getNodeId()) &&
|
||||||
|
Objects.equals(oldCt.getProtocol(), newCt.getProtocol()) &&
|
||||||
|
Objects.equals(oldCt.getStrategy(), newCt.getStrategy()) &&
|
||||||
|
Objects.equals(oldCt.getInx(), newCt.getInx())
|
||||||
|
);
|
||||||
|
if (!found) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChainTunnel> newOutNodes = (dto.getOutNodeId() == null) ? new ArrayList<>() : dto.getOutNodeId();
|
||||||
|
if (oldOutNodes.size() != newOutNodes.size()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Set<Long> oldOutNodeIds = oldOutNodes.stream().map(ChainTunnel::getNodeId).collect(Collectors.toSet());
|
||||||
|
Set<Long> newOutNodeIds = newOutNodes.stream().map(ChainTunnel::getNodeId).collect(Collectors.toSet());
|
||||||
|
if (!oldOutNodeIds.equals(newOutNodeIds)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (ChainTunnel oldOut : oldOutNodes) {
|
||||||
|
boolean found = newOutNodes.stream().anyMatch(newOut ->
|
||||||
|
Objects.equals(oldOut.getNodeId(), newOut.getNodeId()) &&
|
||||||
|
Objects.equals(oldOut.getProtocol(), newOut.getProtocol()) &&
|
||||||
|
Objects.equals(oldOut.getStrategy(), newOut.getStrategy())
|
||||||
|
);
|
||||||
|
if (!found) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupGostConfig(List<ChainTunnel> chainTunnels, Long tunnelId) {
|
||||||
|
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||||
|
if (chainTunnel.getChainType() == 1) {
|
||||||
|
GostUtil.DeleteChains(chainTunnel.getNodeId(), "chains_" + tunnelId);
|
||||||
|
} else if (chainTunnel.getChainType() == 2) {
|
||||||
|
GostUtil.DeleteChains(chainTunnel.getNodeId(), "chains_" + tunnelId);
|
||||||
|
JSONArray services = new JSONArray();
|
||||||
|
services.add(tunnelId + "_tls");
|
||||||
|
GostUtil.DeleteService(chainTunnel.getNodeId(), services);
|
||||||
|
} else if (chainTunnel.getChainType() == 3) {
|
||||||
|
JSONArray services = new JSONArray();
|
||||||
|
services.add(tunnelId + "_tls");
|
||||||
|
GostUtil.DeleteService(chainTunnel.getNodeId(), services);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ChainTunnel> deepCopyChainTunnels(List<ChainTunnel> original) {
|
||||||
|
List<ChainTunnel> copy = new ArrayList<>();
|
||||||
|
for (ChainTunnel ct : original) {
|
||||||
|
ChainTunnel newCt = new ChainTunnel();
|
||||||
|
newCt.setId(ct.getId());
|
||||||
|
newCt.setTunnelId(ct.getTunnelId());
|
||||||
|
newCt.setChainType(ct.getChainType());
|
||||||
|
newCt.setNodeId(ct.getNodeId());
|
||||||
|
newCt.setPort(ct.getPort());
|
||||||
|
newCt.setStrategy(ct.getStrategy());
|
||||||
|
newCt.setInx(ct.getInx());
|
||||||
|
newCt.setProtocol(ct.getProtocol());
|
||||||
|
copy.add(newCt);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
private R applyNewGostConfig(TunnelUpdateDto dto, Tunnel tunnel, Map<Long, Node> nodes) {
|
||||||
|
List<JSONObject> chainSuccess = new ArrayList<>();
|
||||||
|
List<JSONObject> serviceSuccess = new ArrayList<>();
|
||||||
|
|
||||||
|
if (tunnel.getType() == 2) {
|
||||||
|
List<List<ChainTunnel>> chainNodes = dto.getChainNodes() == null ? new ArrayList<>() : dto.getChainNodes();
|
||||||
|
|
||||||
|
for (ChainTunnel inNode : dto.getInNodeId()) {
|
||||||
|
GostDto gostDto;
|
||||||
|
if (chainNodes.isEmpty()) {
|
||||||
|
gostDto = GostUtil.AddChains(inNode.getNodeId(), dto.getOutNodeId(), nodes);
|
||||||
|
} else {
|
||||||
|
gostDto = GostUtil.AddChains(inNode.getNodeId(), chainNodes.get(0), nodes);
|
||||||
|
}
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK")) {
|
||||||
|
rollbackGostChanges(chainSuccess, serviceSuccess);
|
||||||
|
return R.err("创建入口Chain失败: " + gostDto.getMsg());
|
||||||
|
}
|
||||||
|
JSONObject data = new JSONObject();
|
||||||
|
data.put("node_id", inNode.getNodeId());
|
||||||
|
data.put("name", "chains_" + tunnel.getId());
|
||||||
|
chainSuccess.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < chainNodes.size(); i++) {
|
||||||
|
List<ChainTunnel> currentHop = chainNodes.get(i);
|
||||||
|
for (ChainTunnel chainTunnel : currentHop) {
|
||||||
|
GostDto gostDto;
|
||||||
|
if (i + 1 >= chainNodes.size()) {
|
||||||
|
gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), dto.getOutNodeId(), nodes);
|
||||||
|
} else {
|
||||||
|
gostDto = GostUtil.AddChains(chainTunnel.getNodeId(), chainNodes.get(i + 1), nodes);
|
||||||
|
}
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK")) {
|
||||||
|
rollbackGostChanges(chainSuccess, serviceSuccess);
|
||||||
|
return R.err("创建转发链Chain失败: " + gostDto.getMsg());
|
||||||
|
}
|
||||||
|
JSONObject chainData = new JSONObject();
|
||||||
|
chainData.put("node_id", chainTunnel.getNodeId());
|
||||||
|
chainData.put("name", "chains_" + tunnel.getId());
|
||||||
|
chainSuccess.add(chainData);
|
||||||
|
|
||||||
|
GostDto serviceResult = GostUtil.AddChainService(chainTunnel.getNodeId(), chainTunnel, nodes);
|
||||||
|
if (!Objects.equals(serviceResult.getMsg(), "OK")) {
|
||||||
|
rollbackGostChanges(chainSuccess, serviceSuccess);
|
||||||
|
return R.err("创建转发链Service失败: " + serviceResult.getMsg());
|
||||||
|
}
|
||||||
|
JSONObject serviceData = new JSONObject();
|
||||||
|
serviceData.put("node_id", chainTunnel.getNodeId());
|
||||||
|
serviceData.put("name", tunnel.getId() + "_tls");
|
||||||
|
serviceSuccess.add(serviceData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ChainTunnel outNode : dto.getOutNodeId()) {
|
||||||
|
GostDto gostDto = GostUtil.AddChainService(outNode.getNodeId(), outNode, nodes);
|
||||||
|
if (!Objects.equals(gostDto.getMsg(), "OK")) {
|
||||||
|
rollbackGostChanges(chainSuccess, serviceSuccess);
|
||||||
|
return R.err("创建出口Service失败: " + gostDto.getMsg());
|
||||||
|
}
|
||||||
|
JSONObject serviceData = new JSONObject();
|
||||||
|
serviceData.put("node_id", outNode.getNodeId());
|
||||||
|
serviceData.put("name", tunnel.getId() + "_tls");
|
||||||
|
serviceSuccess.add(serviceData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return R.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rollbackGostChanges(List<JSONObject> chainSuccess, List<JSONObject> serviceSuccess) {
|
||||||
|
for (JSONObject chain : chainSuccess) {
|
||||||
|
GostUtil.DeleteChains(chain.getLong("node_id"), chain.getString("name"));
|
||||||
|
}
|
||||||
|
for (JSONObject service : serviceSuccess) {
|
||||||
|
JSONArray services = new JSONArray();
|
||||||
|
services.add(service.getString("name"));
|
||||||
|
GostUtil.DeleteService(service.getLong("node_id"), services);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildGostConfig(List<ChainTunnel> chainTunnels, Tunnel tunnel) {
|
||||||
|
Map<Long, Node> nodes = new HashMap<>();
|
||||||
|
for (ChainTunnel ct : chainTunnels) {
|
||||||
|
Node node = nodeService.getById(ct.getNodeId());
|
||||||
|
if (node != null) {
|
||||||
|
nodes.put(node.getId(), node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ChainTunnel> inNodes = chainTunnels.stream()
|
||||||
|
.filter(ct -> ct.getChainType() == 1)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
Map<Integer, List<ChainTunnel>> chainNodesMap = chainTunnels.stream()
|
||||||
|
.filter(ct -> ct.getChainType() == 2)
|
||||||
|
.collect(Collectors.groupingBy(ct -> ct.getInx() != null ? ct.getInx() : 0));
|
||||||
|
List<List<ChainTunnel>> chainNodesList = chainNodesMap.entrySet().stream()
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.map(Map.Entry::getValue)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
List<ChainTunnel> outNodes = chainTunnels.stream()
|
||||||
|
.filter(ct -> ct.getChainType() == 3)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (tunnel.getType() == 2) {
|
||||||
|
for (ChainTunnel inNode : inNodes) {
|
||||||
|
if (chainNodesList.isEmpty()) {
|
||||||
|
GostUtil.AddChains(inNode.getNodeId(), outNodes, nodes);
|
||||||
|
} else {
|
||||||
|
GostUtil.AddChains(inNode.getNodeId(), chainNodesList.get(0), nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < chainNodesList.size(); i++) {
|
||||||
|
for (ChainTunnel chainTunnel : chainNodesList.get(i)) {
|
||||||
|
if (i + 1 >= chainNodesList.size()) {
|
||||||
|
GostUtil.AddChains(chainTunnel.getNodeId(), outNodes, nodes);
|
||||||
|
} else {
|
||||||
|
GostUtil.AddChains(chainTunnel.getNodeId(), chainNodesList.get(i + 1), nodes);
|
||||||
|
}
|
||||||
|
GostUtil.AddChainService(chainTunnel.getNodeId(), chainTunnel, nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ChainTunnel outNode : outNodes) {
|
||||||
|
GostUtil.AddChainService(outNode.getNodeId(), outNode, nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,44 +264,61 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
|||||||
for (UserPackageDto.UserForwardDetailDto forward : forwards) {
|
for (UserPackageDto.UserForwardDetailDto forward : forwards) {
|
||||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||||
if (tunnel == null) continue;
|
if (tunnel == null) continue;
|
||||||
|
|
||||||
List<ForwardPort> forwardPorts = forwardPortService.list(
|
List<ForwardPort> forwardPorts = forwardPortService.list(
|
||||||
new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId())
|
new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId())
|
||||||
);
|
);
|
||||||
if (forwardPorts.isEmpty()) continue;
|
if (forwardPorts.isEmpty()) continue;
|
||||||
|
|
||||||
boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty();
|
boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty();
|
||||||
List<String> ipList = new ArrayList<>();
|
java.util.Set<String> ipPortSet = new java.util.LinkedHashSet<>();
|
||||||
List<Integer> portList = new ArrayList<>();
|
|
||||||
if (useTunnelInIp) {
|
if (useTunnelInIp) {
|
||||||
|
// 使用隧道的inIp(求笛卡尔积)
|
||||||
|
List<String> ipList = new ArrayList<>();
|
||||||
|
List<Integer> portList = new ArrayList<>();
|
||||||
|
|
||||||
String[] tunnelInIps = tunnel.getInIp().split(",");
|
String[] tunnelInIps = tunnel.getInIp().split(",");
|
||||||
for (String ip : tunnelInIps) {
|
for (String ip : tunnelInIps) {
|
||||||
if (ip != null && !ip.trim().isEmpty()) {
|
if (ip != null && !ip.trim().isEmpty()) {
|
||||||
ipList.add(ip.trim());
|
ipList.add(ip.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for (ForwardPort forwardPort : forwardPorts) {
|
for (ForwardPort forwardPort : forwardPorts) {
|
||||||
Node node = nodeService.getById(forwardPort.getNodeId());
|
if (forwardPort.getPort() != null) {
|
||||||
if (node != null && node.getServerIp() != null) {
|
portList.add(forwardPort.getPort());
|
||||||
ipList.add(node.getServerIp());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (ForwardPort forwardPort : forwardPorts) {
|
List<String> uniqueIps = ipList.stream().distinct().toList();
|
||||||
if (forwardPort.getPort() != null) {
|
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
||||||
portList.add(forwardPort.getPort());
|
|
||||||
}
|
for (String ip : uniqueIps) {
|
||||||
}
|
for (Integer port : uniquePorts) {
|
||||||
List<String> uniqueIps = ipList.stream().distinct().toList();
|
ipPortSet.add(ip + ":" + port);
|
||||||
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
}
|
||||||
java.util.Set<String> ipPortSet = new java.util.LinkedHashSet<>();
|
}
|
||||||
for (String ip : uniqueIps) {
|
|
||||||
for (Integer port : uniquePorts) {
|
if (!uniquePorts.isEmpty()) {
|
||||||
ipPortSet.add(ip + ":" + port);
|
forward.setInPort(uniquePorts.getFirst());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用节点的serverIp(一对一,不求笛卡尔积)
|
||||||
|
for (ForwardPort forwardPort : forwardPorts) {
|
||||||
|
Node node = nodeService.getById(forwardPort.getNodeId());
|
||||||
|
if (node != null && node.getServerIp() != null && forwardPort.getPort() != null) {
|
||||||
|
ipPortSet.add(node.getServerIp() + ":" + forwardPort.getPort());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forwardPorts.isEmpty() && forwardPorts.getFirst().getPort() != null) {
|
||||||
|
forward.setInPort(forwardPorts.getFirst().getPort());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ipPortSet.isEmpty()) {
|
if (!ipPortSet.isEmpty()) {
|
||||||
forward.setInIp(String.join(",", ipPortSet));
|
forward.setInIp(String.join(",", ipPortSet));
|
||||||
forward.setInPort(uniquePorts.getFirst());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# VITE FRONTEND KNOWLEDGE BASE
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
Web management console for Flux Panel.
|
||||||
|
**Stack:** React 18, Vite 5, TypeScript, TailwindCSS 4, HeroUI (NextUI).
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
```
|
||||||
|
vite-frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── pages/ # Route views
|
||||||
|
│ ├── components/ # Reusable UI parts
|
||||||
|
│ ├── layouts/ # Page wrappers
|
||||||
|
│ ├── api/ # Axios wrappers
|
||||||
|
│ ├── config/ # App settings
|
||||||
|
│ └── utils/ # Helpers
|
||||||
|
├── vite.config.ts # Vite config (Base: '/')
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
- **UI Lib**: HeroUI (formerly NextUI) + Tailwind CSS 4.
|
||||||
|
- **Routing**: React Router DOM 6.
|
||||||
|
- **State**: Check `provider.tsx` or local state.
|
||||||
|
- **Build**: Output to `dist/`.
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
```bash
|
||||||
|
# Dev
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Build
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
@@ -4,7 +4,7 @@ FROM node:20.19.0 AS builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ export type SiteConfig = typeof siteConfig;
|
|||||||
|
|
||||||
// 缓存相关常量
|
// 缓存相关常量
|
||||||
const CACHE_PREFIX = 'vite_config_';
|
const CACHE_PREFIX = 'vite_config_';
|
||||||
const VERSION = "2.0.0-beta";
|
const VERSION = "2.0.9";
|
||||||
const APP_VERSION = "1.0.3";
|
const APP_VERSION = "1.0.3";
|
||||||
|
const GITHUB_REPO = import.meta.env.VITE_GITHUB_REPO || "https://github.com/Sagit-chu/flux-panel";
|
||||||
|
|
||||||
const getInitialConfig = () => {
|
const getInitialConfig = () => {
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
@@ -13,6 +14,7 @@ const getInitialConfig = () => {
|
|||||||
name: "flux",
|
name: "flux",
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
app_version: APP_VERSION,
|
app_version: APP_VERSION,
|
||||||
|
github_repo: GITHUB_REPO,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +24,7 @@ const getInitialConfig = () => {
|
|||||||
name: cachedAppName,
|
name: cachedAppName,
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
app_version: APP_VERSION,
|
app_version: APP_VERSION,
|
||||||
|
github_repo: GITHUB_REPO,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ export default function AdminLayout({
|
|||||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Powered by{' '}
|
Powered by{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/bqlpfy/flux-panel"
|
href={siteConfig.github_repo}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
|
|||||||
@@ -406,6 +406,14 @@ export default function ForwardPage() {
|
|||||||
newErrors.tunnelId = '请选择关联隧道';
|
newErrors.tunnelId = '请选择关联隧道';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证入口端口(可选,如果填写则验证)
|
||||||
|
if (form.inPort !== null && form.inPort !== undefined) {
|
||||||
|
const port = Number(form.inPort);
|
||||||
|
if (isNaN(port) || port < 1 || port > 65535) {
|
||||||
|
newErrors.inPort = '端口必须在 1-65535 之间';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!form.remoteAddr.trim()) {
|
if (!form.remoteAddr.trim()) {
|
||||||
newErrors.remoteAddr = '请输入远程地址';
|
newErrors.remoteAddr = '请输入远程地址';
|
||||||
} else {
|
} else {
|
||||||
@@ -526,6 +534,7 @@ export default function ForwardPage() {
|
|||||||
userId: form.userId,
|
userId: form.userId,
|
||||||
name: form.name,
|
name: form.name,
|
||||||
tunnelId: form.tunnelId,
|
tunnelId: form.tunnelId,
|
||||||
|
inPort: form.inPort,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||||
};
|
};
|
||||||
@@ -535,6 +544,7 @@ export default function ForwardPage() {
|
|||||||
const createData = {
|
const createData = {
|
||||||
name: form.name,
|
name: form.name,
|
||||||
tunnelId: form.tunnelId,
|
tunnelId: form.tunnelId,
|
||||||
|
inPort: form.inPort,
|
||||||
remoteAddr: processedRemoteAddr,
|
remoteAddr: processedRemoteAddr,
|
||||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||||
};
|
};
|
||||||
@@ -1596,8 +1606,7 @@ export default function ForwardPage() {
|
|||||||
isInvalid={!!errors.tunnelId}
|
isInvalid={!!errors.tunnelId}
|
||||||
errorMessage={errors.tunnelId}
|
errorMessage={errors.tunnelId}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={isEdit}
|
description={isEdit ? "更改隧道将释放原端口并在新隧道分配端口" : undefined}
|
||||||
description={isEdit ? "编辑时无法修改关联隧道" : undefined}
|
|
||||||
>
|
>
|
||||||
{tunnels.map((tunnel) => (
|
{tunnels.map((tunnel) => (
|
||||||
<SelectItem key={tunnel.id} >
|
<SelectItem key={tunnel.id} >
|
||||||
@@ -1605,6 +1614,24 @@ export default function ForwardPage() {
|
|||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="入口端口"
|
||||||
|
placeholder="留空则自动分配可用端口"
|
||||||
|
type="number"
|
||||||
|
value={form.inPort !== null ? form.inPort.toString() : ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setForm(prev => ({
|
||||||
|
...prev,
|
||||||
|
inPort: value ? parseInt(value) : null
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
isInvalid={!!errors.inPort}
|
||||||
|
errorMessage={errors.inPort}
|
||||||
|
variant="bordered"
|
||||||
|
description="指定入口端口,留空则从节点可用端口中自动分配"
|
||||||
|
/>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
label="远程地址"
|
label="远程地址"
|
||||||
|
|||||||
@@ -308,14 +308,14 @@ export default function IndexPage() {
|
|||||||
<div className="fixed inset-x-0 bottom-4 text-center py-4">
|
<div className="fixed inset-x-0 bottom-4 text-center py-4">
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Powered by{' '}
|
Powered by{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/bqlpfy/flux-panel"
|
href={siteConfig.github_repo}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
>
|
>
|
||||||
flux-panel
|
flux-panel
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
v{ isWebView ? siteConfig.app_version : siteConfig.version}
|
v{ isWebView ? siteConfig.app_version : siteConfig.version}
|
||||||
|
|||||||
+123
-122
@@ -9,6 +9,7 @@ import { Switch } from "@heroui/switch";
|
|||||||
import { Spinner } from "@heroui/spinner";
|
import { Spinner } from "@heroui/spinner";
|
||||||
import { Alert } from "@heroui/alert";
|
import { Alert } from "@heroui/alert";
|
||||||
import { Progress } from "@heroui/progress";
|
import { Progress } from "@heroui/progress";
|
||||||
|
import { Accordion, AccordionItem } from "@heroui/accordion";
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -875,141 +876,141 @@ export default function NodePage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
{/* 高级配置 */}
|
||||||
label="出口网卡名或IP"
|
<Accordion variant="bordered">
|
||||||
placeholder="请输入出口网卡名或IP"
|
<AccordionItem
|
||||||
value={form.interfaceName}
|
key="advanced"
|
||||||
onChange={(e) => setForm(prev => ({ ...prev, interfaceName: e.target.value }))}
|
aria-label="高级配置"
|
||||||
isInvalid={!!errors.interfaceName}
|
title="高级配置"
|
||||||
errorMessage={errors.interfaceName}
|
>
|
||||||
variant="bordered"
|
<div className="space-y-4 pb-2">
|
||||||
description="用于多IP服务器指定使用那个IP请求远程地址,不懂的默认为空就行"
|
<Input
|
||||||
/>
|
label="出口网卡名或IP"
|
||||||
|
placeholder="请输入出口网卡名或IP"
|
||||||
|
value={form.interfaceName}
|
||||||
|
onChange={(e) => setForm(prev => ({ ...prev, interfaceName: e.target.value }))}
|
||||||
|
isInvalid={!!errors.interfaceName}
|
||||||
|
errorMessage={errors.interfaceName}
|
||||||
|
variant="bordered"
|
||||||
|
description="用于多IP服务器指定使用那个IP请求远程地址,不懂的默认为空就行"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<Input
|
<Input
|
||||||
label="TCP监听地址"
|
label="TCP监听地址"
|
||||||
placeholder="请输入TCP监听地址"
|
placeholder="请输入TCP监听地址"
|
||||||
value={form.tcpListenAddr}
|
value={form.tcpListenAddr}
|
||||||
onChange={(e) => setForm(prev => ({ ...prev, tcpListenAddr: e.target.value }))}
|
onChange={(e) => setForm(prev => ({ ...prev, tcpListenAddr: e.target.value }))}
|
||||||
isInvalid={!!errors.tcpListenAddr}
|
isInvalid={!!errors.tcpListenAddr}
|
||||||
errorMessage={errors.tcpListenAddr}
|
errorMessage={errors.tcpListenAddr}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
startContent={
|
startContent={
|
||||||
<div className="pointer-events-none flex items-center">
|
<div className="pointer-events-none flex items-center">
|
||||||
<span className="text-default-400 text-small">TCP</span>
|
<span className="text-default-400 text-small">TCP</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="UDP监听地址"
|
label="UDP监听地址"
|
||||||
placeholder="请输入UDP监听地址"
|
placeholder="请输入UDP监听地址"
|
||||||
value={form.udpListenAddr}
|
value={form.udpListenAddr}
|
||||||
onChange={(e) => setForm(prev => ({ ...prev, udpListenAddr: e.target.value }))}
|
onChange={(e) => setForm(prev => ({ ...prev, udpListenAddr: e.target.value }))}
|
||||||
isInvalid={!!errors.udpListenAddr}
|
isInvalid={!!errors.udpListenAddr}
|
||||||
errorMessage={errors.udpListenAddr}
|
errorMessage={errors.udpListenAddr}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
startContent={
|
startContent={
|
||||||
<div className="pointer-events-none flex items-center">
|
<div className="pointer-events-none flex items-center">
|
||||||
<span className="text-default-400 text-small">UDP</span>
|
<span className="text-default-400 text-small">UDP</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 屏蔽协议 */}
|
|
||||||
<div className="mt-1">
|
|
||||||
<div className="text-sm font-medium text-default-700">屏蔽协议</div>
|
|
||||||
<div className="text-xs text-default-500 mb-2">开启开关以屏蔽对应协议</div>
|
|
||||||
{protocolDisabled && (
|
|
||||||
<Alert
|
|
||||||
color="warning"
|
|
||||||
variant="flat"
|
|
||||||
description={protocolDisabledReason || '等待节点上线后再设置'}
|
|
||||||
className="mb-2"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className={`grid grid-cols-1 sm:grid-cols-3 gap-3 bg-default-50 dark:bg-default-100 p-3 rounded-md border border-default-200 dark:border-default-100/30 ${protocolDisabled ? 'opacity-70' : ''}`}>
|
|
||||||
{/* HTTP tile */}
|
|
||||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
|
|
||||||
<div className="text-sm font-medium text-default-700">HTTP</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-xs text-default-500">禁用/启用</div>
|
|
||||||
<Switch
|
|
||||||
size="sm"
|
|
||||||
isSelected={form.http === 1}
|
|
||||||
isDisabled={protocolDisabled}
|
|
||||||
onValueChange={(v) => setForm(prev => ({ ...prev, http: v ? 1 : 0 }))}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-default-400">{form.http === 1 ? '已开启' : '已关闭'}</div>
|
{/* 屏蔽协议 */}
|
||||||
</div>
|
<div>
|
||||||
|
<div className="text-sm font-medium text-default-700 mb-2">屏蔽协议</div>
|
||||||
|
<div className="text-xs text-default-500 mb-2">开启开关以屏蔽对应协议</div>
|
||||||
|
{protocolDisabled && (
|
||||||
|
<Alert
|
||||||
|
color="warning"
|
||||||
|
variant="flat"
|
||||||
|
description={protocolDisabledReason || '等待节点上线后再设置'}
|
||||||
|
className="mb-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className={`grid grid-cols-1 sm:grid-cols-3 gap-3 bg-default-50 dark:bg-default-100 p-3 rounded-md border border-default-200 dark:border-default-100/30 ${protocolDisabled ? 'opacity-70' : ''}`}>
|
||||||
|
{/* HTTP tile */}
|
||||||
|
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
|
||||||
|
<div className="text-sm font-medium text-default-700">HTTP</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs text-default-500">禁用/启用</div>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={form.http === 1}
|
||||||
|
isDisabled={protocolDisabled}
|
||||||
|
onValueChange={(v) => setForm(prev => ({ ...prev, http: v ? 1 : 0 }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-default-400">{form.http === 1 ? '已开启' : '已关闭'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* TLS tile */}
|
{/* TLS tile */}
|
||||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 10V7a6 6 0 1 1 12 0v3"/><rect x="4" y="10" width="16" height="10" rx="2"/></svg>
|
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M6 10V7a6 6 0 1 1 12 0v3"/><rect x="4" y="10" width="16" height="10" rx="2"/></svg>
|
||||||
<div className="text-sm font-medium text-default-700">TLS</div>
|
<div className="text-sm font-medium text-default-700">TLS</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs text-default-500">禁用/启用</div>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={form.tls === 1}
|
||||||
|
isDisabled={protocolDisabled}
|
||||||
|
onValueChange={(v) => setForm(prev => ({ ...prev, tls: v ? 1 : 0 }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-default-400">{form.tls === 1 ? '已开启' : '已关闭'}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SOCKS tile */}
|
||||||
|
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
<div className="text-sm font-medium text-default-700">SOCKS</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-xs text-default-500">禁用/启用</div>
|
||||||
|
<Switch
|
||||||
|
size="sm"
|
||||||
|
isSelected={form.socks === 1}
|
||||||
|
isDisabled={protocolDisabled}
|
||||||
|
onValueChange={(v) => setForm(prev => ({ ...prev, socks: v ? 1 : 0 }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xs text-default-400">{form.socks === 1 ? '已开启' : '已关闭'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-xs text-default-500">禁用/启用</div>
|
|
||||||
<Switch
|
|
||||||
size="sm"
|
|
||||||
isSelected={form.tls === 1}
|
|
||||||
isDisabled={protocolDisabled}
|
|
||||||
onValueChange={(v) => setForm(prev => ({ ...prev, tls: v ? 1 : 0 }))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-xs text-default-400">{form.tls === 1 ? '已开启' : '已关闭'}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SOCKS tile */}
|
<Alert
|
||||||
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
|
||||||
<div className="text-sm font-medium text-default-700">SOCKS</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="text-xs text-default-500">禁用/启用</div>
|
|
||||||
<Switch
|
|
||||||
size="sm"
|
|
||||||
isSelected={form.socks === 1}
|
|
||||||
isDisabled={protocolDisabled}
|
|
||||||
onValueChange={(v) => setForm(prev => ({ ...prev, socks: v ? 1 : 0 }))}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-xs text-default-400">{form.socks === 1 ? '已开启' : '已关闭'}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<Alert
|
|
||||||
color="danger"
|
color="danger"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
description="请不要在出口节点执行屏蔽协议,否则可能影响转发;屏蔽协议仅需在入口节点执行。"
|
description="请不要在出口节点执行屏蔽协议,否则可能影响转发;屏蔽协议仅需在入口节点执行。"
|
||||||
className="mt-3"
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
color="primary"
|
color="primary"
|
||||||
variant="flat"
|
variant="flat"
|
||||||
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址。"
|
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址。"
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Alert
|
|
||||||
color="primary"
|
|
||||||
variant="flat"
|
|
||||||
title="TCP,UDP监听地址"
|
|
||||||
description="V6或者双栈填写[::],V4填写0.0.0.0。不懂的就去看文档网站内的说明"
|
|
||||||
className="mt-4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -241,14 +241,14 @@ export default function ProfilePage() {
|
|||||||
<div className="fixed inset-x-0 bottom-20 text-center py-4">
|
<div className="fixed inset-x-0 bottom-20 text-center py-4">
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500">
|
<p className="text-xs text-gray-400 dark:text-gray-500">
|
||||||
Powered by{' '}
|
Powered by{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/bqlpfy/flux-panel"
|
href={siteConfig.github_repo}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
className="text-gray-500 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||||
>
|
>
|
||||||
flux-panel
|
flux-panel
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||||
v{ isWebViewFunc() ? siteConfig.app_version : siteConfig.version}
|
v{ isWebViewFunc() ? siteConfig.app_version : siteConfig.version}
|
||||||
|
|||||||
@@ -357,6 +357,9 @@ export default function TunnelPage() {
|
|||||||
.map(group => group.filter(node => node.nodeId !== -1))
|
.map(group => group.filter(node => node.nodeId !== -1))
|
||||||
.filter(group => group.length > 0); // 移除空组
|
.filter(group => group.length > 0); // 移除空组
|
||||||
|
|
||||||
|
// 过滤掉出口节点中的占位节点
|
||||||
|
const cleanedOutNodeId = (form.outNodeId || []).filter(node => node.nodeId !== -1);
|
||||||
|
|
||||||
// 将换行符分隔的IP转换为逗号分隔
|
// 将换行符分隔的IP转换为逗号分隔
|
||||||
const inIpString = form.inIp
|
const inIpString = form.inIp
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@@ -367,6 +370,7 @@ export default function TunnelPage() {
|
|||||||
const data = {
|
const data = {
|
||||||
...form,
|
...form,
|
||||||
inIp: inIpString,
|
inIp: inIpString,
|
||||||
|
outNodeId: cleanedOutNodeId,
|
||||||
chainNodes: cleanedChainNodes
|
chainNodes: cleanedChainNodes
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -689,7 +693,7 @@ export default function TunnelPage() {
|
|||||||
{isEdit ? '编辑隧道' : '新增隧道'}
|
{isEdit ? '编辑隧道' : '新增隧道'}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-small text-default-500">
|
<p className="text-small text-default-500">
|
||||||
{isEdit ? '编辑时只能修改隧道名称、流量计算和流量倍率' : '创建新的隧道配置'}
|
{isEdit ? '修改节点配置会中断现有连接,隧道类型不可修改' : '创建新的隧道配置'}
|
||||||
</p>
|
</p>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -799,11 +803,10 @@ export default function TunnelPage() {
|
|||||||
});
|
});
|
||||||
setForm(prev => ({ ...prev, inNodeId: newInNodeId }));
|
setForm(prev => ({ ...prev, inNodeId: newInNodeId }));
|
||||||
}}
|
}}
|
||||||
isInvalid={!!errors.inNodeId}
|
isInvalid={!!errors.inNodeId}
|
||||||
errorMessage={errors.inNodeId}
|
errorMessage={errors.inNodeId}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={isEdit}
|
>
|
||||||
>
|
|
||||||
{nodes.map((node) => (
|
{nodes.map((node) => (
|
||||||
<SelectItem
|
<SelectItem
|
||||||
key={node.id}
|
key={node.id}
|
||||||
@@ -856,7 +859,6 @@ export default function TunnelPage() {
|
|||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
isDisabled={isEdit}
|
|
||||||
startContent={
|
startContent={
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
@@ -885,7 +887,6 @@ export default function TunnelPage() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
isIconOnly
|
isIconOnly
|
||||||
onPress={() => removeChainNode(groupIndex)}
|
onPress={() => removeChainNode(groupIndex)}
|
||||||
isDisabled={isEdit}
|
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||||
@@ -928,7 +929,6 @@ export default function TunnelPage() {
|
|||||||
}}
|
}}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={isEdit}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
label: "text-xs",
|
label: "text-xs",
|
||||||
value: "text-sm"
|
value: "text-sm"
|
||||||
@@ -987,7 +987,6 @@ export default function TunnelPage() {
|
|||||||
}}
|
}}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={isEdit}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
label: "text-xs",
|
label: "text-xs",
|
||||||
value: "text-sm"
|
value: "text-sm"
|
||||||
@@ -1014,7 +1013,6 @@ export default function TunnelPage() {
|
|||||||
}}
|
}}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
size="sm"
|
size="sm"
|
||||||
isDisabled={isEdit}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
label: "text-xs",
|
label: "text-xs",
|
||||||
value: "text-sm"
|
value: "text-sm"
|
||||||
@@ -1052,7 +1050,7 @@ export default function TunnelPage() {
|
|||||||
label="节点"
|
label="节点"
|
||||||
placeholder="请选择出口节点(可多选)"
|
placeholder="请选择出口节点(可多选)"
|
||||||
selectionMode="multiple"
|
selectionMode="multiple"
|
||||||
selectedKeys={form.outNodeId ? form.outNodeId.map(ct => ct.nodeId.toString()) : []}
|
selectedKeys={form.outNodeId ? form.outNodeId.filter(ct => ct.nodeId !== -1).map(ct => ct.nodeId.toString()) : []}
|
||||||
disabledKeys={[
|
disabledKeys={[
|
||||||
...nodes.filter(node => node.status !== 1).map(node => node.id.toString()),
|
...nodes.filter(node => node.status !== 1).map(node => node.id.toString()),
|
||||||
...form.inNodeId.map(ct => ct.nodeId.toString()),
|
...form.inNodeId.map(ct => ct.nodeId.toString()),
|
||||||
@@ -1061,12 +1059,17 @@ export default function TunnelPage() {
|
|||||||
onSelectionChange={(keys) => {
|
onSelectionChange={(keys) => {
|
||||||
const selectedIds = Array.from(keys).map(key => parseInt(key as string));
|
const selectedIds = Array.from(keys).map(key => parseInt(key as string));
|
||||||
const currentOutNodes = form.outNodeId || [];
|
const currentOutNodes = form.outNodeId || [];
|
||||||
const protocol = currentOutNodes.length > 0 ? currentOutNodes[0].protocol || 'tls' : 'tls';
|
|
||||||
const strategy = currentOutNodes.length > 0 ? currentOutNodes[0].strategy || 'round' : 'round';
|
|
||||||
|
|
||||||
|
let protocol = 'tls';
|
||||||
|
let strategy = 'round';
|
||||||
|
if (currentOutNodes.length > 0) {
|
||||||
|
protocol = currentOutNodes[0].protocol || 'tls';
|
||||||
|
strategy = currentOutNodes[0].strategy || 'round';
|
||||||
|
}
|
||||||
|
|
||||||
|
const realNodes = currentOutNodes.filter(ct => ct.nodeId !== -1);
|
||||||
const newOutNodeId: ChainTunnel[] = selectedIds.map(nodeId => {
|
const newOutNodeId: ChainTunnel[] = selectedIds.map(nodeId => {
|
||||||
// 保留已有的配置或使用默认值
|
const existing = realNodes.find(ct => ct.nodeId === nodeId);
|
||||||
const existing = currentOutNodes.find(ct => ct.nodeId === nodeId);
|
|
||||||
return existing || { nodeId, chainType: 3, protocol, strategy };
|
return existing || { nodeId, chainType: 3, protocol, strategy };
|
||||||
});
|
});
|
||||||
setForm(prev => ({ ...prev, outNodeId: newOutNodeId }));
|
setForm(prev => ({ ...prev, outNodeId: newOutNodeId }));
|
||||||
@@ -1074,7 +1077,6 @@ export default function TunnelPage() {
|
|||||||
isInvalid={!!errors.outNodeId}
|
isInvalid={!!errors.outNodeId}
|
||||||
errorMessage={errors.outNodeId}
|
errorMessage={errors.outNodeId}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={isEdit}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
label: "text-xs",
|
label: "text-xs",
|
||||||
value: "text-sm"
|
value: "text-sm"
|
||||||
@@ -1116,20 +1118,35 @@ export default function TunnelPage() {
|
|||||||
<Select
|
<Select
|
||||||
label="协议"
|
label="协议"
|
||||||
placeholder="选择协议"
|
placeholder="选择协议"
|
||||||
selectedKeys={[form.outNodeId && form.outNodeId.length > 0 ? form.outNodeId[0].protocol || 'tls' : 'tls']}
|
selectedKeys={[(() => {
|
||||||
|
if (!form.outNodeId || form.outNodeId.length === 0) return 'tls';
|
||||||
|
return form.outNodeId[0].protocol || 'tls';
|
||||||
|
})()]}
|
||||||
onSelectionChange={(keys) => {
|
onSelectionChange={(keys) => {
|
||||||
const selectedKey = Array.from(keys)[0] as string;
|
const selectedKey = Array.from(keys)[0] as string;
|
||||||
if (selectedKey && form.outNodeId) {
|
if (selectedKey) {
|
||||||
setForm(prev => ({
|
setForm(prev => {
|
||||||
...prev,
|
const currentOutNodes = prev.outNodeId || [];
|
||||||
outNodeId: (prev.outNodeId || []).map(ct => ({ ...ct, protocol: selectedKey }))
|
const currentStrategy = currentOutNodes.length > 0 ? currentOutNodes[0].strategy || 'round' : 'round';
|
||||||
}));
|
|
||||||
|
if (currentOutNodes.length === 0) {
|
||||||
|
// 如果还没有出口节点,创建一个占位节点保存设置
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
outNodeId: [{ nodeId: -1, chainType: 3, protocol: selectedKey, strategy: currentStrategy }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 更新所有出口节点的协议
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
outNodeId: currentOutNodes.map(ct => ({ ...ct, protocol: selectedKey }))
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
isInvalid={!!errors.protocol}
|
isInvalid={!!errors.protocol}
|
||||||
errorMessage={errors.protocol}
|
errorMessage={errors.protocol}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={isEdit}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
label: "text-xs",
|
label: "text-xs",
|
||||||
value: "text-sm"
|
value: "text-sm"
|
||||||
@@ -1147,18 +1164,31 @@ export default function TunnelPage() {
|
|||||||
<Select
|
<Select
|
||||||
label="负载策略"
|
label="负载策略"
|
||||||
placeholder="选择策略"
|
placeholder="选择策略"
|
||||||
selectedKeys={[form.outNodeId && form.outNodeId.length > 0 ? form.outNodeId[0].strategy || 'round' : 'round']}
|
selectedKeys={[(() => {
|
||||||
|
if (!form.outNodeId || form.outNodeId.length === 0) return 'round';
|
||||||
|
return form.outNodeId[0].strategy || 'round';
|
||||||
|
})()]}
|
||||||
onSelectionChange={(keys) => {
|
onSelectionChange={(keys) => {
|
||||||
const selectedKey = Array.from(keys)[0] as string;
|
const selectedKey = Array.from(keys)[0] as string;
|
||||||
if (selectedKey && form.outNodeId) {
|
if (selectedKey) {
|
||||||
setForm(prev => ({
|
setForm(prev => {
|
||||||
...prev,
|
const currentOutNodes = prev.outNodeId || [];
|
||||||
outNodeId: (prev.outNodeId || []).map(ct => ({ ...ct, strategy: selectedKey }))
|
const currentProtocol = currentOutNodes.length > 0 ? currentOutNodes[0].protocol || 'tls' : 'tls';
|
||||||
}));
|
|
||||||
|
if (currentOutNodes.length === 0) {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
outNodeId: [{ nodeId: -1, chainType: 3, protocol: currentProtocol, strategy: selectedKey }]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
outNodeId: currentOutNodes.map(ct => ({ ...ct, strategy: selectedKey }))
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
variant="bordered"
|
variant="bordered"
|
||||||
isDisabled={isEdit}
|
|
||||||
classNames={{
|
classNames={{
|
||||||
label: "text-xs",
|
label: "text-xs",
|
||||||
value: "text-sm"
|
value: "text-sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user