mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
chore: release 2.1.9-rc8
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
**Generated:** Tue Mar 24 2026
|
**Generated:** Tue Mar 24 2026
|
||||||
**Commit:** 8ebde9d
|
**Commit:** 8ebde9d
|
||||||
**Branch:** main
|
**Branch:** main
|
||||||
**Tag:** 2.1.9-rc7
|
**Tag:** 2.1.9-rc8
|
||||||
|
|
||||||
## OVERVIEW
|
## OVERVIEW
|
||||||
FLVX (formerly Flux Panel) is a traffic forwarding management system built on a forked GOST v3 stack. It ships as a Go-based admin API (SQLite/PostgreSQL) + Vite/React UI + Go forwarding agent, with optional mobile WebView wrappers.
|
FLVX (formerly Flux Panel) is a traffic forwarding management system built on a forked GOST v3 stack. It ships as a Go-based admin API (SQLite/PostgreSQL) + Vite/React UI + Go forwarding agent, with optional mobile WebView wrappers.
|
||||||
|
|||||||
@@ -1102,50 +1102,111 @@ func (h *Handler) syncTunnelForwardsEntryPorts(tunnelID int64, entryNodeIDs []in
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
port := pickForwardPortFromRecords(oldPorts)
|
referencePort := pickForwardPortFromRecords(oldPorts)
|
||||||
if port <= 0 {
|
if referencePort <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the existing port is outside any entry node's allowed range,
|
// Build a map of existing node → port/inIP from old records.
|
||||||
// pick a new random port that satisfies all entry nodes.
|
oldPortByNode := make(map[int64]forwardPortRecord)
|
||||||
if !h.isPortValidForAllEntryNodes(port, entryNodeIDs) {
|
for _, fp := range oldPorts {
|
||||||
newPort := h.pickTunnelPort(tunnelID)
|
if fp.NodeID > 0 {
|
||||||
if newPort > 0 {
|
oldPortByNode[fp.NodeID] = fp
|
||||||
port = newPort
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var entries []forwardPortReplaceEntry
|
entries := make([]forwardPortReplaceEntry, 0, len(entryNodeIDs))
|
||||||
if allowInIP {
|
for _, nid := range entryNodeIDs {
|
||||||
entries = buildForwardPortEntriesWithPreservedInIP(entryNodeIDs, oldPorts, port)
|
if existing, ok := oldPortByNode[nid]; ok && existing.Port > 0 {
|
||||||
} else {
|
// Existing entry node: keep its current port.
|
||||||
entries = make([]forwardPortReplaceEntry, 0, len(entryNodeIDs))
|
inIP := existing.InIP
|
||||||
for _, nid := range entryNodeIDs {
|
if !allowInIP {
|
||||||
entries = append(entries, forwardPortReplaceEntry{NodeID: nid, Port: port, InIP: ""})
|
inIP = ""
|
||||||
|
}
|
||||||
|
entries = append(entries, forwardPortReplaceEntry{NodeID: nid, Port: existing.Port, InIP: inIP})
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New entry node: try to follow the reference port.
|
||||||
|
port := h.resolvePortForNewEntryNode(nid, referencePort, f.ID)
|
||||||
|
inIP := ""
|
||||||
|
if allowInIP {
|
||||||
|
// For single-entry tunnels, try to preserve inIP from old records.
|
||||||
|
for _, fp := range oldPorts {
|
||||||
|
if strings.TrimSpace(fp.InIP) != "" {
|
||||||
|
inIP = fp.InIP
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries = append(entries, forwardPortReplaceEntry{NodeID: nid, Port: port, InIP: inIP})
|
||||||
}
|
}
|
||||||
_ = h.repo.ReplaceForwardPorts(f.ID, entries)
|
_ = h.repo.ReplaceForwardPorts(f.ID, entries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) isPortValidForAllEntryNodes(port int, entryNodeIDs []int64) bool {
|
// resolvePortForNewEntryNode determines the port for a forward on a newly added
|
||||||
if port <= 0 {
|
// entry node. It tries to reuse referencePort (from existing entries); if that
|
||||||
return false
|
// port is out of range or already occupied, it picks a random available port
|
||||||
|
// for this specific node.
|
||||||
|
func (h *Handler) resolvePortForNewEntryNode(nodeID int64, referencePort int, forwardID int64) int {
|
||||||
|
node, err := h.getNodeRecord(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return referencePort
|
||||||
}
|
}
|
||||||
for _, nodeID := range entryNodeIDs {
|
|
||||||
node, err := h.getNodeRecord(nodeID)
|
// Check if referencePort is within the node's allowed range.
|
||||||
if err != nil {
|
if validateLocalNodePort(node, referencePort) == nil &&
|
||||||
continue
|
validateRemoteNodePort(node, referencePort) == nil {
|
||||||
}
|
// In range — check availability.
|
||||||
if validateLocalNodePort(node, port) != nil {
|
occupied, occErr := h.repo.HasOtherForwardOnNodePort(nodeID, referencePort, forwardID)
|
||||||
return false
|
if occErr == nil && !occupied {
|
||||||
}
|
return referencePort
|
||||||
if validateRemoteNodePort(node, port) != nil {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
// referencePort doesn't work for this node; pick a random one.
|
||||||
|
newPort := h.pickRandomPortForNode(nodeID)
|
||||||
|
if newPort > 0 {
|
||||||
|
return newPort
|
||||||
|
}
|
||||||
|
return referencePort // last resort fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// pickRandomPortForNode picks a random available port from a single node's
|
||||||
|
// port range, excluding ports already occupied by other forwards or chains.
|
||||||
|
func (h *Handler) pickRandomPortForNode(nodeID int64) int {
|
||||||
|
portRange, err := h.repo.GetNodePortRange(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if portRange == "" {
|
||||||
|
portRange = "1000-65535"
|
||||||
|
}
|
||||||
|
|
||||||
|
nodePorts, err := parsePorts(portRange)
|
||||||
|
if err != nil || len(nodePorts) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
used, err := h.getUsedPorts(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var available []int
|
||||||
|
for _, p := range nodePorts {
|
||||||
|
if !used[p] {
|
||||||
|
available = append(available, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(available) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(available))))
|
||||||
|
return available[idx.Int64()]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) tunnelDelete(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) tunnelDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -3348,7 +3348,7 @@ func (r *Repository) GetNodeMetrics(nodeID int64, startMs, endMs int64) ([]model
|
|||||||
err := r.db.Model(&model.NodeMetric{}).
|
err := r.db.Model(&model.NodeMetric{}).
|
||||||
Select(
|
Select(
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"? AS node_id, "+
|
"%d AS node_id, "+
|
||||||
"CAST(%s AS INTEGER) AS timestamp, "+
|
"CAST(%s AS INTEGER) AS timestamp, "+
|
||||||
"AVG(cpu_usage) AS cpu_usage, "+
|
"AVG(cpu_usage) AS cpu_usage, "+
|
||||||
"AVG(mem_usage) AS mem_usage, "+
|
"AVG(mem_usage) AS mem_usage, "+
|
||||||
@@ -3363,9 +3363,8 @@ func (r *Repository) GetNodeMetrics(nodeID int64, startMs, endMs int64) ([]model
|
|||||||
"CAST(AVG(tcp_conns) AS INTEGER) AS tcp_conns, "+
|
"CAST(AVG(tcp_conns) AS INTEGER) AS tcp_conns, "+
|
||||||
"CAST(AVG(udp_conns) AS INTEGER) AS udp_conns, "+
|
"CAST(AVG(udp_conns) AS INTEGER) AS udp_conns, "+
|
||||||
"CAST(MAX(uptime) AS INTEGER) AS uptime",
|
"CAST(MAX(uptime) AS INTEGER) AS uptime",
|
||||||
bucketExpr,
|
nodeID, bucketExpr,
|
||||||
),
|
),
|
||||||
nodeID,
|
|
||||||
).
|
).
|
||||||
Where("node_id = ? AND timestamp >= ? AND timestamp <= ?", nodeID, startMs, endMs).
|
Where("node_id = ? AND timestamp >= ? AND timestamp <= ?", nodeID, startMs, endMs).
|
||||||
Group(groupExpr).
|
Group(groupExpr).
|
||||||
|
|||||||
@@ -2,18 +2,20 @@
|
|||||||
|
|
||||||
## Issue
|
## Issue
|
||||||
- GitHub Issue: [#373](https://github.com/Sagit-chu/flvx/issues/373)
|
- GitHub Issue: [#373](https://github.com/Sagit-chu/flvx/issues/373)
|
||||||
- 添加隧道入口时,端口校验不严格 - 默认端口可能不在新入口设置范围内
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
当已有隧道上添加新入口节点时,系统会使用既有转发的端口部署到新入口节点上,
|
|
||||||
但该端口可能不在新入口节点设置的端口范围内,且没有校验提示。
|
|
||||||
|
|
||||||
### 根因
|
|
||||||
`syncTunnelForwardsEntryPorts` 函数在同步入口端口时,直接复用了原有端口,
|
|
||||||
没有检查该端口是否在新入口节点的允许范围内。
|
|
||||||
|
|
||||||
## 修复方案
|
## 修复方案
|
||||||
|
|
||||||
- [x] 1. 新增 `isPortValidForAllEntryNodes` 辅助方法,校验端口是否在各节点范围内
|
在 `syncTunnelForwardsEntryPorts` 中实现逐节点端口分配:
|
||||||
- [x] 2. 在 `syncTunnelForwardsEntryPorts` 中检测端口超范围时,通过 `pickTunnelPort` 自动随机分配新端口
|
|
||||||
- [x] 3. 构建通过 + 全量测试通过
|
- **旧入口节点**:保留原端口不变
|
||||||
|
- **新入口节点**:通过 `resolvePortForNewEntryNode` 决策:
|
||||||
|
- 参考端口在范围内且未被占用 → 跟随设置一样的端口
|
||||||
|
- 参考端口超出范围或被占用 → 通过 `pickRandomPortForNode` 为该节点单独随机分配
|
||||||
|
|
||||||
|
## 任务清单
|
||||||
|
|
||||||
|
- [x] 1. 实现 `pickRandomPortForNode` 辅助方法(单节点端口随机分配)
|
||||||
|
- [x] 2. 实现 `resolvePortForNewEntryNode` 方法(端口决策逻辑)
|
||||||
|
- [x] 3. 重写 `syncTunnelForwardsEntryPorts` 为逐节点分配
|
||||||
|
- [x] 4. 移除不再需要的 `isPortValidForAllEntryNodes`
|
||||||
|
- [x] 5. 构建通过 + 全量测试通过
|
||||||
|
|||||||
Reference in New Issue
Block a user