Compare commits

..

1 Commits

Author SHA1 Message Date
qaq 4fef8eeb32 修复出口协议无法选择,加入自定义转发端口,增加arm支持 2025-11-19 16:32:48 +08:00
14 changed files with 157 additions and 47 deletions
+44 -15
View File
@@ -2,7 +2,7 @@ name: Build and Push Images Based on Version
# 在这里定义统一版本号
env:
VERSION: "2.0.1-beta"
VERSION: "2.0.2-beta"
on:
push:
@@ -20,7 +20,7 @@ jobs:
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # 获取所有历史和 tags
fetch-depth: 0
- name: Display version
run: |
@@ -67,22 +67,38 @@ 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
- name: Compress with UPX
- 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 (AMD64)
working-directory: ./go-gost
run: |
upx --best --lzma gost
- name: Upload GOST artifact
- 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
name: gost-binary-amd64
path: ./go-gost/gost
- 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
needs: check-version
@@ -109,7 +125,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 +169,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 +185,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 ./artifacts/gost
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
- name: Create Release
env:
@@ -202,9 +229,10 @@ jobs:
- **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\`
- **GOST Binary AMD64** - \`gost\` (Linux AMD64, compressed with UPX)
- **GOST Binary ARM64** - \`gost-arm64\` (Linux ARM64, compressed with UPX)
- **Vite Frontend** Docker Image: \`bqlpfy/vite-frontend:${VERSION}\` / \`bqlpfy/vite-frontend:latest\` (支持 AMD64/ARM64)
- **Spring Boot Backend** Docker Image: \`bqlpfy/springboot-backend:${VERSION}\` / \`bqlpfy/springboot-backend:latest\` (支持 AMD64/ARM64)
## 📥 Installation Files
- \`install.sh\` - GOST 客户端安装脚本
@@ -218,8 +246,9 @@ jobs:
--repo ${{ github.repository }}
# 上传所有文件到 release
echo "📤 上传 GOST 二进制..."
echo "📤 上传 GOST 二进制文件..."
gh release upload "${VERSION}" ./artifacts/gost --clobber
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
echo "📤 上传安装脚本..."
gh release upload "${VERSION}" ./install.sh --clobber
+2 -2
View File
@@ -1,6 +1,6 @@
services:
backend:
image: bqlpfy/springboot-backend:2.0.1-beta
image: bqlpfy/springboot-backend:2.0.2-beta
container_name: springboot-backend
restart: unless-stopped
environment:
@@ -23,7 +23,7 @@ services:
start_period: 60s
frontend:
image: bqlpfy/vite-frontend:2.0.1-beta
image: bqlpfy/vite-frontend:2.0.2-beta
container_name: vite-frontend
restart: unless-stopped
ports:
+2 -2
View File
@@ -1,6 +1,6 @@
services:
backend:
image: bqlpfy/springboot-backend:2.0.1-beta
image: bqlpfy/springboot-backend:2.0.2-beta
container_name: springboot-backend
restart: unless-stopped
environment:
@@ -23,7 +23,7 @@ services:
start_period: 60s
frontend:
image: bqlpfy/vite-frontend:2.0.1-beta
image: bqlpfy/vite-frontend:2.0.2-beta
container_name: vite-frontend
restart: unless-stopped
ports:
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
# 下载地址
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.1-beta/gost"
DOWNLOAD_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/gost"
INSTALL_DIR="/etc/flux_agent"
COUNTRY=$(curl -s https://ipinfo.io/country)
if [ "$COUNTRY" = "CN" ]; then
+2 -2
View File
@@ -8,8 +8,8 @@ export LC_ALL=C
# 全局下载地址配置
DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.1-beta/docker-compose-v4.yml"
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.1-beta/docker-compose-v6.yml"
DOCKER_COMPOSEV4_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/docker-compose-v4.yml"
DOCKER_COMPOSEV6_URL="https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/docker-compose-v6.yml"
COUNTRY=$(curl -s https://ipinfo.io/country)
if [ "$COUNTRY" = "CN" ]; then
@@ -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;
}
@@ -57,5 +57,5 @@ public interface TunnelService extends IService<Tunnel> {
*/
R diagnoseTunnel(Long tunnelId);
Integer getNodePort(Long nodeId, Integer type);
Integer getNodePort(Long nodeId, Integer type, Integer port);
}
@@ -163,7 +163,7 @@ 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));
for (ChainTunnel chainTunnel : chainTunnels) {
Integer nodePort = tunnelService.getNodePort(chainTunnel.getNodeId(), 2);
Integer nodePort = tunnelService.getNodePort(chainTunnel.getNodeId(), 2, forwardDto.getInPort());
ForwardPort forwardPort = new ForwardPort();
forwardPort.setForwardId(forward.getId());
@@ -205,7 +205,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) {
@@ -236,6 +236,13 @@ public class ForwardServiceImpl extends ServiceImpl<ForwardMapper, Forward> impl
if (forwardPort == null){
return R.err("部分节点不存在1");
}
if (forwardUpdateDto.getInPort() != null && !forwardUpdateDto.getInPort().equals(forwardPort.getPort())) {
Integer nodePort = tunnelService.getNodePort(forwardPort.getNodeId(), 2, forwardUpdateDto.getInPort());
if (Objects.equals(nodePort, forwardUpdateDto.getInPort())) {
forwardPort.setPort(nodePort);
forwardPortService.updateById(forwardPort);
}
}
GostUtil.AddAndUpdateService(serviceName, limiter, node, existForward, forwardPort, tunnel, "UpdateService");
}
@@ -125,7 +125,7 @@ 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.1-beta/install.sh")
command.append("curl -L https://github.com/bqlpfy/flux-panel/releases/download/2.0.2-beta/install.sh")
.append(" -o ./install.sh && chmod +x ./install.sh && ");
String processedServerAddr = processServerAddress(viteConfig.getValue());
command.append("./install.sh")
@@ -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(), 1, null);
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(), 1, null);
out_node.setPort(nodePort);
chainTunnels.add(out_node);
}
@@ -472,7 +472,7 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
}
@Override
public Integer getNodePort(Long nodeId,Integer type) {
public Integer getNodePort(Long nodeId,Integer type, Integer port) {
Node node = nodeService.getById(nodeId);
if (node == null){
@@ -508,6 +508,9 @@ public class TunnelServiceImpl extends ServiceImpl<TunnelMapper, Tunnel> impleme
if (type == 1) {
return availablePorts.getLast();
}else {
if (port != null && availablePorts.contains(port)) {
return port;
}
return availablePorts.getFirst();
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
// 缓存相关常量
const CACHE_PREFIX = 'vite_config_';
const VERSION = "2.0.1-beta";
const VERSION = "2.0.2-beta";
const APP_VERSION = "1.0.3";
const getInitialConfig = () => {
+28
View File
@@ -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="远程地址"
+56 -17
View File
@@ -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"