| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- package sub
- import (
- "strings"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/mhsanaei/3x-ui/v3/internal/xray"
- )
- const dnsTestStream = `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"base.sni"}}`
- func docDnsBlock(t *testing.T, doc map[string]any) map[string]any {
- t.Helper()
- dns, _ := doc["dns"].(map[string]any)
- if dns == nil {
- t.Fatalf("doc has no dns block: %v", doc["dns"])
- }
- return dns
- }
- func onlySubJsonDoc(t *testing.T, js *SubJsonService, subId string) map[string]any {
- t.Helper()
- out, _, err := js.GetJson(subId, "req.example.com", true)
- if err != nil {
- t.Fatalf("GetJson: %v", err)
- }
- docs := parseSubJsonDocs(t, out)
- if len(docs) != 1 {
- t.Fatalf("docs = %d, want 1:\n%s", len(docs), out)
- }
- return docs[0]
- }
- // A bare servers array must replace the template resolver, not append to it.
- func TestSubJsonDns_ArrayReplacesTemplateServers(t *testing.T) {
- seedSubDB(t)
- seedSubInbound(t, "s1", "tcpin", 4901, 1, dnsTestStream)
- js := NewSubJsonService("", "", "", "", NewSubService(""))
- js.SetDnsConfig(`["https://dns.google/dns-query", {"address": "tls://1.1.1.1", "domains": ["geosite:youtube"]}]`)
- dns := docDnsBlock(t, onlySubJsonDoc(t, js, "s1"))
- servers, _ := dns["servers"].([]any)
- if len(servers) != 2 {
- t.Fatalf("servers = %v, want 2", servers)
- }
- if servers[0] != "https://dns.google/dns-query" {
- t.Fatalf("servers[0] = %v", servers[0])
- }
- second, _ := servers[1].(map[string]any)
- if second["address"] != "tls://1.1.1.1" {
- t.Fatalf("servers[1] = %v", second)
- }
- if domains, _ := second["domains"].([]any); strings.Join(stringify(domains), ",") != "geosite:youtube" {
- t.Fatalf("servers[1].domains = %v", second["domains"])
- }
- if _, hasTemplate := dns["tag"]; hasTemplate {
- t.Fatalf("template dns keys leaked into the override: %v", dns)
- }
- }
- func TestSubJsonDns_ObjectReplacesWholeBlock(t *testing.T) {
- seedSubDB(t)
- seedSubInbound(t, "s1", "tcpin", 4902, 1, dnsTestStream)
- js := NewSubJsonService("", "", "", "", NewSubService(""))
- js.SetDnsConfig(`{"tag":"panel_dns","queryStrategy":"UseIPv4","disableCache":true,"hosts":{"example.com":"1.2.3.4"},"servers":[{"address":"1.1.1.1","skipFallback":true}]}`)
- dns := docDnsBlock(t, onlySubJsonDoc(t, js, "s1"))
- if dns["tag"] != "panel_dns" || dns["queryStrategy"] != "UseIPv4" || dns["disableCache"] != true {
- t.Fatalf("dns header = %v", dns)
- }
- hosts, _ := dns["hosts"].(map[string]any)
- if hosts["example.com"] != "1.2.3.4" {
- t.Fatalf("dns hosts = %v", dns["hosts"])
- }
- servers, _ := dns["servers"].([]any)
- if len(servers) != 1 {
- t.Fatalf("servers = %v", servers)
- }
- server, _ := servers[0].(map[string]any)
- if server["address"] != "1.1.1.1" || server["skipFallback"] != true {
- t.Fatalf("server = %v", server)
- }
- }
- // The explicit panel DNS block wins over the profile's, while the profile keeps
- // owning the routing rules.
- func TestSubJsonDns_OverridesRoutingProfileDns(t *testing.T) {
- seedSubDB(t)
- seedSubInbound(t, "s1", "tcpin", 4903, 1, dnsTestStream)
- js := NewSubJsonService("", "", "", bakedRoutingPayload, NewSubService(""))
- js.SetDnsConfig(`["9.9.9.9"]`)
- doc := onlySubJsonDoc(t, js, "s1")
- dns := docDnsBlock(t, doc)
- servers, _ := dns["servers"].([]any)
- if len(servers) != 1 || servers[0] != "9.9.9.9" {
- t.Fatalf("servers = %v, want the panel override only", servers)
- }
- if hosts, _ := dns["hosts"].(map[string]any); len(hosts) != 0 {
- t.Fatalf("profile dns hosts survived the override: %v", hosts)
- }
- want := "domain->block,domain->proxy,domain->direct,ip->direct,network->proxy"
- if got := strings.Join(ruleSignatures(t, doc), ","); got != want {
- t.Fatalf("rules = %v\nwant %v", got, want)
- }
- }
- func TestSubJsonDns_InvalidSettingKeepsTemplateDns(t *testing.T) {
- cases := []struct {
- name string
- value string
- }{
- {"malformed JSON", `{"servers": [`},
- {"bare string", `"8.8.8.8"`},
- {"servers not an array", `{"servers": "8.8.8.8"}`},
- {"entry without address", `[{"domains": ["geosite:youtube"]}]`},
- {"non-string entry", `[53]`},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- seedSubDB(t)
- seedSubInbound(t, "s1", "tcpin", 4904, 1, dnsTestStream)
- js := NewSubJsonService("", "", "", "", NewSubService(""))
- js.SetDnsConfig(tc.value)
- dns := docDnsBlock(t, onlySubJsonDoc(t, js, "s1"))
- if dns["tag"] != "dns_out" || dns["queryStrategy"] != "UseIP" {
- t.Fatalf("template dns header = %v", dns)
- }
- servers, _ := dns["servers"].([]any)
- if len(servers) != 1 {
- t.Fatalf("template servers = %v", servers)
- }
- first, _ := servers[0].(map[string]any)
- if first["address"] != "8.8.8.8" {
- t.Fatalf("template server = %v", first)
- }
- })
- }
- }
- func TestSubJsonDns_BlankKeepsTemplateDns(t *testing.T) {
- seedSubDB(t)
- seedSubInbound(t, "s1", "tcpin", 4905, 1, dnsTestStream)
- js := NewSubJsonService("", "", "", "", NewSubService(""))
- js.SetDnsConfig(" ")
- dns := docDnsBlock(t, onlySubJsonDoc(t, js, "s1"))
- servers, _ := dns["servers"].([]any)
- first, _ := servers[0].(map[string]any)
- if first["address"] != "8.8.8.8" {
- t.Fatalf("template server = %v", first)
- }
- }
- // Balancer documents are built from the same template, so they carry the
- // override too.
- func TestSubJsonDns_AppliesToBalancerDocuments(t *testing.T) {
- seedSubDB(t)
- tcp := seedSubInbound(t, "s1", "tcpin", 4906, 1, dnsTestStream)
- seedSubBalancer(t, &model.SubBalancer{
- Remark: "auto", Strategy: "random", InboundIds: []int{tcp.Id}, SortOrder: 1, Enabled: true,
- })
- js := NewSubJsonService("", "", "", "", NewSubService(""))
- js.SetDnsConfig(`["https://dns.google/dns-query"]`)
- out, _, err := js.GetJson("s1", "req.example.com", true)
- if err != nil {
- t.Fatalf("GetJson: %v", err)
- }
- balancerDoc := findDocByRemarks(parseSubJsonDocs(t, out), "auto")
- if balancerDoc == nil {
- t.Fatalf("balancer doc missing:\n%s", out)
- }
- servers, _ := docDnsBlock(t, balancerDoc)["servers"].([]any)
- if len(servers) != 1 || servers[0] != "https://dns.google/dns-query" {
- t.Fatalf("balancer dns servers = %v", servers)
- }
- }
- // The validator rejects a block whose types xray cannot decode, even when the
- // servers list itself looks fine.
- func TestSubJsonDns_BrokenBlockKeepsTemplateDns(t *testing.T) {
- seedSubDB(t)
- seedSubInbound(t, "s1", "tcpin", 4907, 1, dnsTestStream)
- js := NewSubJsonService("", "", "", "", NewSubService(""))
- js.SetDnsConfig(`{"servers": ["1.1.1.1"], "hosts": 5}`)
- dns := docDnsBlock(t, onlySubJsonDoc(t, js, "s1"))
- servers, _ := dns["servers"].([]any)
- first, _ := servers[0].(map[string]any)
- if len(servers) != 1 || first["address"] != "8.8.8.8" {
- t.Fatalf("template dns = %v", dns)
- }
- }
- // An unusable routing profile degrades to an empty spec; the DNS override must
- // still reach the document.
- func TestSubJsonDns_AppliesWhenRoutingProfileUnusable(t *testing.T) {
- seedSubDB(t)
- seedSubInbound(t, "s1", "tcpin", 4908, 1, dnsTestStream)
- js := NewSubJsonService("", "", "", "not-a-routing-payload", NewSubService(""))
- js.SetDnsConfig(`["9.9.9.9"]`)
- doc := onlySubJsonDoc(t, js, "s1")
- servers, _ := docDnsBlock(t, doc)["servers"].([]any)
- if len(servers) != 1 || servers[0] != "9.9.9.9" {
- t.Fatalf("dns servers = %v", servers)
- }
- want := "network->proxy"
- if got := strings.Join(ruleSignatures(t, doc), ","); got != want {
- t.Fatalf("rules = %v, want the plain template rule %v", got, want)
- }
- }
- // The dummy info node is emitted as a document too, so it carries the panel DNS.
- func TestSubJsonDns_AppliesToInfoNodeDocument(t *testing.T) {
- setupInfoNodeTestDB(t)
- db := database.GetDB()
- ib := &model.Inbound{
- Id: 1, UserId: 1, Remark: "Germany-VLESS", Enable: true, Port: 443,
- Protocol: model.VLESS,
- Settings: `{"clients":[{"id":"c1-uuid","email":"[email protected]","subId":"sub-json","enable":true,"totalGB":10737418240}]}`,
- StreamSettings: `{"network":"tcp","security":"none"}`,
- }
- if err := db.Create(ib).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientRecord{Id: 1, Email: "[email protected]", SubID: "sub-json", UUID: "c1-uuid", Enable: true, TotalGB: 10737418240}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&model.ClientInbound{InboundId: 1, ClientId: 1}).Error; err != nil {
- t.Fatal(err)
- }
- if err := db.Create(&xray.ClientTraffic{InboundId: 1, Email: "[email protected]", Up: 1073741824, Down: 1073741824, Total: 10737418240, Enable: true}).Error; err != nil {
- t.Fatal(err)
- }
- sub := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}")
- sub.subInfoNodeEnable = true
- js := NewSubJsonService("", "", "", "", sub)
- js.SetDnsConfig(`["https://dns.google/dns-query"]`)
- out, _, err := js.GetJson("sub-json", "sub.example.com", true)
- if err != nil {
- t.Fatalf("GetJson: %v", err)
- }
- docs := parseSubJsonDocs(t, out)
- if len(docs) != 2 {
- t.Fatalf("docs = %d, want info node + inbound:\n%s", len(docs), out)
- }
- servers, _ := docDnsBlock(t, docs[0])["servers"].([]any)
- if len(servers) != 1 || servers[0] != "https://dns.google/dns-query" {
- t.Fatalf("info node dns servers = %v", servers)
- }
- }
|