hysteria_geco_uri_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. package sub
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. "github.com/mhsanaei/3x-ui/v3/internal/util/link"
  8. )
  9. // A salamander mask carrying packetSize (Gecko mode) must export the
  10. // v2rayN-native gecko URI fields, not an fm=<json> dump.
  11. func TestGenHysteriaLinkEmitsGeckoParamsForPacketSize(t *testing.T) {
  12. in := &model.Inbound{
  13. Id: 920001, Listen: "203.0.113.1", Port: 443, Protocol: model.Hysteria,
  14. Settings: `{"version":2,"clients":[{"auth":"secret","email":"user"}]}`,
  15. StreamSettings: `{"security":"tls","finalmask":{"udp":[{"type":"salamander","settings":` +
  16. `{"password":"pw","packetSize":"512-1200"}}]}}`,
  17. }
  18. got := (&SubService{}).genHysteriaLink(in, "user")
  19. for _, want := range []string{"obfs=gecko", "obfs-password=pw", "minPacketSize=512", "maxPacketSize=1200"} {
  20. if !strings.Contains(got, want) {
  21. t.Fatalf("missing %q\n got: %s", want, got)
  22. }
  23. }
  24. if strings.Contains(got, "obfs=salamander") {
  25. t.Fatalf("gecko mask exported as plain salamander:\n %s", got)
  26. }
  27. if strings.Contains(got, "fm=") {
  28. t.Fatalf("expressed salamander mask must not leak into fm= dump:\n %s", got)
  29. }
  30. }
  31. // Password-only masks keep the plain salamander export.
  32. func TestGenHysteriaLinkSalamanderWithoutPacketSizeUnchanged(t *testing.T) {
  33. in := &model.Inbound{
  34. Id: 920002, Listen: "203.0.113.1", Port: 443, Protocol: model.Hysteria,
  35. Settings: `{"version":2,"clients":[{"auth":"secret","email":"user"}]}`,
  36. StreamSettings: `{"security":"tls","finalmask":{"udp":[{"type":"salamander","settings":{"password":"pw"}}]}}`,
  37. }
  38. got := (&SubService{}).genHysteriaLink(in, "user")
  39. if !strings.Contains(got, "obfs=salamander") || !strings.Contains(got, "obfs-password=pw") {
  40. t.Fatalf("password-only mask lost its standard export:\n %s", got)
  41. }
  42. for _, bad := range []string{"minPacketSize=", "maxPacketSize="} {
  43. if strings.Contains(got, bad) {
  44. t.Fatalf("unexpected %s in:\n %s", bad, got)
  45. }
  46. }
  47. }
  48. // Import side: obfs=gecko + min/max rebuild a standard salamander+packetSize mask.
  49. func TestParseLinkAcceptsGeckoObfs(t *testing.T) {
  50. parsed, err := link.ParseLink(
  51. "hysteria2://[email protected]:443?security=tls&obfs=gecko&obfs-password=pw&minPacketSize=512&maxPacketSize=1200#geo")
  52. if err != nil {
  53. t.Fatalf("ParseLink: %v", err)
  54. }
  55. rawStream, _ := parsed.Outbound["streamSettings"].(map[string]any)
  56. if rawStream == nil {
  57. t.Fatalf("no streamSettings in outbound: %v", parsed.Outbound)
  58. }
  59. streamJSON, err := json.Marshal(rawStream)
  60. if err != nil {
  61. t.Fatalf("marshal stream: %v", err)
  62. }
  63. var stream map[string]any
  64. if err := json.Unmarshal(streamJSON, &stream); err != nil {
  65. t.Fatalf("stream json: %v", err)
  66. }
  67. fm, _ := stream["finalmask"].(map[string]any)
  68. if fm == nil {
  69. t.Fatalf("no finalmask rebuilt: %s", streamJSON)
  70. }
  71. udp, _ := fm["udp"].([]any)
  72. var mask map[string]any
  73. for _, m := range udp {
  74. if mm, ok := m.(map[string]any); ok && mm["type"] == "salamander" {
  75. mask = mm
  76. }
  77. }
  78. if mask == nil {
  79. t.Fatalf("no salamander mask rebuilt: %s", streamJSON)
  80. }
  81. settings, _ := mask["settings"].(map[string]any)
  82. if pw, _ := settings["password"].(string); pw != "pw" {
  83. t.Fatalf("password = %v", settings["password"])
  84. }
  85. if ps, _ := settings["packetSize"].(string); ps != "512-1200" {
  86. t.Fatalf("packetSize = %v, want 512-1200", settings["packetSize"])
  87. }
  88. }
  89. // Half-specified or out-of-bounds gecko ranges must be dropped, not stored.
  90. func TestParseLinkRejectsInvalidGeckoPacketSize(t *testing.T) {
  91. cases := map[string]string{
  92. "half min only": "hysteria2://[email protected]:443?security=tls&obfs=gecko&obfs-password=pw&minPacketSize=512#geo",
  93. "half max only": "hysteria2://[email protected]:443?security=tls&obfs=gecko&obfs-password=pw&maxPacketSize=1200#geo",
  94. "non-numeric": "hysteria2://[email protected]:443?security=tls&obfs=gecko&obfs-password=pw&minPacketSize=abc&maxPacketSize=def#geo",
  95. "zero min": "hysteria2://[email protected]:443?security=tls&obfs=gecko&obfs-password=pw&minPacketSize=0&maxPacketSize=1200#geo",
  96. "inverted": "hysteria2://[email protected]:443?security=tls&obfs=gecko&obfs-password=pw&minPacketSize=1200&maxPacketSize=512#geo",
  97. "over cap": "hysteria2://[email protected]:443?security=tls&obfs=gecko&obfs-password=pw&minPacketSize=512&maxPacketSize=4096#geo",
  98. }
  99. for name, uri := range cases {
  100. t.Run(name, func(t *testing.T) {
  101. parsed, err := link.ParseLink(uri)
  102. if err != nil {
  103. t.Fatalf("ParseLink: %v", err)
  104. }
  105. rawStream, _ := parsed.Outbound["streamSettings"].(map[string]any)
  106. streamJSON, _ := json.Marshal(rawStream)
  107. var stream map[string]any
  108. _ = json.Unmarshal(streamJSON, &stream)
  109. fm, _ := stream["finalmask"].(map[string]any)
  110. if fm == nil {
  111. t.Fatalf("no finalmask rebuilt: %s", streamJSON)
  112. }
  113. udp, _ := fm["udp"].([]any)
  114. for _, m := range udp {
  115. if mm, ok := m.(map[string]any); ok && mm["type"] == "salamander" {
  116. settings, _ := mm["settings"].(map[string]any)
  117. if ps, _ := settings["packetSize"].(string); ps != "" {
  118. t.Fatalf("invalid gecko stored packetSize %q", ps)
  119. }
  120. }
  121. }
  122. })
  123. }
  124. }
  125. // Export side must mirror the TS bounds exactly (1 <= min <= max <= 2048).
  126. func TestParseHysteriaPacketSizeBounds(t *testing.T) {
  127. if got := parseHysteriaPacketSize("0-1200"); got != "" {
  128. t.Fatalf("min below 1 accepted: %q", got)
  129. }
  130. if got := parseHysteriaPacketSize("1200-512"); got != "" {
  131. t.Fatalf("inverted range accepted: %q", got)
  132. }
  133. if got := parseHysteriaPacketSize("512-4096"); got != "" {
  134. t.Fatalf("range over xray cap accepted: %q", got)
  135. }
  136. if got := parseHysteriaPacketSize(" 512 - 1200 "); got != "" {
  137. t.Fatalf("padded range must be rejected: %q", got)
  138. }
  139. if got := parseHysteriaPacketSize("+512-1200"); got != "" {
  140. t.Fatalf("plus-prefixed range must be rejected: %q", got)
  141. }
  142. if got := parseHysteriaPacketSize("512-1200"); got != "512-1200" {
  143. t.Fatalf("valid range = %q", got)
  144. }
  145. }