client_update_allowedips_test.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  5. )
  6. // seedDualProtocolClient creates a WireGuard inbound and an AmneziaWG inbound
  7. // (real, distinct subnets: 10.0.0.0/24 and 10.8.1.0/24), attaches the same
  8. // email to both with its own correct, protocol-appropriate address, and
  9. // returns the two inbounds plus the shared client record id.
  10. func seedDualProtocolClient(t *testing.T, email, wgAddr, awgAddr string) (wgIb, awgIb *model.Inbound, recordId int) {
  11. t.Helper()
  12. svc := &ClientService{}
  13. wgClient := model.Client{Email: email, SubID: "sub-" + email, Enable: true, AllowedIPs: []string{wgAddr}}
  14. wgIb = mkInbound(t, 51820, model.WireGuard, clientsSettings(t, []model.Client{wgClient}))
  15. if err := svc.SyncInbound(nil, wgIb.Id, []model.Client{wgClient}); err != nil {
  16. t.Fatalf("seed wg linkage: %v", err)
  17. }
  18. awgClient := model.Client{Email: email, SubID: "sub-" + email, Enable: true, AllowedIPs: []string{awgAddr}}
  19. awgIb = mkInbound(t, 443, model.AmneziaWG, clientsSettings(t, []model.Client{awgClient}))
  20. if err := svc.SyncInbound(nil, awgIb.Id, []model.Client{awgClient}); err != nil {
  21. t.Fatalf("seed awg linkage: %v", err)
  22. }
  23. recordId = lookupClientRecord(t, email).Id
  24. return wgIb, awgIb, recordId
  25. }
  26. func inboundAllowedIPs(t *testing.T, inboundSvc *InboundService, ibId int, email string) []string {
  27. t.Helper()
  28. ib, err := inboundSvc.GetInbound(ibId)
  29. if err != nil {
  30. t.Fatalf("GetInbound %d: %v", ibId, err)
  31. }
  32. clients, err := inboundSvc.GetClients(ib)
  33. if err != nil {
  34. t.Fatalf("GetClients %d: %v", ibId, err)
  35. }
  36. for i := range clients {
  37. if clients[i].Email == email {
  38. return clients[i].AllowedIPs
  39. }
  40. }
  41. t.Fatalf("email %q not found on inbound %d", email, ibId)
  42. return nil
  43. }
  44. // TestUpdateBroadcastAllowedIPsDoesNotOverwriteOtherInboundWhenMismatched is a
  45. // regression test for the same bug class already fixed for Attach
  46. // (addressesFitAmneziaWGInbound), but on the far more common Update path: the
  47. // edit-client form sends one shared AllowedIPs value, and Update's per-inbound
  48. // loop used to broadcast it verbatim to every attached inbound, including one
  49. // it doesn't belong to. A client attached to both wg (10.0.0.5/32) and awg
  50. // (10.8.1.5/32) saving with the wg-labeled value as the single shared field
  51. // must not silently overwrite the awg inbound's own, unrelated address.
  52. func TestUpdateBroadcastAllowedIPsDoesNotOverwriteOtherInboundWhenMismatched(t *testing.T) {
  53. setupBulkDB(t)
  54. inboundSvc := &InboundService{}
  55. svc := &ClientService{}
  56. wgIb, awgIb, recId := seedDualProtocolClient(t, "dual@x", "10.0.0.5/32", "10.8.1.5/32")
  57. updated := model.Client{Email: "dual@x", Enable: true, AllowedIPs: []string{"10.0.0.5/32"}}
  58. if _, err := svc.Update(inboundSvc, recId, updated, 0); err != nil {
  59. t.Fatalf("Update: %v", err)
  60. }
  61. if got := inboundAllowedIPs(t, inboundSvc, wgIb.Id, "dual@x"); len(got) != 1 || got[0] != "10.0.0.5/32" {
  62. t.Fatalf("wg AllowedIPs = %v, want [10.0.0.5/32]", got)
  63. }
  64. if got := inboundAllowedIPs(t, inboundSvc, awgIb.Id, "dual@x"); len(got) != 1 || got[0] != "10.8.1.5/32" {
  65. t.Fatalf("the real bug: awg AllowedIPs = %v, want unchanged [10.8.1.5/32] (must not inherit the wg-labeled shared value)", got)
  66. }
  67. }
  68. // TestUpdateAllowedIPsByInboundAppliesDistinctValuesPerInbound covers the new
  69. // mechanism the two-field client-edit form uses to intentionally change both
  70. // addresses in one save: distinct, valid, per-inbound override values must
  71. // each land on their own inbound.
  72. func TestUpdateAllowedIPsByInboundAppliesDistinctValuesPerInbound(t *testing.T) {
  73. setupBulkDB(t)
  74. inboundSvc := &InboundService{}
  75. svc := &ClientService{}
  76. wgIb, awgIb, recId := seedDualProtocolClient(t, "dual@x", "10.0.0.5/32", "10.8.1.5/32")
  77. updated := model.Client{
  78. Email: "dual@x",
  79. Enable: true,
  80. AllowedIPsByInbound: map[int][]string{
  81. wgIb.Id: {"10.0.0.9/32"},
  82. awgIb.Id: {"10.8.1.9/32"},
  83. },
  84. }
  85. if _, err := svc.Update(inboundSvc, recId, updated, 0); err != nil {
  86. t.Fatalf("Update: %v", err)
  87. }
  88. if got := inboundAllowedIPs(t, inboundSvc, wgIb.Id, "dual@x"); len(got) != 1 || got[0] != "10.0.0.9/32" {
  89. t.Fatalf("wg AllowedIPs = %v, want [10.0.0.9/32]", got)
  90. }
  91. if got := inboundAllowedIPs(t, inboundSvc, awgIb.Id, "dual@x"); len(got) != 1 || got[0] != "10.8.1.9/32" {
  92. t.Fatalf("awg AllowedIPs = %v, want [10.8.1.9/32]", got)
  93. }
  94. }
  95. // TestCreateSharedAllowedIPsThatDontFitAmneziaWGGetsFreshAllocation is
  96. // Create's counterpart to the Update regression above: adding a brand-new
  97. // client to both wg and awg inbounds at once with a single manually-typed
  98. // address must not hand the awg inbound an address from the wrong subnet --
  99. // it must fall back to auto-allocating a real, correctly-scoped address
  100. // instead, exactly as if AllowedIPs had been left empty for that inbound.
  101. func TestCreateSharedAllowedIPsThatDontFitAmneziaWGGetsFreshAllocation(t *testing.T) {
  102. setupBulkDB(t)
  103. inboundSvc := &InboundService{}
  104. svc := &ClientService{}
  105. wgIb := mkInbound(t, 51820, model.WireGuard, wgServerSettings())
  106. awgIb := mkInbound(t, 443, model.AmneziaWG, amneziawgClientTestSettings)
  107. payload := &ClientCreatePayload{
  108. Client: model.Client{Email: "new@x", Enable: true, AllowedIPs: []string{"10.0.0.7/32"}},
  109. InboundIds: []int{wgIb.Id, awgIb.Id},
  110. }
  111. if _, err := svc.Create(inboundSvc, payload); err != nil {
  112. t.Fatalf("Create: %v", err)
  113. }
  114. if got := inboundAllowedIPs(t, inboundSvc, wgIb.Id, "new@x"); len(got) != 1 || got[0] != "10.0.0.7/32" {
  115. t.Fatalf("wg AllowedIPs = %v, want [10.0.0.7/32]", got)
  116. }
  117. got := inboundAllowedIPs(t, inboundSvc, awgIb.Id, "new@x")
  118. if len(got) != 1 {
  119. t.Fatalf("awg AllowedIPs = %v, want exactly one freshly allocated address", got)
  120. }
  121. if got[0] == "10.0.0.7/32" {
  122. t.Fatal("the real bug: awg inbound inherited the wg-shaped shared address instead of allocating its own")
  123. }
  124. if !addressesFitAmneziaWGInbound(got, awgIb) {
  125. t.Fatalf("freshly allocated awg address %v does not actually fit the awg inbound's own subnet", got)
  126. }
  127. }
  128. // TestCreateAllowedIPsByInboundAppliesDistinctValuesPerInbound is Create's
  129. // counterpart to the Update explicit-override test: the add-client form,
  130. // when attaching to both wg and awg at once with the two-field UI, must be
  131. // able to give each inbound its own manually chosen address in one call.
  132. func TestCreateAllowedIPsByInboundAppliesDistinctValuesPerInbound(t *testing.T) {
  133. setupBulkDB(t)
  134. inboundSvc := &InboundService{}
  135. svc := &ClientService{}
  136. wgIb := mkInbound(t, 51820, model.WireGuard, wgServerSettings())
  137. awgIb := mkInbound(t, 443, model.AmneziaWG, amneziawgClientTestSettings)
  138. payload := &ClientCreatePayload{
  139. Client: model.Client{
  140. Email: "new@x",
  141. Enable: true,
  142. AllowedIPsByInbound: map[int][]string{
  143. wgIb.Id: {"10.0.0.9/32"},
  144. awgIb.Id: {"10.8.1.9/32"},
  145. },
  146. },
  147. InboundIds: []int{wgIb.Id, awgIb.Id},
  148. }
  149. if _, err := svc.Create(inboundSvc, payload); err != nil {
  150. t.Fatalf("Create: %v", err)
  151. }
  152. if got := inboundAllowedIPs(t, inboundSvc, wgIb.Id, "new@x"); len(got) != 1 || got[0] != "10.0.0.9/32" {
  153. t.Fatalf("wg AllowedIPs = %v, want [10.0.0.9/32]", got)
  154. }
  155. if got := inboundAllowedIPs(t, inboundSvc, awgIb.Id, "new@x"); len(got) != 1 || got[0] != "10.8.1.9/32" {
  156. t.Fatalf("awg AllowedIPs = %v, want [10.8.1.9/32]", got)
  157. }
  158. }
  159. // TestTunnelAllowedIPsByInbound covers the GET-client read side: a two-field
  160. // display needs the real, distinct per-inbound address for each protocol,
  161. // which ClientRecord's own single AllowedIPs column cannot represent.
  162. func TestTunnelAllowedIPsByInbound(t *testing.T) {
  163. setupBulkDB(t)
  164. inboundSvc := &InboundService{}
  165. svc := &ClientService{}
  166. wgIb, awgIb, _ := seedDualProtocolClient(t, "dual@x", "10.0.0.5/32", "10.8.1.5/32")
  167. vlessIb := mkInbound(t, 8443, model.VLESS, clientsSettings(t, nil))
  168. got, err := svc.TunnelAllowedIPsByInbound(inboundSvc, "dual@x", []int{wgIb.Id, awgIb.Id, vlessIb.Id, 999999})
  169. if err != nil {
  170. t.Fatalf("TunnelAllowedIPsByInbound: %v", err)
  171. }
  172. if len(got) != 2 {
  173. t.Fatalf("result = %v, want exactly 2 entries (vless and the nonexistent id must be skipped)", got)
  174. }
  175. if got[wgIb.Id] != "10.0.0.5/32" {
  176. t.Fatalf("wg entry = %q, want 10.0.0.5/32", got[wgIb.Id])
  177. }
  178. if got[awgIb.Id] != "10.8.1.5/32" {
  179. t.Fatalf("awg entry = %q, want 10.8.1.5/32", got[awgIb.Id])
  180. }
  181. if _, ok := got[vlessIb.Id]; ok {
  182. t.Fatalf("a non-tunnel (VLESS) inbound must not appear in the result")
  183. }
  184. if _, ok := got[999999]; ok {
  185. t.Fatalf("a nonexistent inbound id must not appear in the result")
  186. }
  187. }