chore: release 2.1.9-rc8

This commit is contained in:
sagitchu
2026-03-24 09:45:21 +08:00
parent 4954526cbc
commit efaffb0475
4 changed files with 107 additions and 45 deletions
+1 -1
View File
@@ -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.
+90 -29
View File
@@ -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) {
+2 -3
View File
@@ -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).
+14 -12
View File
@@ -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. 构建通过 + 全量测试通过