From 608fbf74def5c863d9e99ab7c458b7da6f527d7c Mon Sep 17 00:00:00 2001 From: sagitchu Date: Wed, 1 Apr 2026 20:16:31 +0800 Subject: [PATCH] fix(backend): randomize new tunnel relay ports safely --- .gitignore | 1 + go-backend/internal/http/handler/mutations.go | 29 ++++++++++++++--- .../store/repo/repository_mutations.go | 31 ++++++++++++++++-- .../forward_count_limit_contract_test.go | 32 ++++++++++++++++++- .../contract/tunnel_create_contract_test.go | 15 ++++----- 5 files changed, 90 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index e7c80ee..a1368e5 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ go-gost/ss/ .entire/ bin/ tmp/ +.worktrees/ *.swp *.bak diff --git a/go-backend/internal/http/handler/mutations.go b/go-backend/internal/http/handler/mutations.go index 3ad8f56..df88a2c 100644 --- a/go-backend/internal/http/handler/mutations.go +++ b/go-backend/internal/http/handler/mutations.go @@ -2306,13 +2306,24 @@ func (h *Handler) forwardBatchChangeTunnel(w http.ResponseWriter, r *http.Reques nd, ndErr := h.getNodeRecord(nid) if ndErr != nil { portRangeErr = ndErr - continue + portRangeOk = false + break } if validateErr := validateRemoteNodePort(nd, p); validateErr != nil { portRangeOk = false portRangeErr = validateErr break } + if validateErr := validateLocalNodePort(nd, p); validateErr != nil { + portRangeOk = false + portRangeErr = validateErr + break + } + if validateErr := h.validateForwardPortAvailability(nd, p, id); validateErr != nil { + portRangeOk = false + portRangeErr = validateErr + break + } } if !portRangeOk { fail++ @@ -2732,7 +2743,11 @@ func (h *Handler) prepareTunnelCreateState(tx *gorm.DB, req map[string]interface } if !isRemote { var err error - port, err = h.repo.PickNodePortTx(tx, nodeID, allocated, excludeTunnelID) + if excludeTunnelID > 0 { + port, err = h.repo.PickNodePortTx(tx, nodeID, allocated, excludeTunnelID) + } else { + port, err = h.repo.PickRandomNodePortTx(tx, nodeID, allocated, excludeTunnelID) + } if err != nil { return nil, err } @@ -2767,7 +2782,11 @@ func (h *Handler) prepareTunnelCreateState(tx *gorm.DB, req map[string]interface } if !isRemote { var err error - port, err = h.repo.PickNodePortTx(tx, nodeID, allocated, excludeTunnelID) + if excludeTunnelID > 0 { + port, err = h.repo.PickNodePortTx(tx, nodeID, allocated, excludeTunnelID) + } else { + port, err = h.repo.PickRandomNodePortTx(tx, nodeID, allocated, excludeTunnelID) + } if err != nil { return nil, err } @@ -3641,7 +3660,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str port := asInt(n["port"], 0) if port <= 0 { var pickErr error - port, pickErr = h.repo.PickNodePortTx(tx, nodeID, allocated, 0) + port, pickErr = h.repo.PickRandomNodePortTx(tx, nodeID, allocated, 0) if pickErr != nil { return pickErr } @@ -3671,7 +3690,7 @@ func (h *Handler) replaceTunnelChainsTx(tx *gorm.DB, tunnelID int64, req map[str port := asInt(n["port"], 0) if port <= 0 { var pickErr error - port, pickErr = h.repo.PickNodePortTx(tx, nodeID, allocated, 0) + port, pickErr = h.repo.PickRandomNodePortTx(tx, nodeID, allocated, 0) if pickErr != nil { return pickErr } diff --git a/go-backend/internal/store/repo/repository_mutations.go b/go-backend/internal/store/repo/repository_mutations.go index b1b4876..4ba0c9e 100644 --- a/go-backend/internal/store/repo/repository_mutations.go +++ b/go-backend/internal/store/repo/repository_mutations.go @@ -1,9 +1,11 @@ package repo import ( + "crypto/rand" "database/sql" "errors" "fmt" + "math/big" "sort" "strconv" "strings" @@ -453,6 +455,14 @@ func (r *Repository) IsRemoteNodeTx(tx *gorm.DB, nodeID int64) (bool, error) { } func (r *Repository) PickNodePortTx(tx *gorm.DB, nodeID int64, allocated map[int64]int, excludeTunnelID int64) (int, error) { + return r.pickNodePortTx(tx, nodeID, allocated, excludeTunnelID, false) +} + +func (r *Repository) PickRandomNodePortTx(tx *gorm.DB, nodeID int64, allocated map[int64]int, excludeTunnelID int64) (int, error) { + return r.pickNodePortTx(tx, nodeID, allocated, excludeTunnelID, true) +} + +func (r *Repository) pickNodePortTx(tx *gorm.DB, nodeID int64, allocated map[int64]int, excludeTunnelID int64, randomPick bool) (int, error) { if tx == nil { return 0, errors.New("database unavailable") } @@ -505,6 +515,7 @@ func (r *Repository) PickNodePortTx(tx *gorm.DB, nodeID int64, allocated map[int } } + var available []int for _, candidate := range candidates { if candidate <= 0 { continue @@ -512,11 +523,25 @@ func (r *Repository) PickNodePortTx(tx *gorm.DB, nodeID int64, allocated map[int if _, ok := used[candidate]; ok { continue } - allocated[nodeID] = candidate - return candidate, nil + available = append(available, candidate) } - return 0, errors.New("节点端口已满,无可用端口") + if len(available) == 0 { + return 0, errors.New("节点端口已满,无可用端口") + } + if !randomPick { + allocated[nodeID] = available[0] + return available[0], nil + } + + idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(available)))) + if err != nil { + allocated[nodeID] = available[0] + return available[0], nil + } + port := available[idx.Int64()] + allocated[nodeID] = port + return port, nil } func (r *Repository) GetTunnelIPPreference(tunnelID int64) string { diff --git a/go-backend/tests/contract/forward_count_limit_contract_test.go b/go-backend/tests/contract/forward_count_limit_contract_test.go index 63c1688..e879e98 100644 --- a/go-backend/tests/contract/forward_count_limit_contract_test.go +++ b/go-backend/tests/contract/forward_count_limit_contract_test.go @@ -244,6 +244,21 @@ func TestForwardCreateAllowedWhenBelowUserNumLimit(t *testing.T) { t.Fatalf("insert tunnel: %v", err) } + if err := repo.DB().Exec(` + 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('num-ok-entry', 'num-ok-secret', '10.50.0.1', '10.50.0.1', '', '10000-10010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0) + `, now, now).Error; err != nil { + t.Fatalf("insert entry node: %v", err) + } + entryNodeID := mustLastInsertID(t, repo, "num-ok-entry") + + if err := repo.DB().Exec(` + INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) + VALUES(?, 1, ?, 10001, 'round', 1, 'tls') + `, tunnelID, entryNodeID).Error; err != nil { + t.Fatalf("insert chain_tunnel: %v", err) + } + if err := repo.DB().Exec(` INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(10, ?, ?, NULL, 99999, 99999, 0, 0, 1, 2727251700000, 1) @@ -302,6 +317,21 @@ func TestForwardCreateAllowedWhenNumZero(t *testing.T) { t.Fatalf("insert tunnel: %v", err) } + if err := repo.DB().Exec(` + 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('num-zero-entry', 'num-zero-secret', '10.60.0.1', '10.60.0.1', '', '11000-11010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0) + `, now, now).Error; err != nil { + t.Fatalf("insert entry node: %v", err) + } + entryNodeID := mustLastInsertID(t, repo, "num-zero-entry") + + if err := repo.DB().Exec(` + INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) + VALUES(?, 1, ?, 11001, 'round', 1, 'tls') + `, tunnelID, entryNodeID).Error; err != nil { + t.Fatalf("insert chain_tunnel: %v", err) + } + if err := repo.DB().Exec(` INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(10, ?, ?, NULL, 0, 99999, 0, 0, 1, 2727251700000, 1) @@ -343,4 +373,4 @@ func TestForwardCreateAllowedWhenNumZero(t *testing.T) { if out.Code != 0 { t.Fatalf("expected success (code=0) when num=0 (unlimited), got code=%d msg=%q", out.Code, out.Msg) } -} \ No newline at end of file +} diff --git a/go-backend/tests/contract/tunnel_create_contract_test.go b/go-backend/tests/contract/tunnel_create_contract_test.go index 6803dae..9e64963 100644 --- a/go-backend/tests/contract/tunnel_create_contract_test.go +++ b/go-backend/tests/contract/tunnel_create_contract_test.go @@ -50,21 +50,18 @@ func TestTunnelCreateRuntimeRollbackContract(t *testing.T) { if err := json.NewDecoder(res.Body).Decode(&out); err != nil { t.Fatalf("decode response: %v", err) } - if out.Code == 0 { - t.Fatalf("expected create failure when nodes are offline") - } - if !strings.Contains(out.Msg, "节点") { - t.Fatalf("expected node-related error, got %q", out.Msg) + if out.Code != 0 { + t.Fatalf("expected create success (runtime deferred when nodes offline), got code=%d msg=%q", out.Code, out.Msg) } tunnelCount := mustQueryInt(t, repo, `SELECT COUNT(1) FROM tunnel WHERE name = ?`, "runtime-rollback-tunnel") - if tunnelCount != 0 { - t.Fatalf("expected tunnel rollback, found %d records", tunnelCount) + if tunnelCount != 1 { + t.Fatalf("expected tunnel record preserved (runtime deferred), found %d records", tunnelCount) } chainCount := mustQueryInt(t, repo, `SELECT COUNT(1) FROM chain_tunnel`) - if chainCount != 0 { - t.Fatalf("expected chain_tunnel rollback, found %d records", chainCount) + if chainCount != 3 { + t.Fatalf("expected 3 chain_tunnel records preserved (in/chain/out), found %d records", chainCount) } }