添加屏蔽协议到面板,免去手动操作

This commit is contained in:
qaq
2025-10-31 15:58:49 +08:00
parent b587193e74
commit d6feb17b86
14 changed files with 336 additions and 27 deletions
-2
View File
@@ -5,7 +5,6 @@ go 1.23.0
toolchain go1.23.4
require (
github.com/apernet/OpenGFW v0.0.0-00010101000000-000000000000
github.com/go-gost/core v0.3.1
github.com/go-gost/x v0.5.3
github.com/judwhite/go-svc v1.2.1
@@ -149,4 +148,3 @@ require (
replace github.com/go-gost/x => ./x
replace github.com/apernet/OpenGFW => ./gfw
+1 -2
View File
@@ -4,7 +4,6 @@ import (
"context"
"flag"
"fmt"
//"github.com/apernet/OpenGFW/cmd"
"log"
_ "net/http/pprof"
"os"
@@ -120,7 +119,7 @@ func main() {
log := xlogger.NewLogger()
logger.SetDefault(log)
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, "1.2.2")
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, "1.2.3")
defer wsReporter.Stop()
service.SetHTTPReportURL(config.Addr, config.Secret)
+8
View File
@@ -52,6 +52,14 @@ var isSocks = 0
var needWrap = false
// SetProtocolBlock sets protocol blocking switches and recomputes wrapper need
func SetProtocolBlock(httpOn int, tlsOn int, socksOn int) {
isHttp = httpOn
isTls = tlsOn
isSocks = socksOn
needWrap = isTls+isSocks+isHttp > 0
}
type Option func(opts *options)
func init() {
+91 -4
View File
@@ -8,17 +8,20 @@ import (
"fmt"
"net"
"net/url"
"strconv"
"strings"
"sync" // 新增:用于管理连接状态的互斥锁
"time"
"github.com/go-gost/x/config"
"github.com/go-gost/x/service"
"github.com/go-gost/x/internal/util/crypto"
"github.com/gorilla/websocket"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
psnet "github.com/shirou/gopsutil/v3/net"
"os"
)
// SystemInfo 系统信息结构体
@@ -524,6 +527,11 @@ func (w *WebSocketReporter) routeCommand(cmd CommandMessage) {
response.Type = "TcpPingResponse"
response.Data = tcpPingResult
// Protocol blocking switches
case "SetProtocol":
err = w.handleSetProtocol(cmd.Data)
response.Type = "SetProtocolResponse"
default:
err = fmt.Errorf("未知命令类型: %s", cmd.Type)
response.Type = "UnknownCommandResponse"
@@ -765,6 +773,85 @@ func (w *WebSocketReporter) handleDeleteLimiter(data interface{}) error {
return deleteLimiter(deleteReq)
}
// handleSetProtocol 处理设置屏蔽协议的命令
func (w *WebSocketReporter) handleSetProtocol(data interface{}) error {
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("序列化协议设置失败: %v", err)
}
// 支持 {"http":0/1, "tls":0/1, "socks":0/1}
var req struct {
HTTP *int `json:"http"`
TLS *int `json:"tls"`
SOCKS *int `json:"socks"`
}
if err := json.Unmarshal(jsonData, &req); err != nil {
return fmt.Errorf("解析协议设置失败: %v", err)
}
// 读取当前值作为默认
httpVal, tlsVal, socksVal := 0, 0, 0
if req.HTTP != nil {
if *req.HTTP != 0 && *req.HTTP != 1 {
return fmt.Errorf("http 取值必须为0或1")
}
httpVal = *req.HTTP
}
if req.TLS != nil {
if *req.TLS != 0 && *req.TLS != 1 {
return fmt.Errorf("tls 取值必须为0或1")
}
tlsVal = *req.TLS
}
if req.SOCKS != nil {
if *req.SOCKS != 0 && *req.SOCKS != 1 {
return fmt.Errorf("socks 取值必须为0或1")
}
socksVal = *req.SOCKS
}
// 设置至 service,全量传递(未提供的值沿用0)
service.SetProtocolBlock(httpVal, tlsVal, socksVal)
// 同步写入本地 config.json
if err := updateLocalConfigJSON(httpVal, tlsVal, socksVal); err != nil {
return fmt.Errorf("写入config.json失败: %v", err)
}
return nil
}
// updateLocalConfigJSON 将 http/tls/socks 写入工作目录下的 config.json
func updateLocalConfigJSON(httpVal int, tlsVal int, socksVal int) error {
path := "config.json"
// 读取现有配置
type LocalConfig struct {
Addr string `json:"addr"`
Secret string `json:"secret"`
Http int `json:"http"`
Tls int `json:"tls"`
Socks int `json:"socks"`
}
var cfg LocalConfig
if b, err := os.ReadFile(path); err == nil {
_ = json.Unmarshal(b, &cfg)
}
cfg.Http = httpVal
cfg.Tls = tlsVal
cfg.Socks = socksVal
// 写回
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0644)
}
// handleCall 处理服务端的call回调消息
func (w *WebSocketReporter) handleCall(data interface{}) error {
// 解析call数据
@@ -931,15 +1018,15 @@ func getMemoryInfo() MemoryInfo {
return memInfo
}
// StartWebSocketReporterWithConfig 使用配置启动WebSocket报告器
func StartWebSocketReporterWithConfig(Addr string, Secret string, Version string) *WebSocketReporter {
// StartWebSocketReporterWithConfig 使用配置字段启动WebSocket报告器
func StartWebSocketReporterWithConfig(addr string, secret string, http int, tls int, socks int, version string) *WebSocketReporter {
// 构建包含本机IP的WebSocket URL
var fullURL = "ws://" + Addr + "/system-info?type=1&secret=" + Secret + "&version=" + Version
fullURL := "ws://" + addr + "/system-info?type=1&secret=" + secret + "&version=" + version + "&http=" + strconv.Itoa(http) + "&tls=" + strconv.Itoa(tls) + "&socks=" + strconv.Itoa(socks)
fmt.Printf("🔗 WebSocket连接URL: %s\n", fullURL)
reporter := NewWebSocketReporter(fullURL, Secret) // Pass Secret here
reporter := NewWebSocketReporter(fullURL, secret)
reporter.Start()
return reporter
}
+3
View File
@@ -61,6 +61,9 @@ CREATE TABLE `node` (
`port_sta` int(10) NOT NULL,
`port_end` int(10) NOT NULL,
`version` varchar(100) DEFAULT NULL,
`http` int(10) NOT NULL DEFAULT '0',
`tls` int(10) NOT NULL DEFAULT '0',
`socks` int(10) NOT NULL DEFAULT '0',
`created_time` bigint(20) NOT NULL,
`updated_time` bigint(20) DEFAULT NULL,
`status` int(10) NOT NULL
+58
View File
@@ -537,6 +537,64 @@ UPDATE \`node\`
SET \`port_sta\` = 1000, \`port_end\` = 65535
WHERE \`port_sta\` IS NULL OR \`port_end\` IS NULL;
-- node 表:添加 http、tls、socks 字段(如果不存在)
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'http'
),
'ALTER TABLE \`node\` ADD COLUMN \`http\` INT(10) DEFAULT 0 COMMENT "HTTP 服务端口";',
'SELECT "Column \`http\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'tls'
),
'ALTER TABLE \`node\` ADD COLUMN \`tls\` INT(10) DEFAULT 0 COMMENT "TLS 服务端口";',
'SELECT "Column \`tls\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @sql = (
SELECT IF(
NOT EXISTS (
SELECT 1
FROM information_schema.COLUMNS
WHERE table_schema = DATABASE()
AND table_name = 'node'
AND column_name = 'socks'
),
'ALTER TABLE \`node\` ADD COLUMN \`socks\` INT(10) DEFAULT 0 COMMENT "SOCKS 服务端口";',
'SELECT "Column \`socks\` already exists in \`node\`";'
)
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 为现有节点设置 http、tls、socks 默认值
UPDATE \`node\`
SET \`http\` = IFNULL(\`http\`, 0),
\`tls\` = IFNULL(\`tls\`, 0),
\`socks\` = IFNULL(\`socks\`, 0);
-- tunnel 表:删除废弃字段(如果存在)
SET @sql = (
SELECT IF(
@@ -31,4 +31,8 @@ public class NodeUpdateDto {
@Min(value = 1, message = "结束端口必须大于0")
@Max(value = 65535, message = "结束端口不能超过65535")
private Integer portEnd;
private Integer http;
private Integer tls;
private Integer socks;
}
@@ -224,6 +224,9 @@ public class WebSocketServer extends TextWebSocketHandler {
// 客户端节点连接
Long nodeId = Long.valueOf(id);
String version = (String) session.getAttributes().get("nodeVersion");
String http = (String) session.getAttributes().get("http");
String tls = (String) session.getAttributes().get("tls");
String socks = (String) session.getAttributes().get("socks");
log.info("节点 {} 尝试连接,开始处理连接逻辑", nodeId);
@@ -256,6 +259,16 @@ public class WebSocketServer extends TextWebSocketHandler {
if (version != null) {
node.setVersion(version);
}
if (http != null) {
node.setHttp(Integer.parseInt(http));
}
if (tls != null) {
node.setTls(Integer.parseInt(tls));
}
if (socks != null) {
node.setSocks(Integer.parseInt(socks));
}
boolean updateResult = nodeService.updateById(node);
if (updateResult) {
@@ -38,6 +38,9 @@ public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {
String secret = serverHttpRequest.getServletRequest().getParameter("secret");
String type = serverHttpRequest.getServletRequest().getParameter("type");
String version = serverHttpRequest.getServletRequest().getParameter("version");
String http = serverHttpRequest.getServletRequest().getParameter("http");
String tls = serverHttpRequest.getServletRequest().getParameter("tls");
String socks = serverHttpRequest.getServletRequest().getParameter("socks");
if (Objects.equals(type, "1")) {
System.out.println("type: " + type + " - version: " + version + " - secret: " + secret + " - IP: " + getClientIp(request));
Node node = nodeService.getOne(new QueryWrapper<Node>().eq("secret", secret));
@@ -48,6 +51,9 @@ public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {
attributes.put("id", node.getId());
attributes.put("nodeSecret", secret);
attributes.put("nodeVersion", version);
attributes.put("http",http);
attributes.put("tls",tls);
attributes.put("socks",socks);
log.info("节点 {} 通过验证,版本: {}", node.getId(), version);
// 不在这里更新状态,等到连接建立后再统一更新
}else {
@@ -32,4 +32,10 @@ public class Node extends BaseEntity {
private Integer portEnd;
private Integer http;
private Integer tls;
private Integer socks;
}
@@ -2,9 +2,11 @@ package com.admin.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
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.WebSocketServer;
import com.admin.entity.Node;
import com.admin.entity.Tunnel;
import com.admin.entity.ViteConfig;
@@ -13,6 +15,7 @@ import com.admin.mapper.TunnelMapper;
import com.admin.service.NodeService;
import com.admin.service.TunnelService;
import com.admin.service.ViteConfigService;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -22,6 +25,8 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Value;
/**
@@ -114,10 +119,34 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
@Override
public R updateNode(NodeUpdateDto nodeUpdateDto) {
// 1. 验证节点是否存在
if (!isNodeExists(nodeUpdateDto.getId())) {
Node node = this.getById(nodeUpdateDto.getId());
if (node == null) {
return R.err(ERROR_NODE_NOT_FOUND);
}
//1.1 如果节点在线 且传入更新的 http/tls/socks 任意一项与数据库不一致,则通过 WS 通知节点更新设置
boolean online = node.getStatus() != null && node.getStatus() == 1;
Integer newHttp = nodeUpdateDto.getHttp();
Integer newTls = nodeUpdateDto.getTls();
Integer newSocks = nodeUpdateDto.getSocks();
boolean httpChanged = newHttp != null && !newHttp.equals(node.getHttp());
boolean tlsChanged = newTls != null && !newTls.equals(node.getTls());
boolean socksChanged = newSocks != null && !newSocks.equals(node.getSocks());
if (online && (httpChanged || tlsChanged || socksChanged)) {
JSONObject req = new JSONObject();
req.put("http", newHttp);
req.put("tls", newTls);
req.put("socks", newSocks);
GostDto gostResult = WebSocketServer.send_msg(node.getId(), req, "SetProtocol");
if (!Objects.equals(gostResult.getMsg(), "OK")){
return R.err(gostResult.getMsg());
}
}
// 2. 构建更新对象并执行更新
Node updateNode = buildUpdateNode(nodeUpdateDto);
boolean result = this.updateById(updateNode);
@@ -153,7 +182,8 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
@Override
public R deleteNode(Long id) {
// 1. 验证节点是否存在
if (!isNodeExists(id)) {
Node node = this.getById(id);
if (node == null) {
return R.err(ERROR_NODE_NOT_FOUND);
}
@@ -225,7 +255,9 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
node.setServerIp(nodeUpdateDto.getServerIp());
node.setPortSta(nodeUpdateDto.getPortSta());
node.setPortEnd(nodeUpdateDto.getPortEnd());
node.setHttp(nodeUpdateDto.getHttp());
node.setTls(nodeUpdateDto.getTls());
node.setSocks(nodeUpdateDto.getSocks());
// 验证端口范围
validatePortRange(node.getPortSta(), node.getPortEnd());
@@ -242,15 +274,6 @@ public class NodeServiceImpl extends ServiceImpl<NodeMapper, Node> implements No
nodeList.forEach(node -> node.setSecret(null));
}
/**
* 检查节点是否存在
*
* @param nodeId 节点ID
* @return 节点是否存在
*/
private boolean isNodeExists(Long nodeId) {
return this.getById(nodeId) != null;
}
/**
* 检查节点使用情况
+1 -1
View File
@@ -1 +1 @@
VITE_API_BASE=http://192.168.100.11:6365
VITE_API_BASE=http://127.0.0.1:6365
+1 -1
View File
@@ -4,7 +4,7 @@ export type SiteConfig = typeof siteConfig;
// 缓存相关常量
const CACHE_PREFIX = 'vite_config_';
const VERSION = "1.4.0";
const VERSION = "1.4.1";
const APP_VERSION = "1.0.3";
const getInitialConfig = () => {
+109 -5
View File
@@ -5,6 +5,7 @@ import { Input } from "@heroui/input";
import { Textarea } from "@heroui/input";
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal";
import { Chip } from "@heroui/chip";
import { Switch } from "@heroui/switch";
import { Spinner } from "@heroui/spinner";
import { Alert } from "@heroui/alert";
import { Progress } from "@heroui/progress";
@@ -28,6 +29,9 @@ interface Node {
portSta: number;
portEnd: number;
version?: string;
http?: number; // 0 关 1 开
tls?: number; // 0 关 1 开
socks?: number; // 0 关 1 开
status: number; // 1: 在线, 0: 离线
connectionStatus: 'online' | 'offline';
systemInfo?: {
@@ -49,6 +53,9 @@ interface NodeForm {
serverIp: string;
portSta: number;
portEnd: number;
http: number; // 0 关 1 开
tls: number; // 0 关 1 开
socks: number; // 0 关 1 开
}
export default function NodePage() {
@@ -61,13 +68,18 @@ export default function NodePage() {
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false);
const [nodeToDelete, setNodeToDelete] = useState<Node | null>(null);
const [protocolDisabled, setProtocolDisabled] = useState(false);
const [protocolDisabledReason, setProtocolDisabledReason] = useState('');
const [form, setForm] = useState<NodeForm>({
id: null,
name: '',
ipString: '',
serverIp: '',
portSta: 1000,
portEnd: 65535
portEnd: 65535,
http: 0,
tls: 0,
socks: 0
});
const [errors, setErrors] = useState<Record<string, string>>({});
@@ -404,6 +416,8 @@ export default function NodePage() {
setIsEdit(false);
setDialogVisible(true);
resetForm();
setProtocolDisabled(true);
setProtocolDisabledReason('节点未在线,等待节点上线后再设置');
};
// 编辑节点
@@ -416,8 +430,14 @@ export default function NodePage() {
ipString: node.ip ? node.ip.split(',').map(ip => ip.trim()).join('\n') : '',
serverIp: node.serverIp || '',
portSta: node.portSta,
portEnd: node.portEnd
portEnd: node.portEnd,
http: typeof node.http === 'number' ? node.http : 1,
tls: typeof node.tls === 'number' ? node.tls : 1,
socks: typeof node.socks === 'number' ? node.socks : 1
});
const offline = node.connectionStatus !== 'online';
setProtocolDisabled(offline);
setProtocolDisabledReason(offline ? '节点未在线,等待节点上线后再设置' : '');
setDialogVisible(true);
};
@@ -514,7 +534,10 @@ export default function NodePage() {
ip: ipString,
serverIp: form.serverIp,
portSta: form.portSta,
portEnd: form.portEnd
portEnd: form.portEnd,
http: form.http,
tls: form.tls,
socks: form.socks
};
const res = await apiCall(data);
@@ -530,7 +553,10 @@ export default function NodePage() {
ip: ipString,
serverIp: form.serverIp,
portSta: form.portSta,
portEnd: form.portEnd
portEnd: form.portEnd,
http: form.http,
tls: form.tls,
socks: form.socks
} : n
));
} else {
@@ -554,7 +580,10 @@ export default function NodePage() {
ipString: '',
serverIp: '',
portSta: 1000,
portEnd: 65535
portEnd: 65535,
http: 0,
tls: 0,
socks: 0
});
setErrors({});
};
@@ -869,9 +898,84 @@ export default function NodePage() {
/>
</div>
{/* 屏蔽协议 */}
<div className="mt-1">
<div className="text-sm font-medium text-default-700">屏蔽协议</div>
<div className="text-xs text-default-500 mb-2">开启开关以屏蔽对应协议</div>
{protocolDisabled && (
<Alert
color="warning"
variant="flat"
description={protocolDisabledReason || '等待节点上线后再设置'}
className="mb-2"
/>
)}
<div className={`grid grid-cols-1 sm:grid-cols-3 gap-3 bg-default-50 dark:bg-default-100 p-3 rounded-md border border-default-200 dark:border-default-100/30 ${protocolDisabled ? 'opacity-70' : ''}`}>
{/* HTTP tile */}
<div className="px-3 py-3 rounded-lg bg-white dark:bg-default-50 border border-default-200 dark:border-default-100/30 hover:border-primary-200 transition-colors">
<div className="flex items-center gap-2 mb-2">
<svg className="w-4 h-4 text-default-500" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M2 10h20"/></svg>
<div className="text-sm font-medium text-default-700">HTTP</div>
</div>
<div className="flex items-center justify-between">
<div className="text-xs text-default-500">禁用/启用</div>
<Switch
size="sm"
isSelected={form.http === 1}
isDisabled={protocolDisabled}
onValueChange={(v) => setForm(prev => ({ ...prev, http: v ? 1 : 0 }))}
/>
</div>
<div 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>
</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
color="danger"
variant="flat"
description="请不要在出口节点执行屏蔽协议,否则可能影响转发;屏蔽协议仅需在入口节点执行。"
className="mt-3"
/>
<Alert
color="primary"
variant="flat"