mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-30 16:26:36 +08:00
750 lines
26 KiB
Go
750 lines
26 KiB
Go
package contract_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"go-backend/internal/auth"
|
|
"go-backend/internal/http/response"
|
|
"go-backend/internal/security"
|
|
"go-backend/internal/store/repo"
|
|
)
|
|
|
|
func TestFederationDualPanelMiddleExitAutoPortContract(t *testing.T) {
|
|
providerSecret := "provider-contract-jwt"
|
|
providerRouter, providerRepo := setupContractRouter(t, providerSecret)
|
|
providerServer := httptest.NewServer(providerRouter)
|
|
defer providerServer.Close()
|
|
|
|
consumerSecret := "consumer-contract-jwt"
|
|
consumerRouter, consumerRepo := setupContractRouter(t, consumerSecret)
|
|
|
|
consumerAdminToken, err := auth.GenerateToken(1, "consumer-admin", 0, consumerSecret)
|
|
if err != nil {
|
|
t.Fatalf("generate consumer admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
providerEntryNodeID := insertContractNode(t, providerRepo, "provider-entry", "198.51.100.11", "43000-43010", "provider-entry-secret", 1)
|
|
providerMiddleNodeID := insertContractNode(t, providerRepo, "provider-middle", "198.51.100.12", "44000-44010", "provider-middle-secret", 1)
|
|
providerExitNodeID := insertContractNode(t, providerRepo, "provider-exit", "198.51.100.13", "45000-45010", "provider-exit-secret", 1)
|
|
|
|
entryShareID := insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "entry-share",
|
|
NodeID: providerEntryNodeID,
|
|
Token: "share-entry-token",
|
|
PortRangeStart: 43000,
|
|
PortRangeEnd: 43010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
middleShareID := insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "middle-share",
|
|
NodeID: providerMiddleNodeID,
|
|
Token: "share-middle-token",
|
|
PortRangeStart: 44000,
|
|
PortRangeEnd: 44010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
exitShareID := insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "exit-share",
|
|
NodeID: providerExitNodeID,
|
|
Token: "share-exit-token",
|
|
PortRangeStart: 45000,
|
|
PortRangeEnd: 45010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-entry-token")
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-middle-token")
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-exit-token")
|
|
|
|
entryRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-entry-token")
|
|
middleRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-middle-token")
|
|
exitRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-exit-token")
|
|
|
|
stopEntry := startMockNodeSession(t, providerServer.URL, "provider-entry-secret")
|
|
defer stopEntry()
|
|
stopMiddle := startMockNodeSession(t, providerServer.URL, "provider-middle-secret")
|
|
defer stopMiddle()
|
|
stopExit := startMockNodeSession(t, providerServer.URL, "provider-exit-secret")
|
|
defer stopExit()
|
|
|
|
createTunnel := func(name string) int64 {
|
|
payload := map[string]interface{}{
|
|
"name": name,
|
|
"type": 2,
|
|
"flow": 99999,
|
|
"status": 1,
|
|
"inNodeId": []map[string]interface{}{
|
|
{"nodeId": entryRemoteNodeID, "protocol": "tls", "strategy": "round"},
|
|
},
|
|
"chainNodes": [][]map[string]interface{}{
|
|
{{"nodeId": middleRemoteNodeID, "protocol": "tls", "strategy": "round"}},
|
|
},
|
|
"outNodeId": []map[string]interface{}{
|
|
{"nodeId": exitRemoteNodeID, "protocol": "tls", "strategy": "round"},
|
|
},
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal create payload: %v", err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", consumerAdminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
consumerRouter.ServeHTTP(res, req)
|
|
assertCode(t, res, 0)
|
|
|
|
tunnelID := mustQueryInt64(t, consumerRepo, `SELECT id FROM tunnel WHERE name = ? ORDER BY id DESC LIMIT 1`, name)
|
|
if tunnelID <= 0 {
|
|
t.Fatalf("invalid tunnel id for %s", name)
|
|
}
|
|
return tunnelID
|
|
}
|
|
|
|
firstTunnelID := createTunnel("dual-panel-middle-exit-1")
|
|
|
|
assertTunnelPortInRange(t, consumerRepo, firstTunnelID, 2, middleRemoteNodeID, 44000, 44010)
|
|
assertTunnelPortInRange(t, consumerRepo, firstTunnelID, 3, exitRemoteNodeID, 45000, 45010)
|
|
|
|
assertCount(t, consumerRepo, `SELECT COUNT(1) FROM federation_tunnel_binding WHERE tunnel_id = ? AND status = 1`, firstTunnelID, 2)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1 AND applied = 1`, middleShareID, 1)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1 AND applied = 1`, exitShareID, 1)
|
|
|
|
deleteBody, err := json.Marshal(map[string]interface{}{"id": firstTunnelID})
|
|
if err != nil {
|
|
t.Fatalf("marshal delete payload: %v", err)
|
|
}
|
|
deleteReq := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/delete", bytes.NewReader(deleteBody))
|
|
deleteReq.Header.Set("Authorization", consumerAdminToken)
|
|
deleteReq.Header.Set("Content-Type", "application/json")
|
|
deleteRes := httptest.NewRecorder()
|
|
consumerRouter.ServeHTTP(deleteRes, deleteReq)
|
|
assertCode(t, deleteRes, 0)
|
|
|
|
assertCount(t, consumerRepo, `SELECT COUNT(1) FROM federation_tunnel_binding WHERE tunnel_id = ?`, firstTunnelID, 0)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 0`, middleShareID, 1)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 0`, exitShareID, 1)
|
|
|
|
secondTunnelID := createTunnel("dual-panel-middle-exit-2")
|
|
assertTunnelPortInRange(t, consumerRepo, secondTunnelID, 2, middleRemoteNodeID, 44000, 44010)
|
|
assertTunnelPortInRange(t, consumerRepo, secondTunnelID, 3, exitRemoteNodeID, 45000, 45010)
|
|
|
|
forwardPayload := map[string]interface{}{
|
|
"name": "dual-panel-remote-entry-forward",
|
|
"tunnelId": secondTunnelID,
|
|
"remoteAddr": "1.1.1.1:443",
|
|
"strategy": "fifo",
|
|
}
|
|
forwardBody, err := json.Marshal(forwardPayload)
|
|
if err != nil {
|
|
t.Fatalf("marshal forward payload: %v", err)
|
|
}
|
|
forwardReq := httptest.NewRequest(http.MethodPost, "/api/v1/forward/create", bytes.NewReader(forwardBody))
|
|
forwardReq.Header.Set("Authorization", consumerAdminToken)
|
|
forwardReq.Header.Set("Content-Type", "application/json")
|
|
forwardRes := httptest.NewRecorder()
|
|
consumerRouter.ServeHTTP(forwardRes, forwardReq)
|
|
assertCode(t, forwardRes, 0)
|
|
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1 AND applied = 1`, middleShareID, 1)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1 AND applied = 1`, exitShareID, 1)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1 AND applied = 1`, entryShareID, 1)
|
|
}
|
|
|
|
func TestFederationDualPanelRemoteDiagnosisContract(t *testing.T) {
|
|
providerSecret := "provider-contract-jwt"
|
|
providerRouter, providerRepo := setupContractRouter(t, providerSecret)
|
|
providerServer := httptest.NewServer(providerRouter)
|
|
defer providerServer.Close()
|
|
|
|
consumerSecret := "consumer-contract-jwt"
|
|
consumerRouter, consumerRepo := setupContractRouter(t, consumerSecret)
|
|
|
|
consumerAdminToken, err := auth.GenerateToken(1, "consumer-admin", 0, consumerSecret)
|
|
if err != nil {
|
|
t.Fatalf("generate consumer admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
providerEntryNodeID := insertContractNode(t, providerRepo, "provider-entry-dx", "203.0.113.11", "53000-53010", "provider-entry-dx-secret", 1)
|
|
providerMiddleNodeID := insertContractNode(t, providerRepo, "provider-middle-dx", "203.0.113.12", "54000-54010", "provider-middle-dx-secret", 1)
|
|
providerExitNodeID := insertContractNode(t, providerRepo, "provider-exit-dx", "203.0.113.13", "55000-55010", "provider-exit-dx-secret", 1)
|
|
|
|
entryShareID := insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "entry-share-dx",
|
|
NodeID: providerEntryNodeID,
|
|
Token: "share-entry-dx-token",
|
|
PortRangeStart: 53000,
|
|
PortRangeEnd: 53010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
middleShareID := insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "middle-share-dx",
|
|
NodeID: providerMiddleNodeID,
|
|
Token: "share-middle-dx-token",
|
|
PortRangeStart: 54000,
|
|
PortRangeEnd: 54010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
exitShareID := insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "exit-share-dx",
|
|
NodeID: providerExitNodeID,
|
|
Token: "share-exit-dx-token",
|
|
PortRangeStart: 55000,
|
|
PortRangeEnd: 55010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-entry-dx-token")
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-middle-dx-token")
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-exit-dx-token")
|
|
|
|
entryRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-entry-dx-token")
|
|
middleRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-middle-dx-token")
|
|
exitRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-exit-dx-token")
|
|
|
|
stopMiddle := startMockNodeSession(t, providerServer.URL, "provider-middle-dx-secret")
|
|
defer stopMiddle()
|
|
stopExit := startMockNodeSession(t, providerServer.URL, "provider-exit-dx-secret")
|
|
defer stopExit()
|
|
|
|
createPayload := map[string]interface{}{
|
|
"name": "dual-panel-diagnose-remote",
|
|
"type": 2,
|
|
"flow": 99999,
|
|
"status": 1,
|
|
"inNodeId": []map[string]interface{}{
|
|
{"nodeId": entryRemoteNodeID, "protocol": "tls", "strategy": "round"},
|
|
},
|
|
"chainNodes": [][]map[string]interface{}{
|
|
{{"nodeId": middleRemoteNodeID, "protocol": "tls", "strategy": "round"}},
|
|
},
|
|
"outNodeId": []map[string]interface{}{
|
|
{"nodeId": exitRemoteNodeID, "protocol": "tls", "strategy": "round"},
|
|
},
|
|
}
|
|
body, err := json.Marshal(createPayload)
|
|
if err != nil {
|
|
t.Fatalf("marshal create payload: %v", err)
|
|
}
|
|
createReq := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", bytes.NewReader(body))
|
|
createReq.Header.Set("Authorization", consumerAdminToken)
|
|
createReq.Header.Set("Content-Type", "application/json")
|
|
createRes := httptest.NewRecorder()
|
|
consumerRouter.ServeHTTP(createRes, createReq)
|
|
assertCode(t, createRes, 0)
|
|
|
|
tunnelID := mustQueryInt64(t, consumerRepo, `SELECT id FROM tunnel WHERE name = ? ORDER BY id DESC LIMIT 1`, "dual-panel-diagnose-remote")
|
|
if tunnelID <= 0 {
|
|
t.Fatalf("invalid tunnel id")
|
|
}
|
|
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1 AND applied = 1`, middleShareID, 1)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ? AND status = 1 AND applied = 1`, exitShareID, 1)
|
|
assertCount(t, providerRepo, `SELECT COUNT(1) FROM peer_share_runtime WHERE share_id = ?`, entryShareID, 0)
|
|
|
|
diagnoseReq := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/diagnose", bytes.NewBufferString(fmt.Sprintf(`{"tunnelId":%d}`, tunnelID)))
|
|
diagnoseReq.Header.Set("Authorization", consumerAdminToken)
|
|
diagnoseRes := httptest.NewRecorder()
|
|
consumerRouter.ServeHTTP(diagnoseRes, diagnoseReq)
|
|
|
|
var out response.R
|
|
if err := json.NewDecoder(diagnoseRes.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode diagnose response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected diagnose code 0, got %d (%s)", out.Code, out.Msg)
|
|
}
|
|
|
|
payload, ok := out.Data.(map[string]interface{})
|
|
if !ok {
|
|
t.Fatalf("expected map payload, got %T", out.Data)
|
|
}
|
|
results, ok := payload["results"].([]interface{})
|
|
if !ok || len(results) == 0 {
|
|
t.Fatalf("expected non-empty results, got %v", payload["results"])
|
|
}
|
|
|
|
chainToExitFound := false
|
|
for _, raw := range results {
|
|
item, ok := raw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if valueAsInt(item["fromChainType"]) == 2 && valueAsInt(item["toChainType"]) == 3 {
|
|
chainToExitFound = true
|
|
if !valueAsBool(item["success"]) {
|
|
t.Fatalf("expected chain->exit diagnosis success, got item=%v", item)
|
|
}
|
|
if msg := strings.TrimSpace(valueAsString(item["message"])); msg != "mock tcp ok" {
|
|
t.Fatalf("expected remote diagnosis message 'mock tcp ok', got %q", msg)
|
|
}
|
|
}
|
|
}
|
|
if !chainToExitFound {
|
|
t.Fatalf("expected chain->exit diagnosis item in results")
|
|
}
|
|
}
|
|
|
|
func TestFederationDualPanelRemoteEntryRuntimeContract(t *testing.T) {
|
|
providerSecret := "provider-contract-jwt"
|
|
providerRouter, providerRepo := setupContractRouter(t, providerSecret)
|
|
providerServer := httptest.NewServer(providerRouter)
|
|
defer providerServer.Close()
|
|
|
|
consumerSecret := "consumer-contract-jwt"
|
|
consumerRouter, consumerRepo := setupContractRouter(t, consumerSecret)
|
|
|
|
consumerAdminToken, err := auth.GenerateToken(1, "consumer-admin", 0, consumerSecret)
|
|
if err != nil {
|
|
t.Fatalf("generate consumer admin token: %v", err)
|
|
}
|
|
|
|
now := time.Now().UnixMilli()
|
|
providerEntryNodeID := insertContractNode(t, providerRepo, "provider-entry-rt", "198.51.100.21", "43020-43030", "provider-entry-rt-secret", 1)
|
|
providerMiddleNodeID := insertContractNode(t, providerRepo, "provider-middle-rt", "198.51.100.22", "44020-44030", "provider-middle-rt-secret", 1)
|
|
providerExitNodeID := insertContractNode(t, providerRepo, "provider-exit-rt", "198.51.100.23", "45020-45030", "provider-exit-rt-secret", 1)
|
|
|
|
insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "entry-share-rt",
|
|
NodeID: providerEntryNodeID,
|
|
Token: "share-entry-rt-token",
|
|
PortRangeStart: 43020,
|
|
PortRangeEnd: 43030,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "middle-share-rt",
|
|
NodeID: providerMiddleNodeID,
|
|
Token: "share-middle-rt-token",
|
|
PortRangeStart: 44020,
|
|
PortRangeEnd: 44030,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "exit-share-rt",
|
|
NodeID: providerExitNodeID,
|
|
Token: "share-exit-rt-token",
|
|
PortRangeStart: 45020,
|
|
PortRangeEnd: 45030,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-entry-rt-token")
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-middle-rt-token")
|
|
importRemoteNodeForContract(t, consumerRouter, consumerAdminToken, providerServer.URL, "share-exit-rt-token")
|
|
|
|
entryRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-entry-rt-token")
|
|
middleRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-middle-rt-token")
|
|
exitRemoteNodeID := queryRemoteNodeIDByToken(t, consumerRepo, "share-exit-rt-token")
|
|
|
|
var commandMu sync.Mutex
|
|
entryCommands := make([]string, 0, 8)
|
|
stopEntry := startMockNodeSessionWithHook(t, providerServer.URL, "provider-entry-rt-secret", func(cmdType string) {
|
|
commandMu.Lock()
|
|
entryCommands = append(entryCommands, cmdType)
|
|
commandMu.Unlock()
|
|
})
|
|
defer stopEntry()
|
|
stopMiddle := startMockNodeSession(t, providerServer.URL, "provider-middle-rt-secret")
|
|
defer stopMiddle()
|
|
stopExit := startMockNodeSession(t, providerServer.URL, "provider-exit-rt-secret")
|
|
defer stopExit()
|
|
|
|
createTunnel := func(name string) int64 {
|
|
payload := map[string]interface{}{
|
|
"name": name,
|
|
"type": 2,
|
|
"flow": 99999,
|
|
"status": 1,
|
|
"inNodeId": []map[string]interface{}{
|
|
{"nodeId": entryRemoteNodeID, "protocol": "tls", "strategy": "round"},
|
|
},
|
|
"chainNodes": [][]map[string]interface{}{
|
|
{{"nodeId": middleRemoteNodeID, "protocol": "tls", "strategy": "round"}},
|
|
},
|
|
"outNodeId": []map[string]interface{}{
|
|
{"nodeId": exitRemoteNodeID, "protocol": "tls", "strategy": "round"},
|
|
},
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal create payload: %v", err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", consumerAdminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
consumerRouter.ServeHTTP(res, req)
|
|
assertCode(t, res, 0)
|
|
|
|
tunnelID := mustQueryInt64(t, consumerRepo, `SELECT id FROM tunnel WHERE name = ? ORDER BY id DESC LIMIT 1`, name)
|
|
if tunnelID <= 0 {
|
|
t.Fatalf("invalid tunnel id for %s", name)
|
|
}
|
|
return tunnelID
|
|
}
|
|
|
|
createTunnel("dual-panel-remote-entry-online")
|
|
|
|
commandMu.Lock()
|
|
seenAddChains := false
|
|
seenCommands := append([]string(nil), entryCommands...)
|
|
for _, cmdType := range entryCommands {
|
|
if strings.EqualFold(strings.TrimSpace(cmdType), "AddChains") {
|
|
seenAddChains = true
|
|
break
|
|
}
|
|
}
|
|
commandMu.Unlock()
|
|
if !seenAddChains {
|
|
t.Fatalf("expected entry remote node to receive AddChains, commands=%v", seenCommands)
|
|
}
|
|
|
|
stopEntry()
|
|
waitNodeStatus(t, providerRepo, providerEntryNodeID, 0)
|
|
|
|
createTunnel("dual-panel-remote-entry-offline")
|
|
}
|
|
|
|
func insertContractNode(t *testing.T, r *repo.Repository, name, ip, portRange, secret string, status int) int64 {
|
|
t.Helper()
|
|
now := time.Now().UnixMilli()
|
|
if err := r.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(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, name, secret, ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, status, "[::]", "[::]", 0).Error; err != nil {
|
|
t.Fatalf("insert node %s: %v", name, err)
|
|
}
|
|
return mustLastInsertID(t, r, name)
|
|
}
|
|
|
|
func insertPeerShare(t *testing.T, r *repo.Repository, share *repo.PeerShare) int64 {
|
|
t.Helper()
|
|
if share == nil {
|
|
t.Fatalf("share is nil")
|
|
}
|
|
if err := r.CreatePeerShare(share); err != nil {
|
|
t.Fatalf("create peer share %s: %v", share.Name, err)
|
|
}
|
|
saved, err := r.GetPeerShareByToken(share.Token)
|
|
if err != nil {
|
|
t.Fatalf("query peer share %s: %v", share.Name, err)
|
|
}
|
|
if saved == nil {
|
|
t.Fatalf("peer share %s not found after create", share.Name)
|
|
}
|
|
return saved.ID
|
|
}
|
|
|
|
func importRemoteNodeForContract(t *testing.T, router http.Handler, adminToken, remoteURL, token string) {
|
|
t.Helper()
|
|
body, err := json.Marshal(map[string]string{
|
|
"remoteUrl": remoteURL,
|
|
"token": token,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal import payload: %v", err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/federation/node/import", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", adminToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
router.ServeHTTP(res, req)
|
|
assertCode(t, res, 0)
|
|
}
|
|
|
|
func queryRemoteNodeIDByToken(t *testing.T, r *repo.Repository, token string) int64 {
|
|
t.Helper()
|
|
id := mustQueryInt64(t, r, `SELECT id FROM node WHERE is_remote = 1 AND remote_token = ? ORDER BY id DESC LIMIT 1`, token)
|
|
if id <= 0 {
|
|
t.Fatalf("invalid remote node id for token %s", token)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func assertTunnelPortInRange(t *testing.T, r *repo.Repository, tunnelID int64, chainType int, nodeID int64, minPort int, maxPort int) {
|
|
t.Helper()
|
|
port := mustQueryInt(t, r, `SELECT port FROM chain_tunnel WHERE tunnel_id = ? AND chain_type = ? AND node_id = ? LIMIT 1`, tunnelID, chainType, nodeID)
|
|
if port < minPort || port > maxPort {
|
|
t.Fatalf("expected port in range [%d,%d], got %d", minPort, maxPort, port)
|
|
}
|
|
}
|
|
|
|
func assertCount(t *testing.T, r *repo.Repository, query string, arg interface{}, expected int) {
|
|
t.Helper()
|
|
got := mustQueryInt(t, r, query, arg)
|
|
if got != expected {
|
|
t.Fatalf("expected count %d, got %d (query: %s, arg: %v)", expected, got, query, arg)
|
|
}
|
|
}
|
|
|
|
func startMockNodeSession(t *testing.T, baseURL string, nodeSecret string) func() {
|
|
return startMockNodeSessionWithHook(t, baseURL, nodeSecret, nil)
|
|
}
|
|
|
|
func startMockNodeSessionWithHook(t *testing.T, baseURL string, nodeSecret string, onCommand func(cmdType string)) func() {
|
|
t.Helper()
|
|
u, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
t.Fatalf("parse provider url: %v", err)
|
|
}
|
|
if strings.EqualFold(u.Scheme, "https") {
|
|
u.Scheme = "wss"
|
|
} else {
|
|
u.Scheme = "ws"
|
|
}
|
|
u.Path = "/system-info"
|
|
q := u.Query()
|
|
q.Set("type", "1")
|
|
q.Set("secret", nodeSecret)
|
|
q.Set("version", "v1")
|
|
q.Set("http", "1")
|
|
q.Set("tls", "1")
|
|
q.Set("socks", "1")
|
|
u.RawQuery = q.Encode()
|
|
|
|
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
|
if err != nil {
|
|
t.Fatalf("dial mock node websocket: %v", err)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for {
|
|
_, raw, readErr := conn.ReadMessage()
|
|
if readErr != nil {
|
|
return
|
|
}
|
|
|
|
plain := raw
|
|
var wrap struct {
|
|
Encrypted bool `json:"encrypted"`
|
|
Data string `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(raw, &wrap); err == nil && wrap.Encrypted && strings.TrimSpace(wrap.Data) != "" {
|
|
crypto, cryptoErr := security.NewAESCrypto(nodeSecret)
|
|
if cryptoErr == nil {
|
|
if dec, decErr := crypto.Decrypt(wrap.Data); decErr == nil {
|
|
plain = []byte(dec)
|
|
}
|
|
}
|
|
}
|
|
|
|
var cmd struct {
|
|
Type string `json:"type"`
|
|
RequestID string `json:"requestId"`
|
|
}
|
|
if err := json.Unmarshal(plain, &cmd); err != nil {
|
|
continue
|
|
}
|
|
if strings.TrimSpace(cmd.RequestID) == "" {
|
|
continue
|
|
}
|
|
if onCommand != nil {
|
|
onCommand(strings.TrimSpace(cmd.Type))
|
|
}
|
|
|
|
respType := fmt.Sprintf("%sResponse", cmd.Type)
|
|
respPayload := map[string]interface{}{
|
|
"type": respType,
|
|
"success": true,
|
|
"message": "OK",
|
|
"requestId": cmd.RequestID,
|
|
}
|
|
if strings.EqualFold(strings.TrimSpace(cmd.Type), "TcpPing") {
|
|
respPayload["data"] = map[string]interface{}{
|
|
"success": true,
|
|
"averageTime": 8.5,
|
|
"packetLoss": 0,
|
|
"message": "mock tcp ok",
|
|
}
|
|
}
|
|
respBytes, err := json.Marshal(respPayload)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
_ = conn.WriteMessage(websocket.TextMessage, respBytes)
|
|
}
|
|
}()
|
|
|
|
var stopOnce sync.Once
|
|
return func() {
|
|
stopOnce.Do(func() {
|
|
_ = conn.Close()
|
|
wg.Wait()
|
|
})
|
|
}
|
|
}
|
|
|
|
func waitNodeStatus(t *testing.T, r *repo.Repository, nodeID int64, expectedStatus int) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
status, err := tryQueryInt(t, r, `SELECT status FROM node WHERE id = ?`, nodeID)
|
|
if err == nil && status == expectedStatus {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("node %d status did not reach %d before timeout", nodeID, expectedStatus)
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func TestFederationRuntimeCommandPortRangeEnforcement(t *testing.T) {
|
|
providerSecret := "provider-portrange-jwt"
|
|
providerRouter, providerRepo := setupContractRouter(t, providerSecret)
|
|
providerServer := httptest.NewServer(providerRouter)
|
|
defer providerServer.Close()
|
|
|
|
now := time.Now().UnixMilli()
|
|
providerNodeID := insertContractNode(t, providerRepo, "provider-portrange-node", "198.51.100.50", "44000-44010", "provider-portrange-secret", 1)
|
|
|
|
insertPeerShare(t, providerRepo, &repo.PeerShare{
|
|
Name: "portrange-share",
|
|
NodeID: providerNodeID,
|
|
Token: "share-portrange-token",
|
|
PortRangeStart: 44000,
|
|
PortRangeEnd: 44010,
|
|
IsActive: 1,
|
|
CreatedTime: now,
|
|
UpdatedTime: now,
|
|
})
|
|
|
|
stopNode := startMockNodeSession(t, providerServer.URL, "provider-portrange-secret")
|
|
defer stopNode()
|
|
|
|
sendCommand := func(token string, cmdType string, data interface{}) *httptest.ResponseRecorder {
|
|
payload := map[string]interface{}{
|
|
"commandType": cmdType,
|
|
"data": data,
|
|
}
|
|
body, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal command payload: %v", err)
|
|
}
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/federation/runtime/command", bytes.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
res := httptest.NewRecorder()
|
|
providerRouter.ServeHTTP(res, req)
|
|
return res
|
|
}
|
|
|
|
// Test: AddService with port OUTSIDE allowed range should be rejected
|
|
outOfRangeData := map[string]interface{}{
|
|
"services": []map[string]interface{}{
|
|
{
|
|
"name": "test_service_tcp",
|
|
"addr": "[::]:55555",
|
|
"handler": map[string]interface{}{
|
|
"type": "tcp",
|
|
},
|
|
"listener": map[string]interface{}{
|
|
"type": "tcp",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
res := sendCommand("share-portrange-token", "AddService", outOfRangeData)
|
|
var out response.R
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 403 {
|
|
t.Fatalf("expected code 403 for out-of-range port, got %d (msg: %s)", out.Code, out.Msg)
|
|
}
|
|
|
|
// Test: UpdateService with port OUTSIDE allowed range should be rejected
|
|
res = sendCommand("share-portrange-token", "UpdateService", outOfRangeData)
|
|
out = response.R{}
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 403 {
|
|
t.Fatalf("expected code 403 for out-of-range UpdateService, got %d (msg: %s)", out.Code, out.Msg)
|
|
}
|
|
|
|
// Test: AddService with port INSIDE allowed range should succeed
|
|
inRangeData := map[string]interface{}{
|
|
"services": []map[string]interface{}{
|
|
{
|
|
"name": "test_service_ok_tcp",
|
|
"addr": "[::]:44005",
|
|
"handler": map[string]interface{}{
|
|
"type": "tcp",
|
|
},
|
|
"listener": map[string]interface{}{
|
|
"type": "tcp",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
res = sendCommand("share-portrange-token", "AddService", inRangeData)
|
|
out = response.R{}
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 for in-range port, got %d (msg: %s)", out.Code, out.Msg)
|
|
}
|
|
|
|
// Test: Non-service commands should pass through without port validation
|
|
res = sendCommand("share-portrange-token", "UpdateLimiters", map[string]interface{}{
|
|
"limiter": "federation-limit-test",
|
|
"data": map[string]interface{}{
|
|
"name": "federation-limit-test",
|
|
"limits": []string{"$ 1MB 1MB"},
|
|
},
|
|
})
|
|
out = response.R{}
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 for UpdateLimiters command, got %d (msg: %s)", out.Code, out.Msg)
|
|
}
|
|
|
|
res = sendCommand("share-portrange-token", "reload", nil)
|
|
out = response.R{}
|
|
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if out.Code != 0 {
|
|
t.Fatalf("expected code 0 for reload command, got %d (msg: %s)", out.Code, out.Msg)
|
|
}
|
|
}
|