1
0

endpoint_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. package sub
  2. import (
  3. "encoding/base64"
  4. "encoding/json"
  5. "fmt"
  6. "net/url"
  7. "strings"
  8. "testing"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. )
  11. // N1 — externalProxyToEndpoint maps the scalar fields and carries the source
  12. // entry so delegated TLS application reproduces the legacy presence-tracked
  13. // overrides (absent key never clobbers an upstream value).
  14. func TestExternalProxyToEndpoint(t *testing.T) {
  15. ep := map[string]any{
  16. "forceTls": "tls",
  17. "dest": "cdn.example.com",
  18. "port": float64(8443),
  19. "remark": "R",
  20. "sni": "s.example.com",
  21. }
  22. e := externalProxyToEndpoint(ep)
  23. if e.Address != "cdn.example.com" {
  24. t.Fatalf("Address = %q, want cdn.example.com", e.Address)
  25. }
  26. if e.Port != 8443 {
  27. t.Fatalf("Port = %d, want 8443", e.Port)
  28. }
  29. if e.ForceTls != "tls" {
  30. t.Fatalf("ForceTls = %q, want tls", e.ForceTls)
  31. }
  32. if e.Remark != "R" {
  33. t.Fatalf("Remark = %q, want R", e.Remark)
  34. }
  35. if e.ep == nil {
  36. t.Fatalf("ep not carried; delegated TLS application would lose the source entry")
  37. }
  38. // Delegation preserves the sni override and does not invent absent fields.
  39. params := map[string]string{}
  40. applyEndpointTLSParams(e, params, "tls")
  41. if params["sni"] != "s.example.com" {
  42. t.Fatalf("delegated sni = %q, want s.example.com", params["sni"])
  43. }
  44. if _, ok := params["fp"]; ok {
  45. t.Fatalf("absent fingerprint must not be set, got fp=%q", params["fp"])
  46. }
  47. }
  48. // N2 — inboundDefaultEndpoint reproduces the no-externalProxy default: resolved
  49. // address + inbound port, forceTls "same", empty remark, no source entry.
  50. func TestInboundDefaultEndpoint(t *testing.T) {
  51. in := &model.Inbound{Listen: "198.51.100.7", Port: 8080}
  52. s := &SubService{}
  53. e := s.inboundDefaultEndpoint(in)
  54. if e.Address != "198.51.100.7" {
  55. t.Fatalf("Address = %q, want 198.51.100.7", e.Address)
  56. }
  57. if e.Port != 8080 {
  58. t.Fatalf("Port = %d, want 8080", e.Port)
  59. }
  60. if e.ForceTls != "same" {
  61. t.Fatalf("ForceTls = %q, want same", e.ForceTls)
  62. }
  63. if e.Remark != "" {
  64. t.Fatalf("Remark = %q, want empty", e.Remark)
  65. }
  66. if e.ep != nil {
  67. t.Fatalf("default endpoint must not carry a source externalProxy entry")
  68. }
  69. }
  70. // N3 — buildEndpointLinks renders the param-form path: one link per endpoint,
  71. // TLS override applied for tls, fields stripped + security overridden for none,
  72. // joined by "\n", in order.
  73. func TestBuildEndpointLinks_ParamForm(t *testing.T) {
  74. s := &SubService{}
  75. in := &model.Inbound{Remark: "ib"}
  76. params := map[string]string{"type": "tcp", "security": "tls", "sni": "base.sni", "fp": "chrome"}
  77. eps := []ShareEndpoint{
  78. externalProxyToEndpoint(map[string]any{"forceTls": "tls", "dest": "a.example.com", "port": float64(8443), "remark": "A", "sni": "a.sni"}),
  79. externalProxyToEndpoint(map[string]any{"forceTls": "none", "dest": "b.example.com", "port": float64(80), "remark": "B"}),
  80. }
  81. got := s.buildEndpointLinks(eps, params, "tls",
  82. func(e ShareEndpoint) string { return fmt.Sprintf("vless://uid@%s", joinHostPort(e.Address, e.Port)) },
  83. func(e ShareEndpoint) string { return s.genRemark(in, "user", e.Remark, "") },
  84. )
  85. want := "vless://[email protected]:8443?fp=chrome&security=tls&sni=a.sni&type=tcp#ib-A-user\n" +
  86. "vless://[email protected]:80?security=none&type=tcp#ib-B-user"
  87. if got != want {
  88. t.Fatalf("N3 mismatch.\n got: %q\nwant: %q", got, want)
  89. }
  90. }
  91. // N4 — buildEndpointVmessLinks renders the object-form path: base obj cloned per
  92. // endpoint, add/port/tls rewritten, sni override applied, none-strip honored.
  93. func TestBuildEndpointVmessLinks(t *testing.T) {
  94. s := &SubService{}
  95. in := &model.Inbound{Remark: "ib"}
  96. baseObj := map[string]any{
  97. "v": "2", "add": "base.example.com", "port": 443, "type": "none",
  98. "id": "uid", "scy": "auto", "net": "tcp",
  99. "tls": "tls", "sni": "base.sni", "alpn": "h2", "fp": "chrome",
  100. }
  101. eps := []ShareEndpoint{
  102. externalProxyToEndpoint(map[string]any{"forceTls": "same", "dest": "a.example.com", "port": float64(8443), "remark": "A", "sni": "a.sni"}),
  103. externalProxyToEndpoint(map[string]any{"forceTls": "none", "dest": "b.example.com", "port": float64(80), "remark": "B"}),
  104. }
  105. got := s.buildEndpointVmessLinks(eps, baseObj, in, "user", "tcp")
  106. want := "vmess://ewogICJhZGQiOiAiYS5leGFtcGxlLmNvbSIsCiAgImFscG4iOiAiaDIiLAogICJmcCI6ICJjaHJvbWUiLAogICJpZCI6ICJ1aWQiLAogICJuZXQiOiAidGNwIiwKICAicG9ydCI6IDg0NDMsCiAgInBzIjogImliLUEtdXNlciIsCiAgInNjeSI6ICJhdXRvIiwKICAic25pIjogImEuc25pIiwKICAidGxzIjogInRscyIsCiAgInR5cGUiOiAibm9uZSIsCiAgInYiOiAiMiIKfQ==\n" +
  107. "vmess://ewogICJhZGQiOiAiYi5leGFtcGxlLmNvbSIsCiAgImlkIjogInVpZCIsCiAgIm5ldCI6ICJ0Y3AiLAogICJwb3J0IjogODAsCiAgInBzIjogImliLUItdXNlciIsCiAgInNjeSI6ICJhdXRvIiwKICAidGxzIjogIm5vbmUiLAogICJ0eXBlIjogIm5vbmUiLAogICJ2IjogIjIiCn0="
  108. if got != want {
  109. t.Fatalf("N4 mismatch.\n got: %q\nwant: %q", got, want)
  110. }
  111. }
  112. // happ.su documents serverDescription as a "#title?serverDescription=<base64>"
  113. // link parameter, never a key of the VMess object, so nothing may leak into it.
  114. func TestBuildEndpointVmessLinks_HostServerDescription(t *testing.T) {
  115. s := &SubService{}
  116. in := &model.Inbound{Remark: "ib"}
  117. baseObj := map[string]any{"v": "2", "add": "base.example.com", "port": 443, "type": "none", "id": "uid", "scy": "auto", "net": "tcp", "tls": "none"}
  118. host := &model.Host{Address: "a.example.com", Port: 8443, ServerDescription: "Berlin premium"}
  119. eps := []ShareEndpoint{externalProxyToEndpoint(hostToExternalProxyMap(host, "a.example.com", 8443))}
  120. got := s.buildEndpointVmessLinks(eps, baseObj, in, "user", "tcp")
  121. raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(got, "vmess://"))
  122. if err != nil {
  123. t.Fatalf("decode vmess link: %v", err)
  124. }
  125. var obj map[string]any
  126. if err := json.Unmarshal(raw, &obj); err != nil {
  127. t.Fatalf("unmarshal vmess object: %v", err)
  128. }
  129. if obj["add"] != "a.example.com" {
  130. t.Fatalf("host endpoint not applied: add = %v", obj["add"])
  131. }
  132. if value, ok := obj["serverDescription"]; ok {
  133. t.Fatalf("VMess object carries serverDescription = %v; it is not a VMess object key", value)
  134. }
  135. }
  136. // N5 — a host's Final Mask is appended to the inbound's own fm param (#5831).
  137. func TestBuildEndpointLinks_HostFinalMaskMerge(t *testing.T) {
  138. s := &SubService{}
  139. in := &model.Inbound{Remark: "ib"}
  140. params := map[string]string{"type": "tcp", "security": "tls", "fm": `{"tcp":[{"type":"sudoku"}]}`}
  141. eps := []ShareEndpoint{
  142. externalProxyToEndpoint(map[string]any{"forceTls": "same", "dest": "a.example.com", "port": float64(8443), "remark": "A", "isHost": true, "finalMask": `{"tcp":[{"type":"fragment"}]}`}),
  143. }
  144. got := s.buildEndpointLinks(eps, params, "tls",
  145. func(e ShareEndpoint) string { return fmt.Sprintf("vless://uid@%s", joinHostPort(e.Address, e.Port)) },
  146. func(e ShareEndpoint) string { return s.genRemark(in, "user", e.Remark, "") },
  147. )
  148. wantFm := "fm=" + url.QueryEscape(`{"tcp":[{"type":"sudoku"},{"type":"fragment"}]}`)
  149. if !strings.Contains(got, wantFm) {
  150. t.Fatalf("host finalMask not merged into the fm param.\n got: %q\nwant substring: %q", got, wantFm)
  151. }
  152. }
  153. // N6 — same for the VMess object form: the host mask lands in obj["fm"].
  154. func TestBuildEndpointVmessLinks_HostFinalMask(t *testing.T) {
  155. s := &SubService{}
  156. in := &model.Inbound{Remark: "ib"}
  157. baseObj := map[string]any{"v": "2", "add": "base.example.com", "port": 443, "type": "none", "id": "uid", "scy": "auto", "net": "tcp", "tls": "tls"}
  158. eps := []ShareEndpoint{
  159. externalProxyToEndpoint(map[string]any{"forceTls": "same", "dest": "a.example.com", "port": float64(8443), "remark": "A", "isHost": true, "finalMask": `{"udp":[{"type":"salamander"}]}`}),
  160. }
  161. got := s.buildEndpointVmessLinks(eps, baseObj, in, "user", "tcp")
  162. raw, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(got, "vmess://"))
  163. if err != nil {
  164. t.Fatalf("decode vmess link: %v", err)
  165. }
  166. var obj map[string]any
  167. if err := json.Unmarshal(raw, &obj); err != nil {
  168. t.Fatalf("unmarshal vmess obj: %v", err)
  169. }
  170. if fm, _ := obj["fm"].(string); fm != `{"udp":[{"type":"salamander"}]}` {
  171. t.Fatalf("vmess fm = %q, want the host mask", obj["fm"])
  172. }
  173. }