inbound_finalmask_xdns_test.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. )
  8. // legacyXdnsStream carries the 26.9.30 xdns objects, whose "name" and typed resolvers
  9. // xray-core 26.10.10 ignores: the mask would load with no domain at all.
  10. const legacyXdnsStream = `{"network":"kcp","security":"none","kcpSettings":{"mtu":900},"finalmask":{"udp":[{"type":"xdns","settings":{"domains":[{"name":"t.example.com","types":[16],"edns0":1232}],"resolvers":[{"type":"udp","settings":{"addr":"8.8.8.8:53"}}]}}]}}`
  11. const upgradedXdnsSettings = `{"domains":[{"edns0":1232,"names":["t.example.com"],"types":[16]}],"resolvers":[{"addrs":["udp://8.8.8.8:53"]}]}`
  12. func assertXdnsSettingsUpgraded(t *testing.T, stream map[string]any) {
  13. t.Helper()
  14. finalmask, _ := stream["finalmask"].(map[string]any)
  15. udp, _ := finalmask["udp"].([]any)
  16. if len(udp) != 1 {
  17. t.Fatalf("finalmask.udp = %v, want one mask", finalmask["udp"])
  18. }
  19. mask, _ := udp[0].(map[string]any)
  20. got, err := json.Marshal(mask["settings"])
  21. if err != nil {
  22. t.Fatalf("marshal xdns settings: %v", err)
  23. }
  24. if string(got) != upgradedXdnsSettings {
  25. t.Fatalf("xdns settings\n got: %s\nwant: %s", got, upgradedXdnsSettings)
  26. }
  27. }
  28. // An API client or an older panel's export can still post the 26.9.30 shape; stored as
  29. // sent it would reach the sub links and the form in a shape the core silently empties.
  30. func TestAddInbound_StoresXdnsMaskInNamesShape(t *testing.T) {
  31. setupConflictDB(t)
  32. in := &model.Inbound{
  33. Tag: "in-45300-kcp", Enable: true, Listen: "0.0.0.0", Port: 45300, Protocol: model.VLESS,
  34. Settings: `{"clients":[],"decryption":"none"}`, StreamSettings: legacyXdnsStream,
  35. }
  36. if _, _, err := (&InboundService{}).AddInbound(in); err != nil {
  37. t.Fatalf("AddInbound: %v", err)
  38. }
  39. var stored model.Inbound
  40. if err := database.GetDB().Where("tag = ?", "in-45300-kcp").First(&stored).Error; err != nil {
  41. t.Fatalf("reload: %v", err)
  42. }
  43. var stream map[string]any
  44. if err := json.Unmarshal([]byte(stored.StreamSettings), &stream); err != nil {
  45. t.Fatalf("stored stream is not JSON: %v", err)
  46. }
  47. assertXdnsSettingsUpgraded(t, stream)
  48. }
  49. // A row that never went through the save path (restored backup, node sync, direct DB
  50. // edit) must still reach the core with its xdns domains, or the mask serves nothing.
  51. func TestGetXrayConfig_UpgradesLegacyXdnsMask(t *testing.T) {
  52. setupConflictDB(t)
  53. seedInboundConflict(t, "in-45301-kcp", "127.0.0.1", 45301, model.VLESS,
  54. legacyXdnsStream, `{"clients":[],"decryption":"none"}`)
  55. cfg, err := (&XrayService{}).GetXrayConfig()
  56. if err != nil {
  57. t.Fatalf("GetXrayConfig: %v", err)
  58. }
  59. for i := range cfg.InboundConfigs {
  60. if cfg.InboundConfigs[i].Tag != "in-45301-kcp" {
  61. continue
  62. }
  63. raw, err := json.Marshal(cfg.InboundConfigs[i])
  64. if err != nil {
  65. t.Fatalf("marshal emitted inbound: %v", err)
  66. }
  67. var emitted map[string]any
  68. if err := json.Unmarshal(raw, &emitted); err != nil {
  69. t.Fatalf("decode emitted inbound: %v", err)
  70. }
  71. stream, _ := emitted["streamSettings"].(map[string]any)
  72. assertXdnsSettingsUpgraded(t, stream)
  73. assertXrayAccepts(t, "the healed xdns inbound", buildGoldenInbound(t, emitted))
  74. return
  75. }
  76. t.Fatal("inbound in-45301-kcp not found in the generated config")
  77. }
  78. // A template pasted from a 26.9.30 panel after the seeder ran passes the save check,
  79. // since the core loads that shape without error; only the build-time heal catches it.
  80. func TestGetXrayConfig_UpgradesTemplateOutboundXdnsMask(t *testing.T) {
  81. setupConflictDB(t)
  82. template := `{"outbounds":[{"protocol":"freedom","tag":"direct"},{"protocol":"vless","tag":"dns-tunnel",
  83. "settings":{"vnext":[{"address":"t.example.com","port":53,"users":[{"id":"c9f0c2d0-0000-4000-8000-000000000000","encryption":"none"}]}]},
  84. "streamSettings":` + legacyXdnsStream + `}]}`
  85. if err := (&SettingService{}).saveSetting("xrayTemplateConfig", template); err != nil {
  86. t.Fatalf("seed template: %v", err)
  87. }
  88. cfg, err := (&XrayService{}).GetXrayConfig()
  89. if err != nil {
  90. t.Fatalf("GetXrayConfig: %v", err)
  91. }
  92. var outbounds []map[string]any
  93. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  94. t.Fatalf("decode emitted outbounds: %v", err)
  95. }
  96. for _, outbound := range outbounds {
  97. if outbound["tag"] == "dns-tunnel" {
  98. stream, _ := outbound["streamSettings"].(map[string]any)
  99. assertXdnsSettingsUpgraded(t, stream)
  100. return
  101. }
  102. }
  103. t.Fatal("outbound dns-tunnel not found in the generated config")
  104. }
  105. // A hysteria outbound pasted from a pre-26.9.9 template still carries quicParams.udpHop,
  106. // which the core now ignores; the build heal hands it the udphop mask instead.
  107. func TestGetXrayConfig_UpgradesTemplateOutboundUDPHop(t *testing.T) {
  108. setupConflictDB(t)
  109. template := `{"outbounds":[{"protocol":"freedom","tag":"direct"},{"protocol":"hysteria","tag":"hy-out",
  110. "settings":{"version":2,"address":"hy.example.com","port":443},
  111. "streamSettings":{"network":"hysteria","security":"tls","hysteriaSettings":{"version":2,"auth":"a"},
  112. "finalmask":{"quicParams":{"udpHop":{"ports":"20000-30000","interval":"5-10"}}}}}]}`
  113. if err := (&SettingService{}).saveSetting("xrayTemplateConfig", template); err != nil {
  114. t.Fatalf("seed template: %v", err)
  115. }
  116. cfg, err := (&XrayService{}).GetXrayConfig()
  117. if err != nil {
  118. t.Fatalf("GetXrayConfig: %v", err)
  119. }
  120. var outbounds []map[string]any
  121. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  122. t.Fatalf("decode emitted outbounds: %v", err)
  123. }
  124. for _, outbound := range outbounds {
  125. if outbound["tag"] != "hy-out" {
  126. continue
  127. }
  128. stream, _ := outbound["streamSettings"].(map[string]any)
  129. got, _ := json.Marshal(stream["finalmask"])
  130. const want = `{"udp":[{"settings":{"interval":"5-10","mode":"intervalRemote","remotePorts":"20000-30000"},"type":"udphop"}]}`
  131. if string(got) != want {
  132. t.Fatalf("emitted finalmask\n got: %s\nwant: %s", got, want)
  133. }
  134. return
  135. }
  136. t.Fatal("outbound hy-out not found in the generated config")
  137. }