inbound_finalmask_xmc_test.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/xtls/xray-core/infra/conf"
  6. )
  7. const completeXmcProfile = `{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`
  8. func TestValidateFinalMaskXmcProfiles(t *testing.T) {
  9. tests := []struct {
  10. name string
  11. streamSettings string
  12. wantErr bool
  13. }{
  14. {
  15. name: "empty streamSettings",
  16. streamSettings: "",
  17. wantErr: false,
  18. },
  19. {
  20. name: "no finalmask",
  21. streamSettings: `{"network":"tcp","security":"none"}`,
  22. wantErr: false,
  23. },
  24. {
  25. name: "non-xmc mask is untouched",
  26. streamSettings: `{"finalmask":{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}}`,
  27. wantErr: false,
  28. },
  29. {
  30. name: "xmc with a complete profile",
  31. streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"hostname":"mc.example.com","password":"pw","profiles":[` + completeXmcProfile + `]}}]}}`,
  32. wantErr: false,
  33. },
  34. {
  35. name: "legacy usernames shape without profiles",
  36. streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"hostname":"mc.example.com","password":"pw","usernames":["Dream"]}}]}}`,
  37. wantErr: true,
  38. },
  39. {
  40. name: "xmc with an empty profiles array",
  41. streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[]}}]}}`,
  42. wantErr: true,
  43. },
  44. {
  45. name: "profile missing the textures signature",
  46. streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":""}]}}]}}`,
  47. wantErr: true,
  48. },
  49. {
  50. name: "profile with an unparseable uuid",
  51. streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[{"username":"Notch","uuid":"not-a-uuid","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}]}}]}}`,
  52. wantErr: true,
  53. },
  54. {
  55. name: "profile with an out-of-range username",
  56. streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[{"username":"ab","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}]}}]}}`,
  57. wantErr: true,
  58. },
  59. {
  60. name: "one complete and one incomplete profile",
  61. streamSettings: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[` + completeXmcProfile + `,{"username":"Herobrine","uuid":"","texturesValue":"","texturesSignature":""}]}}]}}`,
  62. wantErr: true,
  63. },
  64. }
  65. for _, tt := range tests {
  66. t.Run(tt.name, func(t *testing.T) {
  67. err := validateFinalMaskXmcProfiles(tt.streamSettings)
  68. if (err != nil) != tt.wantErr {
  69. t.Errorf("validateFinalMaskXmcProfiles(%q) error = %v, wantErr %v", tt.streamSettings, err, tt.wantErr)
  70. }
  71. })
  72. }
  73. }
  74. // TestXmcMaskProfilesCompleteMatchesCoreValidation pins the panel's predicate
  75. // to xray-core's own loader instead of a restatement of it: every profile the
  76. // panel accepts must build, and every one it rejects must fail to build. A
  77. // future core release that tightens or relaxes the rules fails here rather
  78. // than silently producing configs the core refuses to start on.
  79. func TestXmcMaskProfilesCompleteMatchesCoreValidation(t *testing.T) {
  80. profiles := []struct {
  81. name string
  82. raw string
  83. }{
  84. {name: "complete", raw: completeXmcProfile},
  85. {name: "undashed uuid", raw: `{"username":"Notch","uuid":"069a79f444e94726a5befca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
  86. {name: "username at the 16 char limit", raw: `{"username":"Abcdefghijklmnop","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
  87. {name: "username over the limit", raw: `{"username":"Abcdefghijklmnopq","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
  88. {name: "username with a hyphen", raw: `{"username":"No-tch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
  89. {name: "empty uuid", raw: `{"username":"Notch","uuid":"","texturesValue":"dmFsdWU=","texturesSignature":"c2ln"}`},
  90. {name: "missing textures value", raw: `{"username":"Notch","uuid":"069a79f4-44e9-4726-a5be-fca90e38aaf5","texturesValue":"","texturesSignature":"c2ln"}`},
  91. }
  92. for _, tt := range profiles {
  93. t.Run(tt.name, func(t *testing.T) {
  94. var coreProfile conf.XMCProfile
  95. if err := json.Unmarshal([]byte(tt.raw), &coreProfile); err != nil {
  96. t.Fatalf("unmarshal into conf.XMCProfile: %v", err)
  97. }
  98. _, coreErr := coreProfile.Build()
  99. mask := map[string]any{"type": "xmc"}
  100. var settings map[string]any
  101. if err := json.Unmarshal([]byte(`{"profiles":[`+tt.raw+`]}`), &settings); err != nil {
  102. t.Fatalf("unmarshal settings: %v", err)
  103. }
  104. mask["settings"] = settings
  105. panelAccepts := xmcMaskProfilesComplete(mask)
  106. coreAccepts := coreErr == nil
  107. if panelAccepts != coreAccepts {
  108. t.Errorf("xmcMaskProfilesComplete = %v, but conf.XMCProfile.Build() accepts = %v (err %v)", panelAccepts, coreAccepts, coreErr)
  109. }
  110. })
  111. }
  112. }
  113. // TestXmcEmptyProfilesRejectedByCore covers the rule that lives on XMC rather
  114. // than XMCProfile: v26.7.28 dropped the "default to Dream" fallback, so a mask
  115. // with no profiles at all is now a build failure.
  116. func TestXmcEmptyProfilesRejectedByCore(t *testing.T) {
  117. var core conf.XMC
  118. if err := json.Unmarshal([]byte(`{"hostname":"mc.example.com","password":"pw","profiles":[]}`), &core); err != nil {
  119. t.Fatalf("unmarshal into conf.XMC: %v", err)
  120. }
  121. if _, err := core.Build(); err == nil {
  122. t.Fatal("conf.XMC.Build() accepted an empty profiles list; the panel's strip/validate pair is no longer needed")
  123. }
  124. }
  125. func TestStripIncompleteXmcMasks(t *testing.T) {
  126. tests := []struct {
  127. name string
  128. stream string
  129. wantDropped int
  130. wantStream string
  131. }{
  132. {
  133. name: "legacy usernames mask is dropped and finalmask removed",
  134. stream: `{"network":"tcp","finalmask":{"tcp":[{"type":"xmc","settings":{"usernames":["Dream"],"password":"pw"}}]}}`,
  135. wantDropped: 1,
  136. wantStream: `{"network":"tcp"}`,
  137. },
  138. {
  139. name: "complete mask is kept",
  140. stream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[` + completeXmcProfile + `]}}]}}`,
  141. wantDropped: 0,
  142. wantStream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"password":"pw","profiles":[` + completeXmcProfile + `]}}]}}`,
  143. },
  144. {
  145. name: "sibling masks survive the drop",
  146. stream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{"usernames":["Dream"]}},{"type":"fragment","settings":{"packets":"tlshello"}}]}}`,
  147. wantDropped: 1,
  148. wantStream: `{"finalmask":{"tcp":[{"type":"fragment","settings":{"packets":"tlshello"}}]}}`,
  149. },
  150. {
  151. name: "udp masks are preserved when tcp empties out",
  152. stream: `{"finalmask":{"tcp":[{"type":"xmc","settings":{}}],"udp":[{"type":"salamander"}]}}`,
  153. wantDropped: 1,
  154. wantStream: `{"finalmask":{"udp":[{"type":"salamander"}]}}`,
  155. },
  156. {
  157. name: "stream without finalmask is untouched",
  158. stream: `{"network":"tcp","security":"tls"}`,
  159. wantDropped: 0,
  160. wantStream: `{"network":"tcp","security":"tls"}`,
  161. },
  162. }
  163. for _, tt := range tests {
  164. t.Run(tt.name, func(t *testing.T) {
  165. var stream map[string]any
  166. if err := json.Unmarshal([]byte(tt.stream), &stream); err != nil {
  167. t.Fatalf("unmarshal stream: %v", err)
  168. }
  169. dropped := stripIncompleteXmcMasks(stream)
  170. if dropped != tt.wantDropped {
  171. t.Errorf("stripIncompleteXmcMasks dropped = %d, want %d", dropped, tt.wantDropped)
  172. }
  173. var want map[string]any
  174. if err := json.Unmarshal([]byte(tt.wantStream), &want); err != nil {
  175. t.Fatalf("unmarshal wantStream: %v", err)
  176. }
  177. got, err := json.Marshal(stream)
  178. if err != nil {
  179. t.Fatalf("marshal stream: %v", err)
  180. }
  181. wantJSON, err := json.Marshal(want)
  182. if err != nil {
  183. t.Fatalf("marshal want: %v", err)
  184. }
  185. if string(got) != string(wantJSON) {
  186. t.Errorf("stream after strip = %s, want %s", got, wantJSON)
  187. }
  188. })
  189. }
  190. }