Files
3x-ui-agent/internal/sub/links_host_test.go
T
Muse 3413ccca9f fix three V1.0.1 leftover tails
1. per-client links (LinkProvider.LinksForClient) now apply the
   inbound's managed Host records with the same precedence as
   subscriptions and export-all (host endpoints after fallback
   projection); zero-host inbounds keep the legacy output.
2. agent node writes no longer store the form placeholder
   address='agent', port=2053: CreateFromRequest/UpdateFromRequest
   sanitize it to the node's resolved agent address (update) or
   empty (create / unreported), port 0; normalize gains a lenient
   endpoint mode for agent writes only.
3. runtime Remote.baseURL refuses the legacy 'agent' placeholder
   with a clear no-panel-endpoint error instead of splicing it into
   a dialable https://agent:2053 URL.
2026-10-22 17:55:19 +00:00

118 lines
4.6 KiB
Go

package sub
import (
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
// Per-client links (GET /panel/api/clients/links/:email, served by
// LinksForClient) must apply an inbound's managed Host records with the
// same precedence as subscriptions (GetSubs) and export-all
// (inboundLinks): after the fallback projection the host endpoint wins.
// Before this coverage existed the per-client endpoint skipped hosts
// entirely, for traditional and agent inbounds alike.
const linksHostStream = `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni"}}`
func TestLinksForClient_AppliesHostRecords(t *testing.T) {
seedSubDB(t)
ib := seedSubInbound(t, "s1", "h", 4433, 1, linksHostStream)
seedHost(t, &model.Host{InboundId: ib.Id, Remark: "CDN", Address: "cdn.example.com", Port: 8443, Security: "tls", Sni: "cdn.sni"})
links := NewLinkProvider().LinksForClient("req.example.com", ib, "h@e")
if len(links) != 1 {
t.Fatalf("links = %d, want 1: %v", len(links), links)
}
if !strings.Contains(links[0], "cdn.example.com:8443") || !strings.Contains(links[0], "sni=cdn.sni") {
t.Fatalf("per-client link must carry the host endpoint: %s", links[0])
}
if strings.Contains(links[0], "203.0.113.5:4433") {
t.Fatalf("per-client link leaked the plain inbound endpoint despite a host record: %s", links[0])
}
}
func TestLinksForClient_MultipleHostsOrdered(t *testing.T) {
seedSubDB(t)
ib := seedSubInbound(t, "s1", "m", 4435, 1, linksHostStream)
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 2, Remark: "B", Address: "b.cdn.com", Port: 8443, Security: "tls"})
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "A", Address: "a.cdn.com", Port: 2096, Security: "tls"})
links := NewLinkProvider().LinksForClient("req.example.com", ib, "m@e")
if len(links) != 2 {
t.Fatalf("links = %d, want 2 (one per host): %v", len(links), links)
}
if !strings.Contains(links[0], "a.cdn.com:2096") || !strings.Contains(links[1], "b.cdn.com:8443") {
t.Fatalf("host links out of order or wrong: %v", links)
}
}
func TestLinksForClient_NoHosts_Unchanged(t *testing.T) {
seedSubDB(t)
ib := seedSubInbound(t, "s1", "z", 4436, 1, linksHostStream)
links := NewLinkProvider().LinksForClient("req.example.com", ib, "z@e")
if len(links) != 1 {
t.Fatalf("links = %d, want 1: %v", len(links), links)
}
if !strings.Contains(links[0], "203.0.113.5:4436") {
t.Fatalf("zero-host per-client link must keep the legacy endpoint: %s", links[0])
}
}
// seedAgentInbound wires a node + agent binding (telemetry IPv4) and an
// inbound hosted on that node, mirroring production agent rows.
func seedAgentInbound(t *testing.T, tag string, port int) (*model.Inbound, *model.Node) {
t.Helper()
db := database.GetDB()
node := &model.Node{Name: "agent-" + tag, Scheme: "https", Address: "agent", Port: 2053, Enable: true}
if err := db.Create(node).Error; err != nil {
t.Fatalf("create node: %v", err)
}
ag := &model.AgentNode{TokenHash: "hash-" + tag, Name: node.Name, NodeID: &node.Id, IPv4: "198.51.100.23", LastSeen: 2000}
if err := db.Create(ag).Error; err != nil {
t.Fatalf("create binding: %v", err)
}
ib := seedSubInbound(t, "s1", tag, port, 1, linksHostStream)
if err := db.Model(&model.Inbound{}).Where("id = ?", ib.Id).Update("node_id", node.Id).Error; err != nil {
t.Fatalf("attach inbound to node: %v", err)
}
ib.NodeID = &node.Id
return ib, node
}
func TestLinksForClient_AgentInbound_HostBeatsTelemetry(t *testing.T) {
seedSubDB(t)
ib, _ := seedAgentInbound(t, "ah", 4437)
seedHost(t, &model.Host{InboundId: ib.Id, Remark: "CDN", Address: "cdn.example.com", Port: 8443, Security: "tls"})
links := NewLinkProvider().LinksForClient("req.example.com", ib, "ah@e")
if len(links) != 1 {
t.Fatalf("links = %d, want 1: %v", len(links), links)
}
if !strings.Contains(links[0], "cdn.example.com:8443") {
t.Fatalf("agent per-client link must carry the host endpoint: %s", links[0])
}
if strings.Contains(links[0], "198.51.100.23") {
t.Fatalf("host record must win over the agent telemetry address: %s", links[0])
}
}
func TestLinksForClient_AgentInbound_NoHost_TelemetryPreserved(t *testing.T) {
seedSubDB(t)
ib, _ := seedAgentInbound(t, "an", 4438)
links := NewLinkProvider().LinksForClient("req.example.com", ib, "an@e")
if len(links) != 1 {
t.Fatalf("links = %d, want 1: %v", len(links), links)
}
if !strings.Contains(links[0], "198.51.100.23:4438") {
t.Fatalf("agent per-client link without hosts must keep the telemetry endpoint: %s", links[0])
}
if strings.Contains(links[0], "@agent:") {
t.Fatalf("agent per-client link leaks the placeholder address: %s", links[0])
}
}