mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 16:06:36 +08:00
1b4500202a
* docs(agents): update knowledge base with encryption, API envelope, and build conventions Add comprehensive documentation of project conventions including: - Encryption patterns (AES with node secret PSK) - API envelope structure (code, msg, data, ts) - Build peculiarities (minify: false, rolldown-vite, UPX compression) - Unique styles (flat monorepo, asymmetric Go layout, hybrid frontend mode) - Module boundaries and anti-patterns - Large file hotspots and code map references Updated 7 AGENTS.md files across root and submodules. * test(backend): add comprehensive dual-stack IP preference test suite Added 43 tests covering: - Core IP selection logic (selectTunnelDialHost) - Node capability detection (nodeSupportsV4/V6) - Address picker functions - API contract tests for create/update/list - Database compatibility (SQLite + PostgreSQL) Fixed pre-existing broken test in federation_runtime_test.go
296 lines
9.8 KiB
Go
296 lines
9.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"go-backend/internal/http/response"
|
|
"go-backend/internal/store/sqlite"
|
|
)
|
|
|
|
func TestPickPeerSharePortUsesRuntimeReservations(t *testing.T) {
|
|
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
|
|
if err != nil {
|
|
t.Fatalf("open repo: %v", err)
|
|
}
|
|
defer repo.Close()
|
|
|
|
h := &Handler{repo: repo}
|
|
now := time.Now().UnixMilli()
|
|
|
|
if _, err := repo.DB().Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, ?, ?, ?, ?, ?, ?)`, 1, 2, 1, 3000, "round", 1, "tls"); err != nil {
|
|
t.Fatalf("insert chain_tunnel: %v", err)
|
|
}
|
|
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, 1, 1, 3001); err != nil {
|
|
t.Fatalf("insert forward_port: %v", err)
|
|
}
|
|
if _, err := repo.DB().Exec(`
|
|
INSERT INTO peer_share_runtime(share_id, node_id, reservation_id, resource_key, binding_id, role, chain_name, service_name, protocol, strategy, port, target, applied, status, created_time, updated_time)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, 77, 1, "res-1", "rk-1", "b-1", "exit", "", "fed_svc_1", "tls", "round", 3002, "", 1, 1, now, now); err != nil {
|
|
t.Fatalf("insert peer_share_runtime: %v", err)
|
|
}
|
|
|
|
share := &sqlite.PeerShare{
|
|
ID: 77,
|
|
NodeID: 1,
|
|
PortRangeStart: 3000,
|
|
PortRangeEnd: 3004,
|
|
}
|
|
|
|
port, err := h.pickPeerSharePort(share, 0)
|
|
if err != nil {
|
|
t.Fatalf("pick auto port: %v", err)
|
|
}
|
|
if port != 3003 {
|
|
t.Fatalf("expected port 3003, got %d", port)
|
|
}
|
|
|
|
if _, err := h.pickPeerSharePort(share, 3001); err == nil {
|
|
t.Fatalf("expected requested busy port to fail")
|
|
}
|
|
}
|
|
|
|
func TestApplyTunnelRuntimeSkipsRemoteChainAndOutNodes(t *testing.T) {
|
|
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "rt-skip.db"))
|
|
if err != nil {
|
|
t.Fatalf("open repo: %v", err)
|
|
}
|
|
defer repo.Close()
|
|
|
|
h := &Handler{repo: repo}
|
|
now := time.Now().UnixMilli()
|
|
for _, n := range []struct {
|
|
id int64
|
|
name string
|
|
ip string
|
|
}{
|
|
{12, "remote-chain", "10.99.0.2"},
|
|
{13, "remote-out", "10.99.0.3"},
|
|
} {
|
|
if _, err := repo.DB().Exec(`
|
|
INSERT INTO node(id, 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, is_remote, remote_url, remote_token)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, n.id, n.name, n.name+"-secret", n.ip, n.ip, "", "40000-40010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0, 1, "http://remote-peer", "remote-token"); err != nil {
|
|
t.Fatalf("insert node %s: %v", n.name, err)
|
|
}
|
|
}
|
|
|
|
state := &tunnelCreateState{
|
|
TunnelID: 1,
|
|
Type: 2,
|
|
InNodes: []tunnelRuntimeNode{},
|
|
ChainHops: [][]tunnelRuntimeNode{
|
|
{
|
|
{NodeID: 12, ChainType: 2, Inx: 1, Port: 41000, Protocol: "tls", Strategy: "round"},
|
|
},
|
|
},
|
|
OutNodes: []tunnelRuntimeNode{
|
|
{NodeID: 13, ChainType: 3, Port: 42000, Protocol: "tls", Strategy: "round"},
|
|
},
|
|
Nodes: map[int64]*nodeRecord{
|
|
12: {ID: 12, Name: "remote-chain", IsRemote: 1, ServerIPv4: "10.99.0.2"},
|
|
13: {ID: 13, Name: "remote-out", IsRemote: 1, ServerIPv4: "10.99.0.3"},
|
|
},
|
|
}
|
|
|
|
chains, services, err := h.applyTunnelRuntime(state)
|
|
if err != nil {
|
|
t.Fatalf("apply runtime: %v", err)
|
|
}
|
|
if len(chains) != 0 {
|
|
t.Fatalf("expected no local chains for remote-only nodes, got %d", len(chains))
|
|
}
|
|
if len(services) != 0 {
|
|
t.Fatalf("expected no local services for remote-only nodes, got %d", len(services))
|
|
}
|
|
}
|
|
|
|
func TestPrepareTunnelCreateStateRemoteAutoPortDefersToFederation(t *testing.T) {
|
|
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
|
|
if err != nil {
|
|
t.Fatalf("open repo: %v", err)
|
|
}
|
|
defer repo.Close()
|
|
|
|
h := &Handler{repo: repo}
|
|
now := time.Now().UnixMilli()
|
|
|
|
insertNode := func(name string, status int, portRange string, isRemote int) int64 {
|
|
res, execErr := 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, is_remote, remote_url, remote_token, remote_config)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, name, name+"-secret", "10.0.0.1", "10.0.0.1", "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0, isRemote, "http://peer", "peer-token", `{"shareId":1}`)
|
|
if execErr != nil {
|
|
t.Fatalf("insert node %s: %v", name, execErr)
|
|
}
|
|
id, idErr := res.LastInsertId()
|
|
if idErr != nil {
|
|
t.Fatalf("node id %s: %v", name, idErr)
|
|
}
|
|
return id
|
|
}
|
|
|
|
entryID := insertNode("entry", 1, "31000-31010", 0)
|
|
remoteOutID := insertNode("remote-out", 1, "30000", 1)
|
|
|
|
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, 1, remoteOutID, 30000); err != nil {
|
|
t.Fatalf("insert forward_port: %v", err)
|
|
}
|
|
|
|
tx, err := repo.DB().Begin()
|
|
if err != nil {
|
|
t.Fatalf("begin tx: %v", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
req := map[string]interface{}{
|
|
"name": "test-tunnel",
|
|
"inNodeId": []interface{}{
|
|
map[string]interface{}{"nodeId": float64(entryID), "protocol": "tls", "strategy": "round"},
|
|
},
|
|
"outNodeId": []interface{}{
|
|
map[string]interface{}{"nodeId": float64(remoteOutID), "protocol": "tls", "strategy": "round", "port": float64(0)},
|
|
},
|
|
"chainNodes": []interface{}{},
|
|
}
|
|
|
|
state, err := h.prepareTunnelCreateState(tx, req, 2, 0)
|
|
if err != nil {
|
|
t.Fatalf("prepare state should not fail for remote auto-port: %v", err)
|
|
}
|
|
if len(state.OutNodes) != 1 {
|
|
t.Fatalf("expected 1 out node, got %d", len(state.OutNodes))
|
|
}
|
|
if state.OutNodes[0].Port != 0 {
|
|
t.Fatalf("expected remote out port to remain 0 before federation reserve, got %d", state.OutNodes[0].Port)
|
|
}
|
|
}
|
|
|
|
func TestPrepareTunnelCreateStateAllowsOfflineRemoteMiddleNode(t *testing.T) {
|
|
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
|
|
if err != nil {
|
|
t.Fatalf("open repo: %v", err)
|
|
}
|
|
defer repo.Close()
|
|
|
|
h := &Handler{repo: repo}
|
|
now := time.Now().UnixMilli()
|
|
|
|
insertNode := func(name string, status int, portRange string, isRemote int) int64 {
|
|
res, execErr := 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, is_remote, remote_url, remote_token, remote_config)
|
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, name, name+"-secret", "10.0.0.1", "10.0.0.1", "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0, isRemote, "http://peer", "peer-token", `{"shareId":2}`)
|
|
if execErr != nil {
|
|
t.Fatalf("insert node %s: %v", name, execErr)
|
|
}
|
|
id, idErr := res.LastInsertId()
|
|
if idErr != nil {
|
|
t.Fatalf("node id %s: %v", name, idErr)
|
|
}
|
|
return id
|
|
}
|
|
|
|
entryID := insertNode("entry-local", 1, "32000-32010", 0)
|
|
remoteMiddleID := insertNode("middle-remote", 0, "33000-33010", 1)
|
|
outID := insertNode("out-local", 1, "34000-34010", 0)
|
|
|
|
tx, err := repo.DB().Begin()
|
|
if err != nil {
|
|
t.Fatalf("begin tx: %v", err)
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
req := map[string]interface{}{
|
|
"name": "remote-middle-offline-status",
|
|
"inNodeId": []interface{}{
|
|
map[string]interface{}{"nodeId": float64(entryID), "protocol": "tls", "strategy": "round"},
|
|
},
|
|
"chainNodes": []interface{}{
|
|
[]interface{}{
|
|
map[string]interface{}{"nodeId": float64(remoteMiddleID), "protocol": "tls", "strategy": "round", "port": float64(0)},
|
|
},
|
|
},
|
|
"outNodeId": []interface{}{
|
|
map[string]interface{}{"nodeId": float64(outID), "protocol": "tls", "strategy": "round", "port": float64(0)},
|
|
},
|
|
}
|
|
|
|
state, err := h.prepareTunnelCreateState(tx, req, 2, 0)
|
|
if err != nil {
|
|
t.Fatalf("prepare state should allow offline remote middle node: %v", err)
|
|
}
|
|
if len(state.ChainHops) != 1 || len(state.ChainHops[0]) != 1 {
|
|
t.Fatalf("expected one middle hop node, got %+v", state.ChainHops)
|
|
}
|
|
if state.ChainHops[0][0].NodeID != remoteMiddleID {
|
|
t.Fatalf("expected remote middle node id %d, got %d", remoteMiddleID, state.ChainHops[0][0].NodeID)
|
|
}
|
|
if state.Nodes[remoteMiddleID] == nil || state.Nodes[remoteMiddleID].IsRemote != 1 {
|
|
t.Fatalf("expected remote middle node metadata in state")
|
|
}
|
|
}
|
|
|
|
func TestFederationRuntimeReservePortRejectsWhenShareFlowExceeded(t *testing.T) {
|
|
repo, err := sqlite.Open(filepath.Join(t.TempDir(), "panel.db"))
|
|
if err != nil {
|
|
t.Fatalf("open repo: %v", err)
|
|
}
|
|
defer repo.Close()
|
|
|
|
h := &Handler{repo: repo}
|
|
now := time.Now().UnixMilli()
|
|
|
|
if err := repo.CreatePeerShare(&sqlite.PeerShare{
|
|
Name: "limited-share",
|
|
NodeID: 1,
|
|
Token: "limited-token",
|
|
MaxBandwidth: 2048,
|
|
CurrentFlow: 2048,
|
|
PortRangeStart: 30000,
|
|
PortRangeEnd: 30010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
}); err != nil {
|
|
t.Fatalf("create share: %v", err)
|
|
}
|
|
|
|
body, err := json.Marshal(map[string]interface{}{
|
|
"resourceKey": "tunnel:1:node:1:type:3:hop:0",
|
|
"protocol": "tls",
|
|
"requestedPort": 0,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal request: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/federation/runtime/reserve-port", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer limited-token")
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
|
|
h.federationRuntimeReservePort(res, req)
|
|
|
|
if res.Code != http.StatusOK {
|
|
t.Fatalf("expected status %d, got %d", http.StatusOK, res.Code)
|
|
}
|
|
|
|
var payload response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if payload.Code != 403 {
|
|
t.Fatalf("expected response code 403, got %d (%s)", payload.Code, payload.Msg)
|
|
}
|
|
if payload.Msg != "Share traffic limit exceeded" {
|
|
t.Fatalf("unexpected response message: %q", payload.Msg)
|
|
}
|
|
}
|