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.
118 lines
4.6 KiB
Go
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])
|
|
}
|
|
}
|