inbound_masque_test.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. package service
  2. import (
  3. "encoding/json"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  7. )
  8. const masqueTestStream = `{"network":"masque","security":"tls","masqueSettings":{"path":"/.well-known/masque/ip/*/*/"},
  9. "tlsSettings":{"alpn":["h3"],"certificates":[{"certificateFile":"/etc/ssl/certs/m.crt","keyFile":"/etc/ssl/private/m.key"}]}}`
  10. // GetXrayConfig rebuilds every inbound's users from the clients table; a MASQUE user
  11. // emitted without its password makes xray-core refuse the whole config at startup.
  12. func TestGetXrayConfig_EmitsMasqueUserPasswords(t *testing.T) {
  13. setupConflictDB(t)
  14. in := &model.Inbound{
  15. Tag: "in-8443-masque", Enable: true, Listen: "127.0.0.1", Port: 8443, Protocol: model.MASQUE,
  16. Settings: `{"clients":[{"email":"[email protected]","password":"masque-pass-ivy","enable":true}],"address":["10.14.0.1/24"]}`,
  17. StreamSettings: masqueTestStream,
  18. }
  19. if _, _, err := (&InboundService{}).AddInbound(in); err != nil {
  20. t.Fatalf("AddInbound: %v", err)
  21. }
  22. cfg, err := (&XrayService{}).GetXrayConfig()
  23. if err != nil {
  24. t.Fatalf("GetXrayConfig: %v", err)
  25. }
  26. for i := range cfg.InboundConfigs {
  27. if cfg.InboundConfigs[i].Tag != "in-8443-masque" {
  28. continue
  29. }
  30. var settings struct {
  31. Clients []map[string]any `json:"clients"`
  32. }
  33. if err := json.Unmarshal(cfg.InboundConfigs[i].Settings, &settings); err != nil {
  34. t.Fatalf("decode emitted settings: %v", err)
  35. }
  36. if len(settings.Clients) != 1 || settings.Clients[0]["pass"] != "masque-pass-ivy" {
  37. t.Fatalf("emitted clients = %v, want one with pass masque-pass-ivy", settings.Clients)
  38. }
  39. raw, err := json.Marshal(cfg.InboundConfigs[i])
  40. if err != nil {
  41. t.Fatalf("marshal emitted inbound: %v", err)
  42. }
  43. var emitted map[string]any
  44. if err := json.Unmarshal(raw, &emitted); err != nil {
  45. t.Fatalf("decode emitted inbound: %v", err)
  46. }
  47. if stream, _ := emitted["streamSettings"].(map[string]any); stream["network"] != "masque" {
  48. t.Fatalf("emitted streamSettings = %v, want the stored masque transport", emitted["streamSettings"])
  49. }
  50. assertXrayAccepts(t, "the emitted MASQUE inbound", buildGoldenInbound(t, emitted))
  51. return
  52. }
  53. t.Fatal("inbound in-8443-masque not found in the generated config")
  54. }
  55. // MASQUE logs users in over Basic auth, so xray-core refuses the whole inbound over one
  56. // email holding ":"; the panel allows that email elsewhere and must refuse it here.
  57. func TestCreateClient_RefusesColonEmailOnMasque(t *testing.T) {
  58. setupConflictDB(t)
  59. in := &model.Inbound{
  60. Tag: "in-8443-masque", Enable: true, Listen: "127.0.0.1", Port: 8443, Protocol: model.MASQUE,
  61. Settings: `{"clients":[{"email":"[email protected]","password":"masque-pass-ivy","enable":true}],"address":["10.14.0.1/24"]}`,
  62. StreamSettings: masqueTestStream,
  63. }
  64. if _, _, err := (&InboundService{}).AddInbound(in); err != nil {
  65. t.Fatalf("AddInbound: %v", err)
  66. }
  67. _, err := (&ClientService{}).Create(&InboundService{}, &ClientCreatePayload{
  68. Client: model.Client{Email: "tg:42", Enable: true},
  69. InboundIds: []int{in.Id},
  70. })
  71. if want := `MASQUE: client email "tg:42" cannot contain ":"`; err == nil || !strings.Contains(err.Error(), want) {
  72. t.Fatalf("Create err = %v, want one containing %q", err, want)
  73. }
  74. cfg, err := (&XrayService{}).GetXrayConfig()
  75. if err != nil {
  76. t.Fatalf("GetXrayConfig: %v", err)
  77. }
  78. for i := range cfg.InboundConfigs {
  79. if cfg.InboundConfigs[i].Tag != in.Tag {
  80. continue
  81. }
  82. raw, _ := json.Marshal(cfg.InboundConfigs[i])
  83. var emitted map[string]any
  84. if err := json.Unmarshal(raw, &emitted); err != nil {
  85. t.Fatalf("decode emitted inbound: %v", err)
  86. }
  87. assertXrayAccepts(t, "the emitted MASQUE inbound", buildGoldenInbound(t, emitted))
  88. return
  89. }
  90. t.Fatalf("inbound %s not found in the generated config", in.Tag)
  91. }
  92. // A client added to a MASQUE inbound without a password could never authenticate,
  93. // and xray-core refuses the whole inbound over one empty pass.
  94. func TestFillProtocolDefaults_MintsMasquePassword(t *testing.T) {
  95. client := model.Client{Email: "[email protected]"}
  96. if err := (&ClientService{}).fillProtocolDefaults(&client, &model.Inbound{Protocol: model.MASQUE}); err != nil {
  97. t.Fatalf("fillProtocolDefaults: %v", err)
  98. }
  99. if len(client.Password) != 32 {
  100. t.Fatalf("Password = %q, want a minted 32-character password", client.Password)
  101. }
  102. kept := model.Client{Email: "[email protected]", Password: "chosen"}
  103. if err := (&ClientService{}).fillProtocolDefaults(&kept, &model.Inbound{Protocol: model.MASQUE}); err != nil {
  104. t.Fatalf("fillProtocolDefaults: %v", err)
  105. }
  106. if kept.Password != "chosen" {
  107. t.Fatalf("Password = %q, want the chosen password kept", kept.Password)
  108. }
  109. }
  110. // Editing a MASQUE client must key on its password like trojan; falling back to the
  111. // empty UUID refused every MASQUE client edit with "empty client ID".
  112. func TestUpdateInboundClient_MasquePasswordChange(t *testing.T) {
  113. setupBulkDB(t)
  114. svc := &ClientService{}
  115. source := []model.Client{{Email: "ivy@x", Password: "pw-old", SubID: "sub-ivy", Enable: true}}
  116. ib := mkInbound(t, 22101, model.MASQUE, clientsSettings(t, source))
  117. if err := svc.SyncInbound(nil, ib.Id, source); err != nil {
  118. t.Fatalf("seed linkage: %v", err)
  119. }
  120. updated := []model.Client{{Email: "ivy@x", Password: "pw-new", SubID: "sub-ivy", Enable: true}}
  121. if _, err := svc.UpdateInboundClient(&InboundService{}, &model.Inbound{
  122. Id: ib.Id,
  123. Settings: clientsSettings(t, updated),
  124. }, "ivy@x"); err != nil {
  125. t.Fatalf("UpdateInboundClient: %v", err)
  126. }
  127. if rec := lookupClientRecord(t, "ivy@x"); rec.Password != "pw-new" {
  128. t.Fatalf("stored password = %q, want pw-new", rec.Password)
  129. }
  130. }