mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 15:46:38 +08:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 37ea338a35 | |||
| 4063d39d6a | |||
| f6dd3ad657 | |||
| 252c16de29 | |||
| 4fef8eeb32 | |||
| 9146eb0375 |
@@ -2,7 +2,7 @@ name: Build and Push Images Based on Version
|
||||
|
||||
# 在这里定义统一版本号
|
||||
env:
|
||||
VERSION: "2.0.0-beta"
|
||||
VERSION: "2.0.4-beta"
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -17,10 +17,11 @@ jobs:
|
||||
outputs:
|
||||
version: ${{ env.VERSION }}
|
||||
should_build: ${{ steps.check-tag.outputs.should_build }}
|
||||
tag_exists: ${{ steps.check-tag.outputs.tag_exists }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # 获取所有历史和 tags
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Display version
|
||||
run: |
|
||||
@@ -30,11 +31,13 @@ jobs:
|
||||
id: check-tag
|
||||
run: |
|
||||
if git rev-parse "${{ env.VERSION }}" >/dev/null 2>&1; then
|
||||
echo "Tag ${{ env.VERSION }} already exists, skip build"
|
||||
echo "Tag ${{ env.VERSION }} already exists, will only update files"
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Tag ${{ env.VERSION }} does not exist, will build all components"
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
echo "tag_exists=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build-gost:
|
||||
@@ -67,21 +70,37 @@ jobs:
|
||||
sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin/
|
||||
rm -rf upx-4.2.1-amd64_linux*
|
||||
|
||||
- name: Build GOST binary
|
||||
- name: Build GOST binary (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost-amd64
|
||||
|
||||
- name: Compress with UPX
|
||||
- name: Build GOST binary (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o gost-arm64
|
||||
|
||||
- name: Upload GOST artifact
|
||||
- name: Compress with UPX (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost-amd64
|
||||
|
||||
- name: Compress with UPX (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost-arm64
|
||||
|
||||
- name: Upload GOST AMD64 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gost-binary
|
||||
path: ./go-gost/gost
|
||||
name: gost-binary-amd64
|
||||
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:
|
||||
name: Build & Push Vite Frontend
|
||||
@@ -109,7 +128,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t bqlpfy/vite-frontend:latest \
|
||||
-t bqlpfy/vite-frontend:${VERSION} \
|
||||
@@ -153,7 +172,7 @@ jobs:
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t bqlpfy/springboot-backend:latest \
|
||||
-t bqlpfy/springboot-backend:${VERSION} \
|
||||
@@ -169,11 +188,22 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download GOST binary
|
||||
- name: Download GOST AMD64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary
|
||||
path: ./artifacts
|
||||
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: Create Release
|
||||
env:
|
||||
@@ -193,33 +223,17 @@ jobs:
|
||||
# 创建 release
|
||||
gh release create "${VERSION}" \
|
||||
--title "Release ${VERSION}" \
|
||||
--notes "Version ${VERSION}
|
||||
|
||||
## 📝 Commit Information
|
||||
--notes "## 📝 Commit Information
|
||||
- **Message**: ${COMMIT_MSG}
|
||||
- **Author**: ${COMMIT_AUTHOR}
|
||||
- **Date**: ${COMMIT_DATE}
|
||||
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
|
||||
|
||||
## 📦 Components
|
||||
- **GOST Binary** (Linux AMD64, compressed with UPX)
|
||||
- **Vite Frontend** Docker Image: \`bqlpfy/vite-frontend:${VERSION}\` / \`bqlpfy/vite-frontend:latest\`
|
||||
- **Spring Boot Backend** Docker Image: \`bqlpfy/springboot-backend:${VERSION}\` / \`bqlpfy/springboot-backend:latest\`
|
||||
|
||||
## 📥 Installation Files
|
||||
- \`install.sh\` - GOST 客户端安装脚本
|
||||
- \`panel_install.sh\` - 面板安装脚本
|
||||
- \`docker-compose-v4.yml\` - Docker Compose 配置 (IPv4)
|
||||
- \`docker-compose-v6.yml\` - Docker Compose 配置 (IPv6)
|
||||
|
||||
## ℹ️ Build Details
|
||||
- Build date: $(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
- Branch: ${{ github.ref_name }}" \
|
||||
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})" \
|
||||
--repo ${{ github.repository }}
|
||||
|
||||
# 上传所有文件到 release
|
||||
echo "📤 上传 GOST 二进制..."
|
||||
gh release upload "${VERSION}" ./artifacts/gost --clobber
|
||||
echo "📤 上传 GOST 二进制文件..."
|
||||
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
|
||||
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
|
||||
|
||||
echo "📤 上传安装脚本..."
|
||||
gh release upload "${VERSION}" ./install.sh --clobber
|
||||
@@ -230,3 +244,34 @@ jobs:
|
||||
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
|
||||
|
||||
echo "✅ 所有文件已上传到 Release ${VERSION}"
|
||||
|
||||
update-release-files:
|
||||
name: Update Release Files Only
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.tag_exists == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Update Release Files
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
|
||||
echo "📤 更新 Release ${VERSION} 中的脚本和配置文件..."
|
||||
|
||||
# 上传安装脚本(覆盖)
|
||||
echo "📤 更新安装脚本..."
|
||||
gh release upload "${VERSION}" ./install.sh --clobber
|
||||
gh release upload "${VERSION}" ./panel_install.sh --clobber
|
||||
|
||||
# 上传 Docker Compose 配置文件(覆盖)
|
||||
echo "📤 更新 Docker Compose 配置文件..."
|
||||
gh release upload "${VERSION}" ./docker-compose-v4.yml --clobber
|
||||
gh release upload "${VERSION}" ./docker-compose-v6.yml --clobber
|
||||
|
||||
echo "✅ 文件更新完成!Release ${VERSION} 中的脚本和配置已更新"
|
||||
echo "ℹ️ Docker 镜像和 GOST 二进制未重新构建"
|
||||
|
||||
@@ -1,6 +1,39 @@
|
||||
|
||||
# flux-panel转发面板 哆啦A梦转发面板
|
||||
|
||||
# 赞助商
|
||||
<p align="center">
|
||||
<a href="https://vps.town" style="margin: 0 20px; text-align:center;">
|
||||
<img src="./doc/vpstown.png" width="300">
|
||||
</a>
|
||||
|
||||
<a href="https://whmcs.as211392.com" style="margin: 0 20px; text-align:center;">
|
||||
<img src="./doc/as211392.png" width="300">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 1.x无法无损升级到2.x请备份好数据
|
||||
1.x迁移2.x流程如下
|
||||
1.进入1.x的后台在转发页面导出所有转发保存
|
||||
2.使用1.x节点脚本卸载所有节点
|
||||
```
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
```
|
||||
3.使用1.x面版脚本卸载面板
|
||||
|
||||
```
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
4.使用2.x脚本安装面板
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
|
||||
使用1.x版本的请使用上面给出的脚本,不要使用最新的,最新的默认2.x
|
||||
嫌弃麻烦或者想稳定的我不推荐你升级到2.x版本 目前还在beta阶段,很多问题。
|
||||
|
||||
|
||||
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
|
||||
---
|
||||
@@ -18,11 +51,21 @@
|
||||
---
|
||||
### Docker Compose部署
|
||||
#### 快速部署
|
||||
面板端:
|
||||
面板端(1.x):
|
||||
```bash
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端(1.x):
|
||||
```bash
|
||||
curl -L https://github.com/bqlpfy/flux-panel/releases/download/1.4.2/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
```
|
||||
|
||||
面板端(2.x):
|
||||
```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
|
||||
```
|
||||
节点端:
|
||||
节点端(2.x):
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/bqlpfy/flux-panel/refs/heads/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
|
||||
|
||||
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:
|
||||
backend:
|
||||
image: bqlpfy/springboot-backend:2.0.0-beta
|
||||
image: bqlpfy/springboot-backend:2.0.4-beta
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -15,6 +15,8 @@ services:
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
interval: 30s
|
||||
@@ -23,7 +25,7 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:2.0.0-beta
|
||||
image: bqlpfy/vite-frontend:2.0.4-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
backend:
|
||||
image: bqlpfy/springboot-backend:2.0.0-beta
|
||||
image: bqlpfy/springboot-backend:2.0.4-beta
|
||||
container_name: springboot-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
@@ -15,6 +15,8 @@ services:
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
interval: 30s
|
||||
@@ -23,7 +25,7 @@ services:
|
||||
start_period: 60s
|
||||
|
||||
frontend:
|
||||
image: bqlpfy/vite-frontend:2.0.0-beta
|
||||
image: bqlpfy/vite-frontend:2.0.4-beta
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
|
||||
+25
-2
@@ -1,7 +1,30 @@
|
||||
#!/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/bqlpfy/flux-panel/releases/download/2.0.4-beta/gost-${ARCH}"
|
||||
}
|
||||
|
||||
# 下载地址
|
||||
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/gost"
|
||||
INSTALL_DIR="/etc/gost"
|
||||
DOWNLOAD_URL=$(build_download_url)
|
||||
INSTALL_DIR="/etc/flux_agent"
|
||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
# 拼接 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_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.0-beta/docker-compose-v6.yml"
|
||||
DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.4-beta/docker-compose-v4.yml"
|
||||
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.4-beta/docker-compose-v6.yml"
|
||||
|
||||
COUNTRY=$(curl -s https://ipinfo.io/country)
|
||||
if [ "$COUNTRY" = "CN" ]; then
|
||||
@@ -234,7 +234,15 @@ update_panel() {
|
||||
configure_docker_ipv6
|
||||
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
|
||||
|
||||
echo "⬇️ 拉取最新镜像..."
|
||||
|
||||
@@ -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 \
|
||||
&& sed -i 's/security.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& 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 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
@@ -21,4 +21,6 @@ public class ForwardDto {
|
||||
private String remoteAddr;
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
}
|
||||
@@ -25,4 +25,6 @@ public class ForwardUpdateDto {
|
||||
|
||||
private String strategy;
|
||||
|
||||
private Integer inPort;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.admin.common.task;
|
||||
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.entity.*;
|
||||
import com.admin.service.*;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.admin.common.utils;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.admin.common.dto.GostDto;
|
||||
import com.admin.entity.*;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
@@ -12,26 +14,34 @@ import java.util.Objects;
|
||||
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);
|
||||
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 req = new JSONObject();
|
||||
req.put("limiter", name + "");
|
||||
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();
|
||||
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();
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
JSONObject dialer = new JSONObject();
|
||||
@@ -43,19 +53,23 @@ public class GostUtil {
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject node = new JSONObject();
|
||||
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("dialer", dialer);
|
||||
|
||||
if (StringUtils.isNotBlank(node_info.getInterfaceName())) {
|
||||
node.put("interface", node_info.getInterfaceName());
|
||||
}
|
||||
|
||||
|
||||
nodes.add(node);
|
||||
}
|
||||
JSONObject hop = new JSONObject();
|
||||
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();
|
||||
selector.put("strategy", chainTunnels.getFirst().getStrategy());
|
||||
selector.put("maxFails", 1);
|
||||
@@ -72,24 +86,34 @@ public class GostUtil {
|
||||
data.put("name", "chains_" + chainTunnels.getFirst().getTunnelId());
|
||||
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();
|
||||
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();
|
||||
Node node_info = node_s.get(chainTunnel.getNodeId());
|
||||
JSONObject service_item = new JSONObject();
|
||||
service_item.put("name", chainTunnel.getTunnelId() + "_tls");
|
||||
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();
|
||||
metadata.put("interface", node_info.getInterfaceName());
|
||||
metadata.put("interface", node_s.get(node_id).getInterfaceName());
|
||||
service_item.put("metadata", metadata);
|
||||
}
|
||||
|
||||
@@ -106,16 +130,14 @@ public class GostUtil {
|
||||
|
||||
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) {
|
||||
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) {
|
||||
public static GostDto AddAndUpdateService(String name, Integer limiter, Node node, Forward forward, ForwardPort forwardPort, Tunnel tunnel, String meth) {
|
||||
JSONArray services = new JSONArray();
|
||||
String[] protocols = {"tcp", "udp"};
|
||||
for (String protocol : protocols) {
|
||||
@@ -127,7 +149,8 @@ public class GostUtil {
|
||||
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();
|
||||
metadata.put("interface", node.getInterfaceName());
|
||||
service.put("metadata", metadata);
|
||||
@@ -155,22 +178,30 @@ public class GostUtil {
|
||||
|
||||
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();
|
||||
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();
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(name + "_tcp");
|
||||
services.add(name + "_udp");
|
||||
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++;
|
||||
}
|
||||
|
||||
if (strategy == null || strategy.equals("")) {
|
||||
if (strategy == null || strategy.isEmpty()) {
|
||||
strategy = "fifo";
|
||||
}
|
||||
|
||||
@@ -222,5 +253,42 @@ public class GostUtil {
|
||||
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");
|
||||
|
||||
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
|
||||
log.info("屏蔽协议数据:http->{}, tls->{}, socks->{}", http, tls, socks);
|
||||
|
||||
// 检查是否已有该节点的连接,如果有则记录日志但直接覆盖
|
||||
WebSocketSession existingSession = nodeSessions.get(nodeId);
|
||||
|
||||
@@ -3,8 +3,11 @@ package com.admin.config;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.Statement;
|
||||
@@ -12,9 +15,11 @@ import java.sql.Statement;
|
||||
/**
|
||||
* SQLite 数据库配置
|
||||
* 启用 WAL (Write-Ahead Logging) 模式以提高并发性能
|
||||
* 添加定期 checkpoint 和优雅关闭处理
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@EnableScheduling
|
||||
public class SQLiteConfig implements ApplicationRunner {
|
||||
|
||||
private final DataSource dataSource;
|
||||
@@ -33,10 +38,46 @@ public class SQLiteConfig implements ApplicationRunner {
|
||||
statement.execute("PRAGMA cache_size=-64000;"); // 64MB 缓存
|
||||
statement.execute("PRAGMA temp_store=MEMORY;");
|
||||
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) {
|
||||
log.error("Failed to configure SQLite database", 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,4 @@ public interface TunnelService extends IService<Tunnel> {
|
||||
* @return 诊断结果
|
||||
*/
|
||||
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.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.*;
|
||||
@@ -82,53 +83,60 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
// 判断是否使用隧道的inIp
|
||||
boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty();
|
||||
|
||||
// 收集所有的IP列表
|
||||
List<String> ipList = new ArrayList<>();
|
||||
// 收集所有的端口列表
|
||||
List<Integer> portList = new ArrayList<>();
|
||||
Set<String> ipPortSet = new LinkedHashSet<>();
|
||||
|
||||
if (useTunnelInIp) {
|
||||
// 使用隧道的inIp
|
||||
// 使用隧道的inIp(求笛卡尔积)
|
||||
List<String> ipList = new ArrayList<>();
|
||||
List<Integer> portList = new ArrayList<>();
|
||||
|
||||
String[] tunnelInIps = tunnel.getInIp().split(",");
|
||||
for (String ip : tunnelInIps) {
|
||||
if (ip != null && !ip.trim().isEmpty()) {
|
||||
ipList.add(ip.trim());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 使用节点的serverIp
|
||||
|
||||
// 收集所有端口
|
||||
for (ForwardPort forwardPort : forwardPorts) {
|
||||
Node node = nodeService.getById(forwardPort.getNodeId());
|
||||
if (node != null && node.getServerIp() != null) {
|
||||
ipList.add(node.getServerIp());
|
||||
if (forwardPort.getPort() != null) {
|
||||
portList.add(forwardPort.getPort());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收集所有端口
|
||||
for (ForwardPort forwardPort : forwardPorts) {
|
||||
if (forwardPort.getPort() != null) {
|
||||
portList.add(forwardPort.getPort());
|
||||
|
||||
// 去重
|
||||
List<String> uniqueIps = ipList.stream().distinct().toList();
|
||||
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
||||
|
||||
// 组合 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());
|
||||
}
|
||||
}
|
||||
|
||||
// 去重
|
||||
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和端口
|
||||
// 设置入口IP
|
||||
if (!ipPortSet.isEmpty()) {
|
||||
forward.setInIp(String.join(",", ipPortSet));
|
||||
// inPort设置为第一个端口(用于向后兼容)
|
||||
forward.setInPort(uniquePorts.getFirst());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,25 +167,43 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
forward.setUserName(currentUser.getUserName());
|
||||
forward.setCreatedTime(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());
|
||||
this.save(forward);
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1));
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
Integer nodePort = tunnelService.getNodePort(chainTunnel.getNodeId(), 2);
|
||||
|
||||
ForwardPort forwardPort = new ForwardPort();
|
||||
forwardPort.setForwardId(forward.getId());
|
||||
forwardPort.setNodeId(chainTunnel.getNodeId());
|
||||
forwardPort.setPort(nodePort);
|
||||
forwardPort.setPort(chainTunnel.getPort());
|
||||
forwardPortService.save(forwardPort);
|
||||
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), permissionResult.getUserTunnel());
|
||||
Integer limiter = permissionResult.getLimiter();
|
||||
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
if (node == null){
|
||||
if (node == null) {
|
||||
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();
|
||||
}
|
||||
@@ -205,7 +231,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
return R.err(permissionResult.getErrorMessage());
|
||||
}
|
||||
|
||||
UserTunnel userTunnel = null;
|
||||
UserTunnel userTunnel;
|
||||
if (currentUser.getRoleId() != 0) {
|
||||
userTunnel = getUserTunnel(currentUser.getUserId(), tunnel.getId().intValue());
|
||||
if (userTunnel == null) {
|
||||
@@ -225,18 +251,22 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
|
||||
|
||||
List<ChainTunnel> chainTunnels = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", tunnel.getId()).eq("chain_type", 1));
|
||||
chainTunnels = get_port(chainTunnels, forwardUpdateDto.getInPort());
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
String serviceName = buildServiceName(existForward.getId(), existForward.getUserId(), userTunnel);
|
||||
Integer limiter = permissionResult.getLimiter();
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
if (node == null){
|
||||
if (node == null) {
|
||||
return R.err("部分节点不存在");
|
||||
}
|
||||
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");
|
||||
}
|
||||
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, tunnel, "UpdateService");
|
||||
if (!Objects.equals(gostDto.getMsg(), "OK")) return R.err(gostDto.getMsg());
|
||||
}
|
||||
|
||||
return R.ok();
|
||||
@@ -282,7 +312,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
|
||||
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel);
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
if (node == null){
|
||||
if (node == null) {
|
||||
return R.err("部分节点不存在");
|
||||
}
|
||||
|
||||
@@ -298,7 +328,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
|
||||
@Override
|
||||
public R pauseForward(Long id) {
|
||||
return changeForwardStatus(id, 0, "PauseService");
|
||||
return changeForwardStatus(id, 0, "PauseService");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -393,7 +423,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
// 1. 入口->第一跳(或出口)
|
||||
for (ChainTunnel inNode : inNodes) {
|
||||
Node fromNode = nodeService.getById(inNode.getNodeId());
|
||||
|
||||
|
||||
if (fromNode != null) {
|
||||
if (!chainNodesList.isEmpty()) {
|
||||
for (ChainTunnel firstChainNode : chainNodesList.getFirst()) {
|
||||
@@ -429,10 +459,10 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
// 2. 链路测试
|
||||
for (int i = 0; i < chainNodesList.size(); i++) {
|
||||
List<ChainTunnel> currentHop = chainNodesList.get(i);
|
||||
|
||||
|
||||
for (ChainTunnel currentNode : currentHop) {
|
||||
Node fromNode = nodeService.getById(currentNode.getNodeId());
|
||||
|
||||
|
||||
if (fromNode != null) {
|
||||
if (i + 1 < chainNodesList.size()) {
|
||||
for (ChainTunnel nextNode : chainNodesList.get(i + 1)) {
|
||||
@@ -500,65 +530,57 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public R updateForwardOrder(Map<String, Object> params) {
|
||||
try {
|
||||
// 1. 获取当前用户信息
|
||||
UserInfo currentUser = getCurrentUserInfo();
|
||||
// 1. 获取当前用户信息
|
||||
UserInfo currentUser = getCurrentUserInfo();
|
||||
|
||||
// 2. 验证参数
|
||||
if (!params.containsKey("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());
|
||||
// 2. 验证参数
|
||||
if (!params.containsKey("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. 执行批量更新
|
||||
this.updateBatchById(forwardsToUpdate);
|
||||
return R.ok();
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -614,10 +636,11 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
for (ChainTunnel chainTunnel : chainTunnels) {
|
||||
String serviceName = buildServiceName(forward.getId(), forward.getUserId(), userTunnel);
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
if (node == null){
|
||||
if (node == null) {
|
||||
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.setUpdatedTime(System.currentTimeMillis());
|
||||
@@ -916,6 +939,94 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
return forwardId + "_" + userId + "_" + userTunnelId;
|
||||
}
|
||||
|
||||
|
||||
public List<ChainTunnel> get_port(List<ChainTunnel> chainTunnelList, Integer in_port) {
|
||||
List<List<Integer>> list = new ArrayList<>();
|
||||
|
||||
// 获取每个节点的端口列表
|
||||
for (ChainTunnel tunnel : chainTunnelList) {
|
||||
List<Integer> nodePort = getNodePort(tunnel.getNodeId());
|
||||
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.get(0));
|
||||
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) {
|
||||
|
||||
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));
|
||||
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
|
||||
@@ -966,7 +1077,7 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
|
||||
private double averageTime;
|
||||
private double packetLoss;
|
||||
private long timestamp;
|
||||
|
||||
|
||||
// 链路类型相关字段
|
||||
private Integer fromChainType; // 1: 入口, 2: 链, 3: 出口
|
||||
private Integer fromInx;
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.admin.common.dto.GostDto;
|
||||
import com.admin.common.dto.NodeDto;
|
||||
import com.admin.common.dto.NodeUpdateDto;
|
||||
import com.admin.common.lang.R;
|
||||
import com.admin.common.utils.GostUtil;
|
||||
import com.admin.common.utils.WebSocketServer;
|
||||
import com.admin.entity.*;
|
||||
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"));
|
||||
if (viteConfig == null) return R.err("请先前往网站配置中设置ip");
|
||||
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.4-beta/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")
|
||||
.append(" -a ").append(processedServerAddr) // 服务器地址
|
||||
.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.mapper.SpeedLimitMapper;
|
||||
import com.admin.service.*;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.Data;
|
||||
@@ -19,6 +20,7 @@ import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@@ -65,11 +67,23 @@ public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLi
|
||||
|
||||
String speedInMBps = convertBitsToMBps(speedLimit.getSpeed());
|
||||
|
||||
List<Long> limit_success = new ArrayList<>();
|
||||
|
||||
List<ChainTunnel> tunnelList = chainTunnelService.list(new QueryWrapper<ChainTunnel>().eq("tunnel_id", speedLimit.getTunnelId()));
|
||||
for (ChainTunnel chainTunnel : tunnelList) {
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
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();
|
||||
@@ -94,7 +108,8 @@ public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLi
|
||||
for (ChainTunnel chainTunnel : tunnelList) {
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
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);
|
||||
@@ -116,7 +131,8 @@ public class SpeedLimitServiceImpl extends ServiceImpl<SpeedLimitMapper, SpeedLi
|
||||
for (ChainTunnel chainTunnel : tunnelList) {
|
||||
Node node = nodeService.getById(chainTunnel.getNodeId());
|
||||
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);
|
||||
|
||||
@@ -81,7 +81,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
Node node = nodeService.getById(chain_node.getNodeId());
|
||||
if (node == null) return R.err("节点不存在");
|
||||
nodes.put(node.getId(), node);
|
||||
Integer nodePort = getNodePort(chain_node.getNodeId(), 1);
|
||||
Integer nodePort = getNodePort(chain_node.getNodeId());
|
||||
chain_node.setPort(nodePort);
|
||||
chain_node.setInx(inx); // 设置转发链序号
|
||||
chainTunnels.add(chain_node);
|
||||
@@ -93,7 +93,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
Node node = nodeService.getById(out_node.getNodeId());
|
||||
if (node == null) return R.err("节点不存在");
|
||||
nodes.put(node.getId(), node);
|
||||
Integer nodePort = getNodePort(out_node.getNodeId(), 1);
|
||||
Integer nodePort = getNodePort(out_node.getNodeId());
|
||||
out_node.setPort(nodePort);
|
||||
chainTunnels.add(out_node);
|
||||
}
|
||||
@@ -132,14 +132,35 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
}
|
||||
chainTunnelService.saveBatch(chainTunnels);
|
||||
|
||||
List<JSONObject> chain_success = new ArrayList<>();
|
||||
List<JSONObject> service_success = new ArrayList<>();
|
||||
|
||||
|
||||
|
||||
if (tunnel.getType() == 2) {
|
||||
|
||||
for (ChainTunnel in_node : tunnelDto.getInNodeId()) {
|
||||
// 创建Chain, 指向chainNode的第一跳。如果chainNode为空就是指向出口
|
||||
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 {
|
||||
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) {
|
||||
int inx = i+1;
|
||||
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 {
|
||||
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()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -286,12 +367,12 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
GostUtil.DeleteChains(chainTunnel.getNodeId(), "chains_" + chainTunnel.getTunnelId());
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(chainTunnel.getTunnelId() + "_tls");
|
||||
GostUtil.DeleteChainService(chainTunnel.getNodeId(), services);
|
||||
GostUtil.DeleteService(chainTunnel.getNodeId(), services);
|
||||
}
|
||||
else { // 出口
|
||||
JSONArray services = new JSONArray();
|
||||
services.add(chainTunnel.getTunnelId() + "_tls");
|
||||
GostUtil.DeleteChainService(chainTunnel.getNodeId(), services);
|
||||
GostUtil.DeleteService(chainTunnel.getNodeId(), services);
|
||||
}
|
||||
}
|
||||
chainTunnelService.remove(new QueryWrapper<ChainTunnel>().eq("tunnel_id", id));
|
||||
@@ -471,8 +552,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
return R.ok(diagnosisReport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getNodePort(Long nodeId,Integer type) {
|
||||
public Integer getNodePort(Long nodeId) {
|
||||
|
||||
Node node = nodeService.getById(nodeId);
|
||||
if (node == null){
|
||||
@@ -505,11 +585,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
if (availablePorts.isEmpty()) {
|
||||
throw new RuntimeException("节点端口已满,无可用端口");
|
||||
}
|
||||
if (type == 1) {
|
||||
return availablePorts.getLast();
|
||||
}else {
|
||||
return availablePorts.getFirst();
|
||||
}
|
||||
return availablePorts.getFirst();
|
||||
}
|
||||
|
||||
public static List<Integer> parsePorts(String input) {
|
||||
@@ -531,6 +607,10 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
|
||||
return set.stream().sorted().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void isError(GostDto gostDto){
|
||||
|
||||
}
|
||||
|
||||
private DiagnosisResult performTcpPingDiagnosis(Node node, String targetIp, int port, String description) {
|
||||
try {
|
||||
// 构建TCP ping请求数据
|
||||
|
||||
@@ -264,44 +264,61 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
|
||||
for (UserPackageDto.UserForwardDetailDto forward : forwards) {
|
||||
Tunnel tunnel = tunnelService.getById(forward.getTunnelId());
|
||||
if (tunnel == null) continue;
|
||||
|
||||
List<ForwardPort> forwardPorts = forwardPortService.list(
|
||||
new QueryWrapper<ForwardPort>().eq("forward_id", forward.getId())
|
||||
);
|
||||
if (forwardPorts.isEmpty()) continue;
|
||||
|
||||
boolean useTunnelInIp = tunnel.getInIp() != null && !tunnel.getInIp().trim().isEmpty();
|
||||
List<String> ipList = new ArrayList<>();
|
||||
List<Integer> portList = new ArrayList<>();
|
||||
java.util.Set<String> ipPortSet = new java.util.LinkedHashSet<>();
|
||||
|
||||
if (useTunnelInIp) {
|
||||
// 使用隧道的inIp(求笛卡尔积)
|
||||
List<String> ipList = new ArrayList<>();
|
||||
List<Integer> portList = new ArrayList<>();
|
||||
|
||||
String[] tunnelInIps = tunnel.getInIp().split(",");
|
||||
for (String ip : tunnelInIps) {
|
||||
if (ip != null && !ip.trim().isEmpty()) {
|
||||
ipList.add(ip.trim());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
for (ForwardPort forwardPort : forwardPorts) {
|
||||
Node node = nodeService.getById(forwardPort.getNodeId());
|
||||
if (node != null && node.getServerIp() != null) {
|
||||
ipList.add(node.getServerIp());
|
||||
if (forwardPort.getPort() != null) {
|
||||
portList.add(forwardPort.getPort());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ForwardPort forwardPort : forwardPorts) {
|
||||
if (forwardPort.getPort() != null) {
|
||||
portList.add(forwardPort.getPort());
|
||||
}
|
||||
}
|
||||
List<String> uniqueIps = ipList.stream().distinct().toList();
|
||||
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
||||
java.util.Set<String> ipPortSet = new java.util.LinkedHashSet<>();
|
||||
for (String ip : uniqueIps) {
|
||||
for (Integer port : uniquePorts) {
|
||||
ipPortSet.add(ip + ":" + port);
|
||||
|
||||
List<String> uniqueIps = ipList.stream().distinct().toList();
|
||||
List<Integer> uniquePorts = portList.stream().distinct().toList();
|
||||
|
||||
for (String ip : uniqueIps) {
|
||||
for (Integer port : uniquePorts) {
|
||||
ipPortSet.add(ip + ":" + port);
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
if (!forwardPorts.isEmpty() && forwardPorts.getFirst().getPort() != null) {
|
||||
forward.setInPort(forwardPorts.getFirst().getPort());
|
||||
}
|
||||
}
|
||||
|
||||
if (!ipPortSet.isEmpty()) {
|
||||
forward.setInIp(String.join(",", ipPortSet));
|
||||
forward.setInPort(uniquePorts.getFirst());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
|
||||
|
||||
// 缓存相关常量
|
||||
const CACHE_PREFIX = 'vite_config_';
|
||||
const VERSION = "2.0.0-beta";
|
||||
const VERSION = "2.0.4-beta";
|
||||
const APP_VERSION = "1.0.3";
|
||||
|
||||
const getInitialConfig = () => {
|
||||
|
||||
@@ -406,6 +406,14 @@ export default function ForwardPage() {
|
||||
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()) {
|
||||
newErrors.remoteAddr = '请输入远程地址';
|
||||
} else {
|
||||
@@ -526,6 +534,7 @@ export default function ForwardPage() {
|
||||
userId: form.userId,
|
||||
name: form.name,
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
@@ -535,6 +544,7 @@ export default function ForwardPage() {
|
||||
const createData = {
|
||||
name: form.name,
|
||||
tunnelId: form.tunnelId,
|
||||
inPort: form.inPort,
|
||||
remoteAddr: processedRemoteAddr,
|
||||
strategy: addressCount > 1 ? form.strategy : 'fifo'
|
||||
};
|
||||
@@ -1605,6 +1615,24 @@ export default function ForwardPage() {
|
||||
</SelectItem>
|
||||
))}
|
||||
</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
|
||||
label="远程地址"
|
||||
|
||||
+123
-122
@@ -9,6 +9,7 @@ import { Switch } from "@heroui/switch";
|
||||
import { Spinner } from "@heroui/spinner";
|
||||
import { Alert } from "@heroui/alert";
|
||||
import { Progress } from "@heroui/progress";
|
||||
import { Accordion, AccordionItem } from "@heroui/accordion";
|
||||
import toast from 'react-hot-toast';
|
||||
import axios from 'axios';
|
||||
|
||||
@@ -875,141 +876,141 @@ export default function NodePage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<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请求远程地址,不懂的默认为空就行"
|
||||
/>
|
||||
{/* 高级配置 */}
|
||||
<Accordion variant="bordered">
|
||||
<AccordionItem
|
||||
key="advanced"
|
||||
aria-label="高级配置"
|
||||
title="高级配置"
|
||||
>
|
||||
<div className="space-y-4 pb-2">
|
||||
<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">
|
||||
<Input
|
||||
label="TCP监听地址"
|
||||
placeholder="请输入TCP监听地址"
|
||||
value={form.tcpListenAddr}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, tcpListenAddr: e.target.value }))}
|
||||
isInvalid={!!errors.tcpListenAddr}
|
||||
errorMessage={errors.tcpListenAddr}
|
||||
variant="bordered"
|
||||
startContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">TCP</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="TCP监听地址"
|
||||
placeholder="请输入TCP监听地址"
|
||||
value={form.tcpListenAddr}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, tcpListenAddr: e.target.value }))}
|
||||
isInvalid={!!errors.tcpListenAddr}
|
||||
errorMessage={errors.tcpListenAddr}
|
||||
variant="bordered"
|
||||
startContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">TCP</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="UDP监听地址"
|
||||
placeholder="请输入UDP监听地址"
|
||||
value={form.udpListenAddr}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, udpListenAddr: e.target.value }))}
|
||||
isInvalid={!!errors.udpListenAddr}
|
||||
errorMessage={errors.udpListenAddr}
|
||||
variant="bordered"
|
||||
startContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">UDP</span>
|
||||
</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 }))}
|
||||
<Input
|
||||
label="UDP监听地址"
|
||||
placeholder="请输入UDP监听地址"
|
||||
value={form.udpListenAddr}
|
||||
onChange={(e) => setForm(prev => ({ ...prev, udpListenAddr: e.target.value }))}
|
||||
isInvalid={!!errors.udpListenAddr}
|
||||
errorMessage={errors.udpListenAddr}
|
||||
variant="bordered"
|
||||
startContent={
|
||||
<div className="pointer-events-none flex items-center">
|
||||
<span className="text-default-400 text-small">UDP</span>
|
||||
</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 */}
|
||||
<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="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>
|
||||
{/* 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="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>
|
||||
<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 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>
|
||||
|
||||
|
||||
|
||||
<Alert
|
||||
<Alert
|
||||
color="danger"
|
||||
variant="flat"
|
||||
description="请不要在出口节点执行屏蔽协议,否则可能影响转发;屏蔽协议仅需在入口节点执行。"
|
||||
className="mt-3"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
<Alert
|
||||
color="primary"
|
||||
variant="flat"
|
||||
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址。"
|
||||
className="mt-4"
|
||||
/>
|
||||
color="primary"
|
||||
variant="flat"
|
||||
description="服务器ip是你要添加的服务器的ip地址,不是面板的ip地址。"
|
||||
className="mt-4"
|
||||
/>
|
||||
|
||||
<Alert
|
||||
color="primary"
|
||||
variant="flat"
|
||||
title="TCP,UDP监听地址"
|
||||
description="V6或者双栈填写[::],V4填写0.0.0.0。不懂的就去看文档网站内的说明"
|
||||
className="mt-4"
|
||||
/>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -357,6 +357,9 @@ export default function TunnelPage() {
|
||||
.map(group => group.filter(node => node.nodeId !== -1))
|
||||
.filter(group => group.length > 0); // 移除空组
|
||||
|
||||
// 过滤掉出口节点中的占位节点
|
||||
const cleanedOutNodeId = (form.outNodeId || []).filter(node => node.nodeId !== -1);
|
||||
|
||||
// 将换行符分隔的IP转换为逗号分隔
|
||||
const inIpString = form.inIp
|
||||
.split('\n')
|
||||
@@ -367,6 +370,7 @@ export default function TunnelPage() {
|
||||
const data = {
|
||||
...form,
|
||||
inIp: inIpString,
|
||||
outNodeId: cleanedOutNodeId,
|
||||
chainNodes: cleanedChainNodes
|
||||
};
|
||||
|
||||
@@ -1052,7 +1056,7 @@ export default function TunnelPage() {
|
||||
label="节点"
|
||||
placeholder="请选择出口节点(可多选)"
|
||||
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={[
|
||||
...nodes.filter(node => node.status !== 1).map(node => node.id.toString()),
|
||||
...form.inNodeId.map(ct => ct.nodeId.toString()),
|
||||
@@ -1061,12 +1065,17 @@ export default function TunnelPage() {
|
||||
onSelectionChange={(keys) => {
|
||||
const selectedIds = Array.from(keys).map(key => parseInt(key as string));
|
||||
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 existing = currentOutNodes.find(ct => ct.nodeId === nodeId);
|
||||
const existing = realNodes.find(ct => ct.nodeId === nodeId);
|
||||
return existing || { nodeId, chainType: 3, protocol, strategy };
|
||||
});
|
||||
setForm(prev => ({ ...prev, outNodeId: newOutNodeId }));
|
||||
@@ -1116,14 +1125,30 @@ export default function TunnelPage() {
|
||||
<Select
|
||||
label="协议"
|
||||
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) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
if (selectedKey && form.outNodeId) {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
outNodeId: (prev.outNodeId || []).map(ct => ({ ...ct, protocol: selectedKey }))
|
||||
}));
|
||||
if (selectedKey) {
|
||||
setForm(prev => {
|
||||
const currentOutNodes = prev.outNodeId || [];
|
||||
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}
|
||||
@@ -1147,14 +1172,28 @@ export default function TunnelPage() {
|
||||
<Select
|
||||
label="负载策略"
|
||||
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) => {
|
||||
const selectedKey = Array.from(keys)[0] as string;
|
||||
if (selectedKey && form.outNodeId) {
|
||||
setForm(prev => ({
|
||||
...prev,
|
||||
outNodeId: (prev.outNodeId || []).map(ct => ({ ...ct, strategy: selectedKey }))
|
||||
}));
|
||||
if (selectedKey) {
|
||||
setForm(prev => {
|
||||
const currentOutNodes = prev.outNodeId || [];
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user