mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-11-15 17:14:23 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f357b30500 | |||
| 8339f52280 |
@@ -103,10 +103,10 @@ jobs:
|
|||||||
- name: Download dependencies
|
- name: Download dependencies
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
- name: Run PostgreSQL contract test
|
- name: Run PostgreSQL contract tests
|
||||||
env:
|
env:
|
||||||
FLVX_POSTGRES_TEST_DSN: 'postgres://flux_test:flux_test_pass@127.0.0.1:5432/flux_test?sslmode=disable'
|
FLVX_POSTGRES_TEST_DSN: 'postgres://flux_test:flux_test_pass@127.0.0.1:5432/flux_test?sslmode=disable'
|
||||||
run: go test ./tests/contract -run TestPostgresNodeCreateRepairsMissingIDDefaultContract -count=1
|
run: go test ./tests/contract -run 'TestPostgres(NodeCreateRepairsMissingIDDefaultContract|NewRelayTunnelEmitsMatchingPrivateEndpointAuthentication)$' -count=1
|
||||||
|
|
||||||
agent:
|
agent:
|
||||||
name: Build Agent
|
name: Build Agent
|
||||||
|
|||||||
@@ -1123,6 +1123,14 @@ func normalizeAndValidateConfigValue(key, value string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return pngDataURLPrefix + payload, nil
|
return pngDataURLPrefix + payload, nil
|
||||||
|
case sequentialPortConfigKey:
|
||||||
|
normalized := strings.TrimSpace(strings.ToLower(value))
|
||||||
|
switch normalized {
|
||||||
|
case "true", "false":
|
||||||
|
return normalized, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("顺次分配端口开关配置值无效")
|
||||||
|
}
|
||||||
case monitorTunnelQualityEnabledConfigKey:
|
case monitorTunnelQualityEnabledConfigKey:
|
||||||
normalized := strings.TrimSpace(strings.ToLower(value))
|
normalized := strings.TrimSpace(strings.ToLower(value))
|
||||||
switch normalized {
|
switch normalized {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -1315,13 +1314,13 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var runtimePending error
|
||||||
newEntryNodeIDs, _ = h.tunnelEntryNodeIDs(id)
|
newEntryNodeIDs, _ = h.tunnelEntryNodeIDs(id)
|
||||||
if !sameInt64Set(oldEntryNodeIDs, newEntryNodeIDs) {
|
if !sameInt64Set(oldEntryNodeIDs, newEntryNodeIDs) {
|
||||||
h.cleanupTunnelForwardRuntimesOnRemovedEntryNodes(id, oldEntryNodeIDs, newEntryNodeIDs)
|
h.cleanupTunnelForwardRuntimesOnRemovedEntryNodes(id, oldEntryNodeIDs, newEntryNodeIDs)
|
||||||
h.syncTunnelForwardsEntryPorts(id, newEntryNodeIDs)
|
runtimePending = h.syncTunnelForwardsEntryPorts(id, newEntryNodeIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
var runtimePending error
|
|
||||||
if typeVal == 2 {
|
if typeVal == 2 {
|
||||||
applyRuntime := h.applyTunnelRuntime
|
applyRuntime := h.applyTunnelRuntime
|
||||||
if oldTunnel != nil && oldTunnel.Type == 2 {
|
if oldTunnel != nil && oldTunnel.Type == 2 {
|
||||||
@@ -1329,9 +1328,9 @@ func (h *Handler) tunnelUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
createdChains, createdServices, applyErr := applyRuntime(runtimeState)
|
createdChains, createdServices, applyErr := applyRuntime(runtimeState)
|
||||||
if isTunnelRuntimePending(applyErr) {
|
if isTunnelRuntimePending(applyErr) {
|
||||||
runtimePending = applyErr
|
runtimePending = errors.Join(runtimePending, applyErr)
|
||||||
}
|
}
|
||||||
if applyErr != nil && runtimePending == nil {
|
if applyErr != nil && !isTunnelRuntimePending(applyErr) {
|
||||||
updateProtocol := "tls"
|
updateProtocol := "tls"
|
||||||
if len(runtimeState.InNodes) > 0 && strings.TrimSpace(runtimeState.InNodes[0].Protocol) != "" {
|
if len(runtimeState.InNodes) > 0 && strings.TrimSpace(runtimeState.InNodes[0].Protocol) != "" {
|
||||||
updateProtocol = strings.TrimSpace(runtimeState.InNodes[0].Protocol)
|
updateProtocol = strings.TrimSpace(runtimeState.InNodes[0].Protocol)
|
||||||
@@ -1597,19 +1596,20 @@ func (h *Handler) validateTunnelEntryPortConflictsForNewEntriesTx(tx *gorm.DB, t
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) syncTunnelForwardsEntryPorts(tunnelID int64, entryNodeIDs []int64) {
|
func (h *Handler) syncTunnelForwardsEntryPorts(tunnelID int64, entryNodeIDs []int64) error {
|
||||||
if h == nil || h.repo == nil || tunnelID <= 0 {
|
if h == nil || h.repo == nil || tunnelID <= 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
entryNodeIDs = uniqueInt64s(entryNodeIDs)
|
entryNodeIDs = uniqueInt64s(entryNodeIDs)
|
||||||
if len(entryNodeIDs) == 0 {
|
if len(entryNodeIDs) == 0 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
forwards, err := h.listForwardsByTunnel(tunnelID)
|
forwards, err := h.listForwardsByTunnel(tunnelID)
|
||||||
if err != nil || len(forwards) == 0 {
|
if err != nil {
|
||||||
return
|
return err
|
||||||
}
|
}
|
||||||
|
var syncErrors []error
|
||||||
|
|
||||||
allowInIP := len(entryNodeIDs) == 1
|
allowInIP := len(entryNodeIDs) == 1
|
||||||
for i := range forwards {
|
for i := range forwards {
|
||||||
@@ -1619,6 +1619,7 @@ func (h *Handler) syncTunnelForwardsEntryPorts(tunnelID int64, entryNodeIDs []in
|
|||||||
}
|
}
|
||||||
oldPorts, err := h.listForwardPorts(f.ID)
|
oldPorts, err := h.listForwardPorts(f.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
syncErrors = append(syncErrors, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
referencePort := pickForwardPortFromRecords(oldPorts)
|
referencePort := pickForwardPortFromRecords(oldPorts)
|
||||||
@@ -1660,14 +1661,17 @@ func (h *Handler) syncTunnelForwardsEntryPorts(tunnelID int64, entryNodeIDs []in
|
|||||||
}
|
}
|
||||||
entries = append(entries, forwardPortReplaceEntry{NodeID: nid, Port: port, InIP: inIP})
|
entries = append(entries, forwardPortReplaceEntry{NodeID: nid, Port: port, InIP: inIP})
|
||||||
}
|
}
|
||||||
_ = h.repo.ReplaceForwardPorts(f.ID, entries)
|
if err := h.repo.ReplaceForwardPortsIndividually(f.ID, entries, h.isSequentialPortEnabled()); err != nil {
|
||||||
|
syncErrors = append(syncErrors, fmt.Errorf("转发 %s 入口端口更新失败: %w", f.Name, err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return errors.Join(syncErrors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolvePortForNewEntryNode determines the port for a forward on a newly added
|
// resolvePortForNewEntryNode determines the port for a forward on a newly added
|
||||||
// entry node. It tries to reuse referencePort (from existing entries); if that
|
// entry node. It tries to reuse referencePort (from existing entries); if that
|
||||||
// port is out of range or already occupied, it picks a random available port
|
// port is out of range or already occupied, zero requests atomic allocation
|
||||||
// for this specific node.
|
// when ReplaceForwardPorts persists the new entry. Explicit ports are rechecked there.
|
||||||
func (h *Handler) resolvePortForNewEntryNode(nodeID int64, referencePort int, forwardID int64) int {
|
func (h *Handler) resolvePortForNewEntryNode(nodeID int64, referencePort int, forwardID int64) int {
|
||||||
node, err := h.getNodeRecord(nodeID)
|
node, err := h.getNodeRecord(nodeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1684,48 +1688,23 @@ func (h *Handler) resolvePortForNewEntryNode(nodeID int64, referencePort int, fo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// referencePort doesn't work for this node; pick a random one.
|
// Defer fallback selection to the same transaction that saves the entries.
|
||||||
newPort := h.pickRandomPortForNode(nodeID)
|
return 0
|
||||||
if newPort > 0 {
|
|
||||||
return newPort
|
|
||||||
}
|
|
||||||
return referencePort // last resort fallback
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// pickRandomPortForNode picks a random available port from a single node's
|
// sequentialPortConfigKey 是「未指定端口时按端口范围顺次分配」开关的配置键。
|
||||||
// port range, excluding ports already occupied by other forwards or chains.
|
const sequentialPortConfigKey = "sequential_port_enabled"
|
||||||
func (h *Handler) pickRandomPortForNode(nodeID int64) int {
|
|
||||||
portRange, err := h.repo.GetNodePortRange(nodeID)
|
// isSequentialPortEnabled 返回是否开启了顺次分配端口(默认关闭,保持随机分配)。
|
||||||
|
func (h *Handler) isSequentialPortEnabled() bool {
|
||||||
|
if h == nil || h.repo == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
value, err := h.repo.GetViteConfigValue(sequentialPortConfigKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return false
|
||||||
}
|
}
|
||||||
if portRange == "" {
|
return strings.EqualFold(strings.TrimSpace(value), "true")
|
||||||
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) {
|
||||||
@@ -2290,12 +2269,7 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
port := asInt(req["inPort"], 0)
|
port := asInt(req["inPort"], 0)
|
||||||
if port <= 0 {
|
// A zero port is allocated and persisted atomically by the repository.
|
||||||
port = h.pickTunnelPort(tunnelID)
|
|
||||||
}
|
|
||||||
if port <= 0 {
|
|
||||||
port = 10000
|
|
||||||
}
|
|
||||||
entryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
entryNodes, _ := h.tunnelEntryNodeIDs(tunnelID)
|
||||||
isNftTunnel, _, err := h.tunnelUsesNftables(tunnelID)
|
isNftTunnel, _, err := h.tunnelUsesNftables(tunnelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -2314,6 +2288,9 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, nodeID := range entryNodes {
|
for _, nodeID := range entryNodes {
|
||||||
|
if port <= 0 {
|
||||||
|
break // Repository validates all nodes while allocating atomically.
|
||||||
|
}
|
||||||
node, nodeErr := h.getNodeRecord(nodeID)
|
node, nodeErr := h.getNodeRecord(nodeID)
|
||||||
if nodeErr != nil {
|
if nodeErr != nil {
|
||||||
continue
|
continue
|
||||||
@@ -2346,7 +2323,7 @@ func (h *Handler) forwardCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
proxyProtocolReceive := asInt(req["proxyProtocolReceive"], 0)
|
proxyProtocolReceive := asInt(req["proxyProtocolReceive"], 0)
|
||||||
proxyProtocolSend := asInt(req["proxyProtocolSend"], proxyProtocol)
|
proxyProtocolSend := asInt(req["proxyProtocolSend"], proxyProtocol)
|
||||||
|
|
||||||
forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn, ipMaxConn, nullableInt(ipSpeedID), proxyProtocol, proxyProtocolReceive, proxyProtocolSend)
|
forwardID, err := h.repo.CreateForwardTx(userID, userName, name, tunnelID, remoteAddr, defaultString(asString(req["strategy"]), "fifo"), now, inx, entryNodes, port, inIp, nullableInt(speedID), maxConn, ipMaxConn, nullableInt(ipSpeedID), proxyProtocol, proxyProtocolReceive, proxyProtocolSend, h.isSequentialPortEnabled())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||||
return
|
return
|
||||||
@@ -2484,9 +2461,6 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
if minPort.Valid {
|
if minPort.Valid {
|
||||||
port = int(minPort.Int64)
|
port = int(minPort.Int64)
|
||||||
}
|
}
|
||||||
if port <= 0 {
|
|
||||||
port = h.pickTunnelPort(tunnelID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
hasInIP := false
|
hasInIP := false
|
||||||
inIp := ""
|
inIp := ""
|
||||||
@@ -2511,6 +2485,9 @@ func (h *Handler) forwardUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
keptNodeIDs = diffInt64s(oldNodeIDs, removedNodeIDs)
|
keptNodeIDs = diffInt64s(oldNodeIDs, removedNodeIDs)
|
||||||
}
|
}
|
||||||
for _, nodeID := range fwdEntryNodes {
|
for _, nodeID := range fwdEntryNodes {
|
||||||
|
if port <= 0 {
|
||||||
|
break // Repository validates all nodes while allocating atomically.
|
||||||
|
}
|
||||||
node, nodeErr := h.getNodeRecord(nodeID)
|
node, nodeErr := h.getNodeRecord(nodeID)
|
||||||
if nodeErr != nil {
|
if nodeErr != nil {
|
||||||
continue
|
continue
|
||||||
@@ -3033,9 +3010,6 @@ func (h *Handler) forwardBatchChangeTunnel(w http.ResponseWriter, r *http.Reques
|
|||||||
if port.Valid {
|
if port.Valid {
|
||||||
p = int(port.Int64)
|
p = int(port.Int64)
|
||||||
}
|
}
|
||||||
if p <= 0 {
|
|
||||||
p = h.pickTunnelPort(req.TargetTunnelID)
|
|
||||||
}
|
|
||||||
bctEntryNodes, _ := h.tunnelEntryNodeIDs(req.TargetTunnelID)
|
bctEntryNodes, _ := h.tunnelEntryNodeIDs(req.TargetTunnelID)
|
||||||
newNodeIDs := uniqueInt64s(bctEntryNodes)
|
newNodeIDs := uniqueInt64s(bctEntryNodes)
|
||||||
removedNodeIDs := diffInt64s(oldNodeIDs, newNodeIDs)
|
removedNodeIDs := diffInt64s(oldNodeIDs, newNodeIDs)
|
||||||
@@ -3043,6 +3017,9 @@ func (h *Handler) forwardBatchChangeTunnel(w http.ResponseWriter, r *http.Reques
|
|||||||
portRangeOk := true
|
portRangeOk := true
|
||||||
var portRangeErr error
|
var portRangeErr error
|
||||||
for _, nid := range bctEntryNodes {
|
for _, nid := range bctEntryNodes {
|
||||||
|
if p <= 0 {
|
||||||
|
break // Repository validates all nodes while allocating atomically.
|
||||||
|
}
|
||||||
nd, ndErr := h.getNodeRecord(nid)
|
nd, ndErr := h.getNodeRecord(nid)
|
||||||
if ndErr != nil {
|
if ndErr != nil {
|
||||||
portRangeErr = ndErr
|
portRangeErr = ndErr
|
||||||
@@ -4972,75 +4949,6 @@ func (h *Handler) tunnelEntryNodeIDs(tunnelID int64) ([]int64, error) {
|
|||||||
return h.repo.TunnelEntryNodeIDs(tunnelID)
|
return h.repo.TunnelEntryNodeIDs(tunnelID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) pickTunnelPort(tunnelID int64) int {
|
|
||||||
entryNodes, err := h.tunnelEntryNodeIDs(tunnelID)
|
|
||||||
if err != nil || len(entryNodes) == 0 {
|
|
||||||
return 10000
|
|
||||||
}
|
|
||||||
|
|
||||||
var commonAvailable []int
|
|
||||||
firstNode := true
|
|
||||||
|
|
||||||
for _, nodeID := range entryNodes {
|
|
||||||
portRange, err := h.repo.GetNodePortRange(nodeID)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if portRange == "" {
|
|
||||||
portRange = "1000-65535"
|
|
||||||
}
|
|
||||||
|
|
||||||
nodePorts, err := parsePorts(portRange)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
used, err := h.getUsedPorts(nodeID)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var available []int
|
|
||||||
for _, p := range nodePorts {
|
|
||||||
if !used[p] {
|
|
||||||
available = append(available, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if firstNode {
|
|
||||||
commonAvailable = available
|
|
||||||
firstNode = false
|
|
||||||
} else {
|
|
||||||
set := make(map[int]bool)
|
|
||||||
for _, p := range available {
|
|
||||||
set[p] = true
|
|
||||||
}
|
|
||||||
var newCommon []int
|
|
||||||
for _, p := range commonAvailable {
|
|
||||||
if set[p] {
|
|
||||||
newCommon = append(newCommon, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commonAvailable = newCommon
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(commonAvailable) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(commonAvailable) > 0 {
|
|
||||||
idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(commonAvailable))))
|
|
||||||
return commonAvailable[idx.Int64()]
|
|
||||||
}
|
|
||||||
|
|
||||||
return 10000
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) getUsedPorts(nodeID int64) (map[int]bool, error) {
|
|
||||||
return h.repo.GetUsedPortsOnNodeAsMap(nodeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parsePorts(portRange string) ([]int, error) {
|
func parsePorts(portRange string) ([]int, error) {
|
||||||
var ports []int
|
var ports []int
|
||||||
parts := strings.Split(portRange, ",")
|
parts := strings.Split(portRange, ",")
|
||||||
@@ -5113,7 +5021,7 @@ func (h *Handler) replaceForwardPorts(forwardID, tunnelID int64, port int, inIp
|
|||||||
for i, nid := range entryNodes {
|
for i, nid := range entryNodes {
|
||||||
entries[i] = forwardPortReplaceEntry{NodeID: nid, Port: port, InIP: inIp}
|
entries[i] = forwardPortReplaceEntry{NodeID: nid, Port: port, InIP: inIp}
|
||||||
}
|
}
|
||||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
return h.repo.ReplaceForwardPorts(forwardID, entries, h.isSequentialPortEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) replaceForwardPortsPreservingInIP(forwardID, tunnelID int64, port int, oldPorts []forwardPortRecord) error {
|
func (h *Handler) replaceForwardPortsPreservingInIP(forwardID, tunnelID int64, port int, oldPorts []forwardPortRecord) error {
|
||||||
@@ -5122,7 +5030,7 @@ func (h *Handler) replaceForwardPortsPreservingInIP(forwardID, tunnelID int64, p
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
entries := buildForwardPortEntriesWithPreservedInIP(entryNodes, oldPorts, port)
|
entries := buildForwardPortEntriesWithPreservedInIP(entryNodes, oldPorts, port)
|
||||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
return h.repo.ReplaceForwardPorts(forwardID, entries, h.isSequentialPortEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) replaceForwardPortsWithRecords(forwardID int64, ports []forwardPortRecord) error {
|
func (h *Handler) replaceForwardPortsWithRecords(forwardID int64, ports []forwardPortRecord) error {
|
||||||
@@ -5130,7 +5038,7 @@ func (h *Handler) replaceForwardPortsWithRecords(forwardID int64, ports []forwar
|
|||||||
for i, fp := range ports {
|
for i, fp := range ports {
|
||||||
entries[i] = forwardPortReplaceEntry{NodeID: fp.NodeID, Port: fp.Port, InIP: fp.InIP}
|
entries[i] = forwardPortReplaceEntry{NodeID: fp.NodeID, Port: fp.Port, InIP: fp.InIP}
|
||||||
}
|
}
|
||||||
return h.repo.ReplaceForwardPorts(forwardID, entries)
|
return h.repo.ReplaceForwardPorts(forwardID, entries, h.isSequentialPortEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) rollbackForwardMutation(oldForward *forwardRecord, oldPorts []forwardPortRecord) {
|
func (h *Handler) rollbackForwardMutation(oldForward *forwardRecord, oldPorts []forwardPortRecord) {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNormalizeSequentialPortConfigValue(t *testing.T) {
|
||||||
|
for in, want := range map[string]string{"true": "true", " FALSE ": "false"} {
|
||||||
|
got, err := normalizeAndValidateConfigValue(sequentialPortConfigKey, in)
|
||||||
|
if err != nil || got != want {
|
||||||
|
t.Fatalf("value %q: got %q err=%v, want %q", in, got, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := normalizeAndValidateConfigValue(sequentialPortConfigKey, "maybe"); err == nil {
|
||||||
|
t.Fatal("expected invalid value to be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSequentialPortConfigReadsRepositoryAndDefaultsOff(t *testing.T) {
|
||||||
|
_, r := setupConfigAccessTestRouter(t)
|
||||||
|
h := &Handler{repo: r}
|
||||||
|
if h.isSequentialPortEnabled() {
|
||||||
|
t.Fatal("missing config must preserve random allocation")
|
||||||
|
}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
value string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"true", true}, {" TRUE ", true}, {"false", false}, {"invalid", false},
|
||||||
|
} {
|
||||||
|
seedConfigValue(t, r, sequentialPortConfigKey, tc.value)
|
||||||
|
if got := h.isSequentialPortEnabled(); got != tc.want {
|
||||||
|
t.Fatalf("config %q: got %v, want %v", tc.value, got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -500,10 +500,7 @@ func (h *Handler) planSingleTunnelDeleteForwardMigration(forward *forwardRecord,
|
|||||||
targetPort = int(minPort.Int64)
|
targetPort = int(minPort.Int64)
|
||||||
}
|
}
|
||||||
if targetPort <= 0 {
|
if targetPort <= 0 {
|
||||||
targetPort = h.pickTunnelPort(targetTunnelID)
|
return tunnelForwardMigrationPlan{}, errors.New("转发入口端口无效,请先修复该规则")
|
||||||
}
|
|
||||||
if targetPort <= 0 {
|
|
||||||
targetPort = 10000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasCustomInIP := false
|
hasCustomInIP := false
|
||||||
|
|||||||
@@ -1609,7 +1609,15 @@ func (r *Repository) CreatePeerShareRuntime(item *model.PeerShareRuntime) error
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return errors.New("runtime item is nil")
|
return errors.New("runtime item is nil")
|
||||||
}
|
}
|
||||||
return r.db.Create(item).Error
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := lockPortAllocationTx(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.checkPeerRuntimePortTx(tx, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Create(item).Error
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) UpdatePeerShareRuntime(item *model.PeerShareRuntime) error {
|
func (r *Repository) UpdatePeerShareRuntime(item *model.PeerShareRuntime) error {
|
||||||
@@ -1619,17 +1627,34 @@ func (r *Repository) UpdatePeerShareRuntime(item *model.PeerShareRuntime) error
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return errors.New("runtime item is nil")
|
return errors.New("runtime item is nil")
|
||||||
}
|
}
|
||||||
return r.db.Model(&model.PeerShareRuntime{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
"reservation_id": item.ReservationID,
|
if err := lockPortAllocationTx(tx); err != nil {
|
||||||
"relay_auth": item.RelayAuth,
|
return err
|
||||||
"relay_auth_version": item.RelayAuthVersion,
|
}
|
||||||
"binding_id": item.BindingID, "role": item.Role,
|
var current model.PeerShareRuntime
|
||||||
"chain_name": item.ChainName, "service_name": item.ServiceName,
|
if err := tx.Where("id = ?", item.ID).First(¤t).Error; err != nil {
|
||||||
"protocol": item.Protocol, "strategy": item.Strategy,
|
return err
|
||||||
"port": item.Port, "target": item.Target,
|
}
|
||||||
"applied": item.Applied, "status": item.Status,
|
if current.NodeID != item.NodeID {
|
||||||
"updated_time": item.UpdatedTime,
|
return errors.New("runtime node cannot change")
|
||||||
}).Error
|
}
|
||||||
|
if !(current.Status == 1 && current.Port == item.Port) {
|
||||||
|
if err := r.checkPeerRuntimePortTx(tx, item); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx.Model(&model.PeerShareRuntime{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||||
|
"reservation_id": item.ReservationID,
|
||||||
|
"relay_auth": item.RelayAuth,
|
||||||
|
"relay_auth_version": item.RelayAuthVersion,
|
||||||
|
"binding_id": item.BindingID, "role": item.Role,
|
||||||
|
"chain_name": item.ChainName, "service_name": item.ServiceName,
|
||||||
|
"protocol": item.Protocol, "strategy": item.Strategy,
|
||||||
|
"port": item.Port, "target": item.Target,
|
||||||
|
"applied": item.Applied, "status": item.Status,
|
||||||
|
"updated_time": item.UpdatedTime,
|
||||||
|
}).Error
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) MarkPeerShareRuntimeReleased(id int64, updatedTime int64) error {
|
func (r *Repository) MarkPeerShareRuntimeReleased(id int64, updatedTime int64) error {
|
||||||
@@ -2664,6 +2689,15 @@ func importTunnels(tx *gorm.DB, tunnels []model.TunnelBackup, now int64) (int, e
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
for _, ct := range t.ChainTunnels {
|
for _, ct := range t.ChainTunnels {
|
||||||
|
if ct.Port > 0 {
|
||||||
|
used, err := (&Repository{}).usedPortSetTx(tx, ct.NodeID, 0, ct.TunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
if used[ct.Port] {
|
||||||
|
return count, errors.New("导入链路端口已占用")
|
||||||
|
}
|
||||||
|
}
|
||||||
chainItem := model.ChainTunnel{
|
chainItem := model.ChainTunnel{
|
||||||
ID: ct.ID,
|
ID: ct.ID,
|
||||||
TunnelID: ct.TunnelID,
|
TunnelID: ct.TunnelID,
|
||||||
@@ -2735,6 +2769,13 @@ func importForwards(tx *gorm.DB, forwards []model.ForwardBackup, now int64) (int
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
for _, fp := range *f.ForwardPorts {
|
for _, fp := range *f.ForwardPorts {
|
||||||
|
used, err := (&Repository{}).usedPortSetTx(tx, fp.NodeID, f.ID, 0)
|
||||||
|
if err != nil {
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
if used[fp.Port] {
|
||||||
|
return count, errors.New("导入转发端口已占用")
|
||||||
|
}
|
||||||
if err := tx.Create(&model.ForwardPort{ForwardID: f.ID, NodeID: fp.NodeID, Port: fp.Port}).Error; err != nil {
|
if err := tx.Create(&model.ForwardPort{ForwardID: f.ID, NodeID: fp.NodeID, Port: fp.Port}).Error; err != nil {
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ var errBackupImportBusy = errors.New("database is busy; retry the backup import"
|
|||||||
func backupImportTransaction(db *gorm.DB, importRows func(*gorm.DB) error) error {
|
func backupImportTransaction(db *gorm.DB, importRows func(*gorm.DB) error) error {
|
||||||
var err error
|
var err error
|
||||||
for attempt := 0; attempt < 10; attempt++ {
|
for attempt := 0; attempt < 10; attempt++ {
|
||||||
err = db.Transaction(importRows)
|
err = db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := lockPortAllocationTx(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return importRows(tx)
|
||||||
|
})
|
||||||
if !errors.Is(err, errBackupImportBusy) {
|
if !errors.Is(err, errBackupImportBusy) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,19 +146,13 @@ func (r *Repository) CreateFederationTunnel(name string, tunnelType int, protoco
|
|||||||
InIP: sql.NullString{String: "", Valid: false},
|
InIP: sql.NullString{String: "", Valid: false},
|
||||||
}
|
}
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := lockPortAllocationTx(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := tx.Create(&tunnel).Error; err != nil {
|
if err := tx.Create(&tunnel).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ct := model.ChainTunnel{
|
if err := r.CreateChainTunnelTx(tx, tunnel.ID, "1", nodeID, sql.NullInt64{Int64: int64(remotePort), Valid: true}, "fifo", 0, protocol, ""); err != nil {
|
||||||
TunnelID: tunnel.ID,
|
|
||||||
ChainType: "1",
|
|
||||||
NodeID: nodeID,
|
|
||||||
Port: sql.NullInt64{Int64: int64(remotePort), Valid: true},
|
|
||||||
Strategy: sql.NullString{String: "fifo", Valid: true},
|
|
||||||
Inx: sql.NullInt64{Int64: 0, Valid: true},
|
|
||||||
Protocol: sql.NullString{String: protocol, Valid: true},
|
|
||||||
}
|
|
||||||
if err := tx.Create(&ct).Error; err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -160,6 +160,9 @@ func TestForwardRepositoryPersistsPerIPLimits(t *testing.T) {
|
|||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
|
if err := r.db.Create(&model.Node{ID: 3, Port: "1000-65535"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
forwardID, err := r.CreateForwardTx(1, "admin", "per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 0, 5, int64(21), 0, 0, 0)
|
forwardID, err := r.CreateForwardTx(1, "admin", "per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 0, 5, int64(21), 0, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CreateForwardTx: %v", err)
|
t.Fatalf("CreateForwardTx: %v", err)
|
||||||
@@ -224,6 +227,9 @@ func TestForwardRepositoryPersistsProxyProtocolReceiveAndSend(t *testing.T) {
|
|||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
|
if err := r.db.Create(&model.Node{ID: 3, Port: "1000-65535"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
forwardID, err := r.CreateForwardTx(1, "admin", "proxy-protocol-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 0, 0, nil, 0, 1, 2)
|
forwardID, err := r.CreateForwardTx(1, "admin", "proxy-protocol-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 0, 0, nil, 0, 1, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CreateForwardTx: %v", err)
|
t.Fatalf("CreateForwardTx: %v", err)
|
||||||
@@ -301,7 +307,10 @@ func TestRollbackForwardFieldsRestoresPerIPLimits(t *testing.T) {
|
|||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
forwardID, err := r.CreateForwardTx(1, "admin", "rollback-per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, nil, 0, "", nil, 7, 5, int64(21), 2, 0, 2)
|
if err := r.db.Create(&model.Node{ID: 3, Port: "1000-65535"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
forwardID, err := r.CreateForwardTx(1, "admin", "rollback-per-ip-forward", 2, "1.1.1.1:443", "fifo", now, 1, []int64{3}, 24000, "", nil, 7, 5, int64(21), 2, 0, 2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CreateForwardTx: %v", err)
|
t.Fatalf("CreateForwardTx: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -448,7 +448,14 @@ func (r *Repository) BeginTx() *gorm.DB {
|
|||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return r.db.Begin()
|
tx := r.db.Begin()
|
||||||
|
if tx.Error == nil {
|
||||||
|
if err := lockPortAllocationTx(tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
tx.AddError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) UpdateTunnelOrder(tunnelID int64, inx int, now int64) {
|
func (r *Repository) UpdateTunnelOrder(tunnelID int64, inx int, now int64) {
|
||||||
@@ -492,6 +499,18 @@ func (r *Repository) CreateChainTunnelTx(tx *gorm.DB, tunnelID int64, chainType
|
|||||||
if tx == nil {
|
if tx == nil {
|
||||||
return errors.New("database unavailable")
|
return errors.New("database unavailable")
|
||||||
}
|
}
|
||||||
|
if port.Valid && port.Int64 > 0 {
|
||||||
|
if err := r.LockPortNodesTx(tx, []int64{nodeID}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
used, err := r.usedPortSetTx(tx, nodeID, 0, tunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if used[int(port.Int64)] {
|
||||||
|
return errors.New("端口已占用")
|
||||||
|
}
|
||||||
|
}
|
||||||
ct := model.ChainTunnel{
|
ct := model.ChainTunnel{
|
||||||
TunnelID: tunnelID,
|
TunnelID: tunnelID,
|
||||||
ChainType: chainType,
|
ChainType: chainType,
|
||||||
@@ -538,6 +557,9 @@ func (r *Repository) pickNodePortTx(tx *gorm.DB, nodeID int64, allocated map[int
|
|||||||
if nodeID <= 0 {
|
if nodeID <= 0 {
|
||||||
return 0, errors.New("节点不存在")
|
return 0, errors.New("节点不存在")
|
||||||
}
|
}
|
||||||
|
if err := r.LockPortNodesTx(tx, []int64{nodeID}); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
if port, ok := allocated[nodeID]; ok && port > 0 {
|
if port, ok := allocated[nodeID]; ok && port > 0 {
|
||||||
return port, nil
|
return port, nil
|
||||||
}
|
}
|
||||||
@@ -556,47 +578,16 @@ func (r *Repository) pickNodePortTx(tx *gorm.DB, nodeID int64, allocated map[int
|
|||||||
return 0, errors.New("节点端口已满,无可用端口")
|
return 0, errors.New("节点端口已满,无可用端口")
|
||||||
}
|
}
|
||||||
|
|
||||||
used := make(map[int]struct{})
|
used, err := r.usedPortSetTx(tx, nodeID, 0, excludeTunnelID)
|
||||||
|
|
||||||
chainQuery := tx.Model(&model.ChainTunnel{}).Where("node_id = ? AND port > 0", nodeID)
|
|
||||||
if excludeTunnelID > 0 {
|
|
||||||
chainQuery = chainQuery.Where("tunnel_id != ?", excludeTunnelID)
|
|
||||||
}
|
|
||||||
var chainPorts []int
|
|
||||||
if err := chainQuery.Pluck("port", &chainPorts).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
for _, p := range chainPorts {
|
|
||||||
if p > 0 {
|
|
||||||
used[p] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var forwardPorts []int
|
|
||||||
if err := tx.Model(&model.ForwardPort{}).
|
|
||||||
Where("node_id = ? AND port > 0", nodeID).
|
|
||||||
Pluck("port", &forwardPorts).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
for _, p := range forwardPorts {
|
|
||||||
if p > 0 {
|
|
||||||
used[p] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingPorts, err := r.PendingTunnelCleanupPortsTx(tx, nodeID, excludeTunnelID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
for _, port := range pendingPorts {
|
|
||||||
used[port] = struct{}{}
|
|
||||||
}
|
|
||||||
var available []int
|
var available []int
|
||||||
for _, candidate := range candidates {
|
for _, candidate := range candidates {
|
||||||
if candidate <= 0 {
|
if candidate <= 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := used[candidate]; ok {
|
if used[candidate] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
available = append(available, candidate)
|
available = append(available, candidate)
|
||||||
@@ -836,11 +827,64 @@ func (r *Repository) ReplaceForwardPorts(forwardID int64, entries []struct {
|
|||||||
NodeID int64
|
NodeID int64
|
||||||
Port int
|
Port int
|
||||||
InIP string
|
InIP string
|
||||||
}) error {
|
}, sequential ...bool) error {
|
||||||
|
return r.replaceForwardPorts(forwardID, entries, len(sequential) > 0 && sequential[0], false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceForwardPortsIndividually preserves independent ports when adding entry nodes.
|
||||||
|
func (r *Repository) ReplaceForwardPortsIndividually(forwardID int64, entries []struct {
|
||||||
|
NodeID int64
|
||||||
|
Port int
|
||||||
|
InIP string
|
||||||
|
}, sequential bool) error {
|
||||||
|
return r.replaceForwardPorts(forwardID, entries, sequential, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) replaceForwardPorts(forwardID int64, entries []struct {
|
||||||
|
NodeID int64
|
||||||
|
Port int
|
||||||
|
InIP string
|
||||||
|
}, sequential, individually bool) error {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return errors.New("repository not initialized")
|
return errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
ids := make([]int64, 0, len(entries))
|
||||||
|
autoIDs := []int64{}
|
||||||
|
for _, e := range entries {
|
||||||
|
ids = append(ids, e.NodeID)
|
||||||
|
if e.Port <= 0 {
|
||||||
|
autoIDs = append(autoIDs, e.NodeID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := r.LockPortNodesTx(tx, ids); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
autoPort := 0
|
||||||
|
if len(autoIDs) > 0 && !individually {
|
||||||
|
var err error
|
||||||
|
autoPort, err = r.selectForwardPortTx(tx, autoIDs, 0, forwardID, sequential)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries = append(entries[:0:0], entries...)
|
||||||
|
for i := range entries {
|
||||||
|
if entries[i].Port <= 0 {
|
||||||
|
if individually {
|
||||||
|
var err error
|
||||||
|
entries[i].Port, err = r.selectForwardPortTx(tx, []int64{entries[i].NodeID}, 0, forwardID, sequential)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entries[i].Port = autoPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := r.selectForwardPortTx(tx, []int64{entries[i].NodeID}, entries[i].Port, forwardID, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if err := tx.Where("forward_id = ?", forwardID).Delete(&model.ForwardPort{}).Error; err != nil {
|
if err := tx.Where("forward_id = ?", forwardID).Delete(&model.ForwardPort{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1362,13 +1406,21 @@ func (r *Repository) EnsureUserTunnelGrant(userID, tunnelID int64) (int64, bool,
|
|||||||
return ut.ID, true, nil
|
return ut.ID, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CreateForwardTx(userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, now int64, inx int, entryNodeIDs []int64, port int, inIp string, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int, proxyProtocolReceive int, proxyProtocolSend int) (int64, error) {
|
func (r *Repository) CreateForwardTx(userID int64, userName, name string, tunnelID int64, remoteAddr, strategy string, now int64, inx int, entryNodeIDs []int64, port int, inIp string, speedID interface{}, maxConn int, ipMaxConn int, ipSpeedID interface{}, proxyProtocol int, proxyProtocolReceive int, proxyProtocolSend int, sequential ...bool) (int64, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return 0, errors.New("repository not initialized")
|
return 0, errors.New("repository not initialized")
|
||||||
}
|
}
|
||||||
_, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend)
|
_, proxyProtocolSend = normalizeForwardProxyProtocol(proxyProtocol, proxyProtocolReceive, proxyProtocolSend)
|
||||||
var forwardID int64
|
var forwardID int64
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := r.LockPortNodesTx(tx, entryNodeIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
selectedPort, err := r.selectForwardPortTx(tx, entryNodeIDs, port, 0, len(sequential) > 0 && sequential[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
port = selectedPort
|
||||||
fwd := model.Forward{
|
fwd := model.Forward{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
UserName: userName,
|
UserName: userName,
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go-backend/internal/store/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// lockPortAllocationTx serializes reservation mutations across processes sharing a database.
|
||||||
|
// PostgreSQL uses a transaction-scoped advisory lock; SQLite acquires its writer
|
||||||
|
// lock without changing any row, including when the node table is empty.
|
||||||
|
// All panel instances sharing this database must use this protocol; mixed old
|
||||||
|
// binaries and out-of-band SQL writes are not protected by advisory locks.
|
||||||
|
func lockPortAllocationTx(tx *gorm.DB) error {
|
||||||
|
if tx.Dialector.Name() == "postgres" {
|
||||||
|
return tx.Exec("SELECT pg_advisory_xact_lock(?)", int64(0x464c5658504f5254)).Error
|
||||||
|
}
|
||||||
|
if tx.Dialector.Name() == "sqlite" {
|
||||||
|
return tx.Model(&model.Node{}).Where("1 = 0").UpdateColumn("id", gorm.Expr("id")).Error
|
||||||
|
}
|
||||||
|
return errors.New("unsupported database for atomic port allocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockPortNodesTx must precede occupancy reads and reservation writes.
|
||||||
|
func (r *Repository) LockPortNodesTx(tx *gorm.DB, ids []int64) error {
|
||||||
|
if err := lockPortAllocationTx(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, id := range ids {
|
||||||
|
if id <= 0 {
|
||||||
|
return errors.New("节点不存在")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) usedPortSetTx(tx *gorm.DB, nodeID, excludeForwardID, excludeTunnelID int64, excludeRuntimeIDs ...int64) (map[int]bool, error) {
|
||||||
|
used := map[int]bool{}
|
||||||
|
var ports []int
|
||||||
|
if err := tx.Model(&model.ForwardPort{}).Where("node_id = ? AND forward_id <> ?", nodeID, excludeForwardID).Pluck("port", &ports).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, p := range ports {
|
||||||
|
used[p] = true
|
||||||
|
}
|
||||||
|
query := tx.Model(&model.ChainTunnel{}).Where("node_id = ? AND port > 0", nodeID)
|
||||||
|
if excludeTunnelID > 0 {
|
||||||
|
query = query.Where("tunnel_id <> ?", excludeTunnelID)
|
||||||
|
}
|
||||||
|
if err := query.Pluck("port", &ports).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, p := range ports {
|
||||||
|
used[p] = true
|
||||||
|
}
|
||||||
|
runtimeQuery := tx.Model(&model.PeerShareRuntime{}).Where("node_id = ? AND (status = 1 OR release_pending <> 0) AND port > 0", nodeID)
|
||||||
|
if len(excludeRuntimeIDs) > 0 {
|
||||||
|
runtimeQuery = runtimeQuery.Where("id <> ?", excludeRuntimeIDs[0])
|
||||||
|
}
|
||||||
|
if err := runtimeQuery.Pluck("port", &ports).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, p := range ports {
|
||||||
|
used[p] = true
|
||||||
|
}
|
||||||
|
pending, err := r.PendingTunnelCleanupPortsTx(tx, nodeID, excludeTunnelID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, p := range pending {
|
||||||
|
used[p] = true
|
||||||
|
}
|
||||||
|
return used, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection and insert must be in the same transaction after locking all nodes.
|
||||||
|
func (r *Repository) selectForwardPortTx(tx *gorm.DB, ids []int64, requested int, excludeForwardID int64, sequential bool) (int, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return 0, errors.New("隧道没有可用入口节点")
|
||||||
|
}
|
||||||
|
var common []int
|
||||||
|
for i, id := range ids {
|
||||||
|
var node model.Node
|
||||||
|
if err := tx.Where("id = ?", id).First(&node).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
spec := strings.TrimSpace(node.Port)
|
||||||
|
if spec == "" {
|
||||||
|
spec = "1000-65535"
|
||||||
|
if requested > 0 {
|
||||||
|
spec = "1-65535"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates := parsePortRangeSpec(spec)
|
||||||
|
used, err := r.usedPortSetTx(tx, id, excludeForwardID, 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var remote struct {
|
||||||
|
Start int `json:"portRangeStart"`
|
||||||
|
End int `json:"portRangeEnd"`
|
||||||
|
}
|
||||||
|
if node.IsRemote == 1 {
|
||||||
|
_ = json.Unmarshal([]byte(node.RemoteConfig.String), &remote)
|
||||||
|
}
|
||||||
|
available := map[int]bool{}
|
||||||
|
for _, p := range candidates {
|
||||||
|
if p <= 65535 && !used[p] && (remote.Start <= 0 || remote.End <= 0 || (p >= remote.Start && p <= remote.End)) {
|
||||||
|
available[p] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requested > 0 {
|
||||||
|
if !available[requested] {
|
||||||
|
return 0, errors.New("端口已占用或不在节点允许范围内")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
for _, p := range candidates {
|
||||||
|
if available[p] {
|
||||||
|
common = append(common, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filtered := common[:0]
|
||||||
|
for _, p := range common {
|
||||||
|
if available[p] {
|
||||||
|
filtered = append(filtered, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
common = filtered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if requested > 0 {
|
||||||
|
return requested, nil
|
||||||
|
}
|
||||||
|
if len(common) == 0 {
|
||||||
|
return 0, errors.New("节点端口已满,无可用端口")
|
||||||
|
}
|
||||||
|
if sequential {
|
||||||
|
return common[0], nil
|
||||||
|
}
|
||||||
|
idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(common))))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return common[idx.Int64()], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPeerRuntimePortTx permits only a verified federation-forward attachment
|
||||||
|
// to share its owning chain reservation. Other new reservations must be free.
|
||||||
|
func (r *Repository) checkPeerRuntimePortTx(tx *gorm.DB, item *model.PeerShareRuntime) error {
|
||||||
|
if item.Port <= 0 || (item.Status != 1 && item.ReleasePending == 0) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var ownedTunnel int64
|
||||||
|
if item.Role == "forward" {
|
||||||
|
var shareID, tunnelID int64
|
||||||
|
var port int
|
||||||
|
_, err := fmt.Sscanf(item.ResourceKey, "federation-forward-%d-%d-%d", &shareID, &tunnelID, &port)
|
||||||
|
if err == nil && shareID == item.ShareID && port == item.Port && item.ResourceKey == fmt.Sprintf("federation-forward-%d-%d-%d", shareID, tunnelID, port) {
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&model.ChainTunnel{}).Where("tunnel_id = ? AND node_id = ? AND port = ?", tunnelID, item.NodeID, item.Port).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
ownedTunnel = tunnelID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
used, err := r.usedPortSetTx(tx, item.NodeID, 0, ownedTunnel, item.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if used[item.Port] {
|
||||||
|
return errors.New("端口已占用")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go-backend/internal/store/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func portTestCreate(r *Repository, port int, nodes ...int64) (int64, error) {
|
||||||
|
return r.CreateForwardTx(1, "admin", "concurrent", 1, "127.0.0.1:80", "fifo", time.Now().UnixMilli(), 0, nodes, port, "", nil, 0, 0, nil, 0, 0, 0, true)
|
||||||
|
}
|
||||||
|
func TestAtomicPortAllocationAcrossRepositories(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "ports.db")
|
||||||
|
a, err := Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer a.Close()
|
||||||
|
b, err := Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Close()
|
||||||
|
runConcurrentPortAllocation(t, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runConcurrentPortAllocation(t *testing.T, a, b *Repository) {
|
||||||
|
for _, id := range []int64{1, 2} {
|
||||||
|
if err := a.db.Create(&model.Node{ID: id, Name: fmt.Sprint(id), Port: "20000-20031"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const count = 16
|
||||||
|
start := make(chan struct{})
|
||||||
|
errs := make(chan error, count)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
<-start
|
||||||
|
r := a
|
||||||
|
if i%2 == 1 {
|
||||||
|
r = b
|
||||||
|
}
|
||||||
|
_, err := portTestCreate(r, 0, 1, 2)
|
||||||
|
errs <- err
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
close(start)
|
||||||
|
wg.Wait()
|
||||||
|
close(errs)
|
||||||
|
for err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var rows []model.ForwardPort
|
||||||
|
if err := a.db.Order("port").Find(&rows).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rows) != count*2 {
|
||||||
|
t.Fatalf("got %d reservations", len(rows))
|
||||||
|
}
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, row := range rows {
|
||||||
|
k := fmt.Sprintf("%d:%d", row.NodeID, row.Port)
|
||||||
|
if seen[k] {
|
||||||
|
t.Fatalf("duplicate %s", k)
|
||||||
|
}
|
||||||
|
seen[k] = true
|
||||||
|
}
|
||||||
|
explicitStart := make(chan struct{})
|
||||||
|
explicitResults := make(chan error, 2)
|
||||||
|
for _, r := range []*Repository{a, b} {
|
||||||
|
go func(r *Repository) { <-explicitStart; _, err := portTestCreate(r, 20031, 1, 2); explicitResults <- err }(r)
|
||||||
|
}
|
||||||
|
close(explicitStart)
|
||||||
|
success := 0
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if <-explicitResults == nil {
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success != 1 {
|
||||||
|
t.Fatalf("explicit port race: %d successes, want exactly 1", success)
|
||||||
|
}
|
||||||
|
for p := 20000; p < 20000+count; p++ {
|
||||||
|
for _, n := range []int{1, 2} {
|
||||||
|
if !seen[fmt.Sprintf("%d:%d", n, p)] {
|
||||||
|
t.Fatalf("gap at %d:%d", n, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestAtomicPortAllocationRejectsConflictsAndExhaustion(t *testing.T) {
|
||||||
|
r, err := Open(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
if err := r.db.Create(&model.Node{ID: 1, Port: "21000-21001"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
id, err := portTestCreate(r, 21000, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := portTestCreate(r, 21000, 1); err == nil {
|
||||||
|
t.Fatal("explicit collision accepted")
|
||||||
|
}
|
||||||
|
if err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
return r.CreateChainTunnelTx(tx, 2, "2", 1, sql.NullInt64{Int64: 21000, Valid: true}, "", 0, "", "")
|
||||||
|
}); err == nil {
|
||||||
|
t.Fatal("chain collision accepted")
|
||||||
|
}
|
||||||
|
if _, err := portTestCreate(r, 0, 1); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := portTestCreate(r, 0, 1); err == nil {
|
||||||
|
t.Fatal("exhausted range accepted")
|
||||||
|
}
|
||||||
|
entries := []struct {
|
||||||
|
NodeID int64
|
||||||
|
Port int
|
||||||
|
InIP string
|
||||||
|
}{{1, 21001, ""}}
|
||||||
|
if err := r.ReplaceForwardPorts(id, entries, true); err == nil {
|
||||||
|
t.Fatal("replacement collision accepted")
|
||||||
|
}
|
||||||
|
var old model.ForwardPort
|
||||||
|
if err := r.db.Where("forward_id = ?", id).First(&old).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if old.Port != 21000 {
|
||||||
|
t.Fatal("failed replacement lost original reservation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomicPortAllocationPostgresAcrossRepositories(t *testing.T) {
|
||||||
|
dsn := os.Getenv("FLVX_POSTGRES_TEST_DSN")
|
||||||
|
if dsn == "" {
|
||||||
|
t.Skip("set FLVX_POSTGRES_TEST_DSN for isolated PostgreSQL allocation test")
|
||||||
|
}
|
||||||
|
admin, err := sql.Open("pgx", dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
schema := fmt.Sprintf("port_allocation_%d", time.Now().UnixNano())
|
||||||
|
if _, err := admin.Exec("CREATE SCHEMA " + schema); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { admin.Exec("DROP SCHEMA " + schema + " CASCADE"); admin.Close() })
|
||||||
|
scoped := dsn + " search_path=" + schema
|
||||||
|
if strings.Contains(dsn, "://") {
|
||||||
|
u, err := url.Parse(dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("search_path", schema)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
scoped = u.String()
|
||||||
|
}
|
||||||
|
a, err := OpenPostgres(scoped)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer a.Close()
|
||||||
|
b, err := OpenPostgres(scoped)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Close()
|
||||||
|
runConcurrentPortAllocation(t, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomicPortAllocationRespectsRangesAndPendingCleanup(t *testing.T) {
|
||||||
|
r, err := Open(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
nodes := []model.Node{{ID: 1, Port: "23004,23000-23002", IsRemote: 1, RemoteConfig: sql.NullString{String: `{"portRangeStart":23001,"portRangeEnd":23004}`, Valid: true}}, {ID: 2, Port: "23001-23004"}}
|
||||||
|
if err := r.db.Create(&nodes).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.db.Create(&model.TunnelRuntimeCleanup{TunnelID: 2, NodeID: 2, Kind: "service", Port: 23001}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
id, err := portTestCreate(r, 0, 1, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := r.GetMinForwardPort(id).Int64; got != 23002 {
|
||||||
|
t.Fatalf("got %d, want23002", got)
|
||||||
|
}
|
||||||
|
id2, err := portTestCreate(r, 0, 1, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := r.GetMinForwardPort(id2).Int64; got != 23004 {
|
||||||
|
t.Fatalf("got %d, want23004", got)
|
||||||
|
}
|
||||||
|
if err := r.DeleteForwardCascade(id); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
id3, err := portTestCreate(r, 0, 1, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := r.GetMinForwardPort(id3).Int64; got != 23002 {
|
||||||
|
t.Fatalf("released gap not reused: %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomicPortAllocationIndependentNewEntries(t *testing.T) {
|
||||||
|
r, err := Open(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer r.Close()
|
||||||
|
if err := r.db.Create(&[]model.Node{{ID: 1, Port: "24000-24001"}, {ID: 2, Port: "25000-25001"}}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
entries := []struct {
|
||||||
|
NodeID int64
|
||||||
|
Port int
|
||||||
|
InIP string
|
||||||
|
}{{1, 0, ""}, {2, 0, ""}}
|
||||||
|
if err := r.ReplaceForwardPortsIndividually(1, entries, true); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var rows []model.ForwardPort
|
||||||
|
if err := r.db.Order("node_id").Find(&rows).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(rows) != 2 || rows[0].Port != 24000 || rows[1].Port != 25000 {
|
||||||
|
t.Fatalf("unexpected independent allocation: %+v", rows)
|
||||||
|
}
|
||||||
|
if _, err := r.CreateFederationTunnel("conflict", 1, "tcp", 0, 1, 24000); err == nil {
|
||||||
|
t.Fatal("federation collision accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAtomicPortPeerRuntimeConflicts(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "mixed.db")
|
||||||
|
a, err := Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer a.Close()
|
||||||
|
b, err := Open(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer b.Close()
|
||||||
|
if err := a.db.Create(&model.Node{ID: 1, Port: "25000-25010"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
runtime := func(key string, port int) *model.PeerShareRuntime {
|
||||||
|
return &model.PeerShareRuntime{ShareID: 1, NodeID: 1, Port: port, ResourceKey: key, ReservationID: key, Status: 1}
|
||||||
|
}
|
||||||
|
start := make(chan struct{})
|
||||||
|
results := make(chan error, 2)
|
||||||
|
go func() { <-start; _, err := portTestCreate(a, 25000, 1); results <- err }()
|
||||||
|
go func() { <-start; results <- b.CreatePeerShareRuntime(runtime("peer", 25000)) }()
|
||||||
|
close(start)
|
||||||
|
success := 0
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if <-results == nil {
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if success != 1 {
|
||||||
|
t.Fatalf("mixed reservation race succeeded %d times", success)
|
||||||
|
}
|
||||||
|
first := runtime("first", 25001)
|
||||||
|
if err := a.CreatePeerShareRuntime(first); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := b.CreatePeerShareRuntime(runtime("second", 25001)); err == nil {
|
||||||
|
t.Fatal("duplicate peer port accepted")
|
||||||
|
}
|
||||||
|
first.Role = "middle"
|
||||||
|
first.Applied = 1
|
||||||
|
if err := a.UpdatePeerShareRuntime(first); err != nil {
|
||||||
|
t.Fatalf("own metadata update: %v", err)
|
||||||
|
}
|
||||||
|
owned := runtime("federation-forward-1-22-25002", 25002)
|
||||||
|
owned.Role = "forward"
|
||||||
|
if err := a.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
return a.CreateChainTunnelTx(tx, 22, "1", 1, sql.NullInt64{Int64: 25002, Valid: true}, "fifo", 0, "tcp", "")
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := a.CreatePeerShareRuntime(runtime("federation-forward-1-22-25002", 25002)); err == nil {
|
||||||
|
t.Fatal("generic spoof of ownership accepted")
|
||||||
|
}
|
||||||
|
if err := a.CreatePeerShareRuntime(owned); err != nil {
|
||||||
|
t.Fatalf("valid attachment rejected: %v", err)
|
||||||
|
}
|
||||||
|
other := runtime("other-forward", 25002)
|
||||||
|
other.Role = "forward"
|
||||||
|
if err := b.CreatePeerShareRuntime(other); err == nil {
|
||||||
|
t.Fatal("unowned forward attachment accepted")
|
||||||
|
}
|
||||||
|
pending := runtime("pending", 25003)
|
||||||
|
pending.Status = 0
|
||||||
|
pending.ReleasePending = 1
|
||||||
|
if err := a.db.Create(pending).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := portTestCreate(b, 25003, 1); err == nil {
|
||||||
|
t.Fatal("pending runtime reservation reused")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package repo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"go-backend/internal/relaysecurity"
|
"go-backend/internal/relaysecurity"
|
||||||
"go-backend/internal/store/model"
|
"go-backend/internal/store/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -22,7 +24,8 @@ func (r *Repository) RelayEndpointCredentials(tx *gorm.DB, tunnelID, nodeID int6
|
|||||||
err := find()
|
err := find()
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) && create {
|
if errors.Is(err, gorm.ErrRecordNotFound) && create {
|
||||||
var existing int64
|
var existing int64
|
||||||
if e := db.Model(&model.ChainTunnel{}).Where("tunnel_id = ? AND node_id = ? AND chain_type = ?", tunnelID, nodeID, role).Count(&existing).Error; e != nil {
|
// chain_tunnel stores roles as text; relay_endpoint_auth uses integers.
|
||||||
|
if e := db.Model(&model.ChainTunnel{}).Where("tunnel_id = ? AND node_id = ? AND chain_type = ?", tunnelID, nodeID, strconv.Itoa(role)).Count(&existing).Error; e != nil {
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
if existing > 0 {
|
if existing > 0 {
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-backend/internal/relaysecurity"
|
||||||
|
"go-backend/internal/store/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRelayEndpointCredentialsBindsLegacyChainTypeAsText(t *testing.T) {
|
||||||
|
for _, role := range []int{2, 3} {
|
||||||
|
t.Run(strconv.Itoa(role), func(t *testing.T) {
|
||||||
|
r, err := Open(filepath.Join(t.TempDir(), "relay.db"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = r.Close() })
|
||||||
|
|
||||||
|
// SQLite coerces integer parameters for this legacy text column.
|
||||||
|
// Check the actual repository query with pgx's text encoder as well,
|
||||||
|
// so the PostgreSQL regression is caught without a running server.
|
||||||
|
checked := 0
|
||||||
|
if err := r.DB().Callback().Query().After("gorm:query").Register("test:relay_chain_text_parameter", func(db *gorm.DB) {
|
||||||
|
if db.Statement.Table != "chain_tunnel" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
checked++
|
||||||
|
if len(db.Statement.Vars) != 3 {
|
||||||
|
db.AddError(fmt.Errorf("expected three chain lookup arguments, got %d", len(db.Statement.Vars)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
encoded, err := pgtype.NewMap().Encode(pgtype.TextOID, pgtype.TextFormatCode, db.Statement.Vars[2], nil)
|
||||||
|
if err != nil {
|
||||||
|
db.AddError(fmt.Errorf("encode chain_type for PostgreSQL: %w", err))
|
||||||
|
} else if string(encoded) != strconv.Itoa(role) {
|
||||||
|
db.AddError(fmt.Errorf("unexpected chain_type argument: %q", encoded))
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := model.Tunnel{Name: "relay", Type: 2, Flow: 1, RelayAuthMode: relaysecurity.Required}
|
||||||
|
if err := r.DB().Create(&tunnel).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const nodeID = int64(7)
|
||||||
|
if err := r.DB().Transaction(func(tx *gorm.DB) error {
|
||||||
|
created, err := r.RelayEndpointCredentials(tx, tunnel.ID, nodeID, role, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, create := range []bool{false, true} {
|
||||||
|
loaded, err := r.RelayEndpointCredentials(tx, tunnel.ID, nodeID, role, create)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *created != *loaded {
|
||||||
|
return fmt.Errorf("reloading endpoint changed credentials")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("create and reload endpoint: %v", err)
|
||||||
|
}
|
||||||
|
if checked != 1 {
|
||||||
|
t.Fatalf("expected one chain lookup for a new endpoint, got %d", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
chain := model.ChainTunnel{TunnelID: tunnel.ID, NodeID: nodeID, ChainType: strconv.Itoa(role)}
|
||||||
|
if err := r.DB().Create(&chain).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := r.DB().Where("tunnel_id = ?", tunnel.ID).Delete(&model.RelayEndpointAuth{}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := r.RelayEndpointCredentials(nil, tunnel.ID, nodeID, role, true); err == nil || !strings.Contains(err.Error(), "refusing regeneration") {
|
||||||
|
t.Fatalf("missing protected credentials must fail closed, got %v", err)
|
||||||
|
}
|
||||||
|
if checked != 2 {
|
||||||
|
t.Fatalf("expected chain lookup before refusing regeneration, got %d", checked)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := r.DB().Model(&model.RelayEndpointAuth{}).Count(&count).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if count != 0 {
|
||||||
|
t.Fatal("regenerated credentials for an existing protected endpoint")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (r *Repository) QueueTunnelRuntimeCleanupTx(tx *gorm.DB, items []model.TunnelRuntimeCleanup) error {
|
func (r *Repository) QueueTunnelRuntimeCleanupTx(tx *gorm.DB, items []model.TunnelRuntimeCleanup) error {
|
||||||
|
if err := lockPortAllocationTx(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
var node model.Node
|
var node model.Node
|
||||||
if err := tx.Select("id", "is_remote").Where("id = ?", item.NodeID).First(&node).Error; err != nil {
|
if err := tx.Select("id", "is_remote").Where("id = ?", item.NodeID).First(&node).Error; err != nil {
|
||||||
|
|||||||
@@ -440,13 +440,15 @@ func TestFederationDualPanelRemoteEntryRuntimeContract(t *testing.T) {
|
|||||||
func insertContractNode(t *testing.T, r *repo.Repository, name, ip, portRange, secret string, status int) int64 {
|
func insertContractNode(t *testing.T, r *repo.Repository, name, ip, portRange, secret string, status int) int64 {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
now := time.Now().UnixMilli()
|
now := time.Now().UnixMilli()
|
||||||
if err := r.DB().Exec(`
|
var id int64
|
||||||
|
if err := r.DB().Raw(`
|
||||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, name, secret, ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0).Error; err != nil {
|
RETURNING id
|
||||||
|
`, name, secret, ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0).Row().Scan(&id); err != nil {
|
||||||
t.Fatalf("insert node %s: %v", name, err)
|
t.Fatalf("insert node %s: %v", name, err)
|
||||||
}
|
}
|
||||||
return mustLastInsertID(t, r, name)
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertPeerShare(t *testing.T, r *repo.Repository, share *repo.PeerShare) int64 {
|
func insertPeerShare(t *testing.T, r *repo.Repository, share *repo.PeerShare) int64 {
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func TestPostgresBackupImportAdvancesEveryExplicitIDTable(t *testing.T) {
|
|||||||
{"tunnel", func() error { _, err := r.CreateTunnelTx(r.DB(), "new", 1, 1, 0, 0, 1, nil, 0, "", "", 0); return err }},
|
{"tunnel", func() error { _, err := r.CreateTunnelTx(r.DB(), "new", 1, 1, 0, 0, 1, nil, 0, "", "", 0); return err }},
|
||||||
{"chain tunnel", func() error { return r.CreateChainTunnelTx(r.DB(), 1, "1", 2, sql.NullInt64{}, "", 1, "tls", "") }},
|
{"chain tunnel", func() error { return r.CreateChainTunnelTx(r.DB(), 1, "1", 2, sql.NullInt64{}, "", 1, "tls", "") }},
|
||||||
{"forward", func() error {
|
{"forward", func() error {
|
||||||
_, err := r.CreateForwardTx(1, "old", "new", 1, "127.0.0.1:80", "", 0, 0, nil, 0, "", nil, 0, 0, nil, 0, 0, 0)
|
_, err := r.CreateForwardTx(1, "old", "new", 1, "127.0.0.1:80", "", 0, 0, []int64{1}, 12346, "", nil, 0, 0, nil, 0, 0, 0)
|
||||||
return err
|
return err
|
||||||
}},
|
}},
|
||||||
{"user tunnel", func() error { return r.InsertUserTunnel(2, 1, nil, 0, 0, 0, 0, 1) }},
|
{"user tunnel", func() error { return r.InsertUserTunnel(2, 1, nil, 0, 0, 0, 0, 1) }},
|
||||||
@@ -428,6 +428,9 @@ func TestPostgresBackupImportAvoidsCascadeDeleteDeadlock(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if err := r.DB().Create(&model.Node{ID: 1, Name: "entry", Port: "10000-20000"}).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
forward, err := r.CreateForwardTx(user, "delete-me", "old", 1, "127.0.0.1:80", "", 0, 0, []int64{1}, 12345, "", nil, 0, 0, nil, 0, 0, 0)
|
forward, err := r.CreateForwardTx(user, "delete-me", "old", 1, "127.0.0.1:80", "", 0, 0, []int64{1}, 12345, "", nil, 0, 0, nil, 0, 0, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -11,11 +11,24 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-backend/internal/auth"
|
"go-backend/internal/auth"
|
||||||
|
httpserver "go-backend/internal/http"
|
||||||
|
"go-backend/internal/http/handler"
|
||||||
"go-backend/internal/store/repo"
|
"go-backend/internal/store/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewRelayTunnelEmitsMatchingPrivateEndpointAuthentication(t *testing.T) {
|
func TestNewRelayTunnelEmitsMatchingPrivateEndpointAuthentication(t *testing.T) {
|
||||||
router, r := setupContractRouter(t, "relay-auth-contract")
|
router, r := setupContractRouter(t, "relay-auth-contract")
|
||||||
|
assertNewRelayTunnelEndpointAuthentication(t, router, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostgresNewRelayTunnelEmitsMatchingPrivateEndpointAuthentication(t *testing.T) {
|
||||||
|
r := ownerNamesRepository(t, "postgres")()
|
||||||
|
router := httpserver.NewRouter(handler.New(r, "relay-auth-contract"), "relay-auth-contract")
|
||||||
|
assertNewRelayTunnelEndpointAuthentication(t, router, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNewRelayTunnelEndpointAuthentication(t *testing.T, router http.Handler, r *repo.Repository) {
|
||||||
|
t.Helper()
|
||||||
server := httptest.NewServer(router)
|
server := httptest.NewServer(router)
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
token, err := auth.GenerateToken(1, "admin", 0, "relay-auth-contract")
|
token, err := auth.GenerateToken(1, "admin", 0, "relay-auth-contract")
|
||||||
|
|||||||
@@ -231,6 +231,13 @@ const CONFIG_ITEMS: ConfigItem[] = [
|
|||||||
"开启后,普通用户创建或编辑规则时可将目标地址指向 127.0.0.1、10.x.x.x、172.16-31.x.x、192.168.x.x 等本地或内网地址。默认关闭以降低开放代理风险。",
|
"开启后,普通用户创建或编辑规则时可将目标地址指向 127.0.0.1、10.x.x.x、172.16-31.x.x、192.168.x.x 等本地或内网地址。默认关闭以降低开放代理风险。",
|
||||||
type: "switch",
|
type: "switch",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "sequential_port_enabled",
|
||||||
|
label: "顺次分配入口端口",
|
||||||
|
description:
|
||||||
|
"开启后,创建规则时如果不指定入口端口,将从节点端口范围内按从小到大的顺序分配第一个空闲端口(例如范围 100-1000,已用 100、101、103,则下一个为 102,再下一个为 104)。关闭时为随机分配。",
|
||||||
|
type: "switch",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const BACKUP_TYPE_OPTIONS = [
|
const BACKUP_TYPE_OPTIONS = [
|
||||||
@@ -274,6 +281,7 @@ const getInitialConfigs = (): Record<string, string> => {
|
|||||||
"is_commercial",
|
"is_commercial",
|
||||||
"license_expiry",
|
"license_expiry",
|
||||||
"hide_footer_brand",
|
"hide_footer_brand",
|
||||||
|
"sequential_port_enabled",
|
||||||
];
|
];
|
||||||
const initialConfigs: Record<string, string> = {};
|
const initialConfigs: Record<string, string> = {};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user