client_inbound_apply_test.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. package service
  2. import (
  3. "testing"
  4. "github.com/mhsanaei/3x-ui/v3/internal/database"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  6. )
  7. // otherTunnelAllowedIPs must see across protocols (a WireGuard inbound's
  8. // client address collides with an AmneziaWG one just as easily as two
  9. // AmneziaWG inbounds would), must exclude the inbound doing the asking, and
  10. // must ignore inbounds that aren't WireGuard/AmneziaWG entirely.
  11. func TestOtherTunnelAllowedIPs(t *testing.T) {
  12. setupConflictDB(t)
  13. seedInboundConflict(t, "wg-1", "0.0.0.0", 51820, model.WireGuard, ``, `{"clients":[{"email":"a@wg","allowedIPs":["10.0.0.5/32"]}]}`)
  14. seedInboundConflict(t, "awg-1", "0.0.0.0", 443, model.AmneziaWG, ``, `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24},"clients":[{"email":"b@awg","allowedIPs":["10.8.1.21/32"]}]}`)
  15. seedInboundConflict(t, "vless-1", "0.0.0.0", 8443, model.VLESS, `{"network":"tcp"}`, `{"clients":[{"email":"c@vless"}]}`)
  16. var wgInbound model.Inbound
  17. if err := database.GetDB().Where("tag = ?", "wg-1").First(&wgInbound).Error; err != nil {
  18. t.Fatalf("read seeded wg row: %v", err)
  19. }
  20. svc := &ClientService{}
  21. inboundSvc := &InboundService{}
  22. used, err := svc.otherTunnelAllowedIPs(database.GetDB(), inboundSvc, wgInbound.Id, nil)
  23. if err != nil {
  24. t.Fatalf("otherTunnelAllowedIPs: %v", err)
  25. }
  26. if len(used) != 1 {
  27. t.Fatalf("expected exactly one cross-inbound address (self excluded, vless ignored), got %v", used)
  28. }
  29. label, ok := used["10.8.1.21/32"]
  30. if !ok {
  31. t.Fatalf("expected the awg inbound's address to be reported as used, got %v", used)
  32. }
  33. if label == "" {
  34. t.Fatal("expected a non-empty description of which inbound holds the address")
  35. }
  36. }
  37. // TestOtherTunnelAllowedIPsExcludesSelfEmail is a regression test for a real
  38. // bug in ClientService.Attach: attaching one identity to multiple
  39. // WireGuard/AmneziaWG inbounds in the same call copies that identity's own
  40. // stored AllowedIPs into every inbound it processes (by design -- the same
  41. // person should get the same tunnel address on every protocol they use).
  42. // Attach's loop calls addInboundClient once per inbound, and each of those
  43. // calls independently computes otherTunnelAllowedIPs -- so by the second
  44. // inbound in the loop, the first inbound's now-successful copy of the
  45. // identity's own address looked like a cross-inbound collision against
  46. // itself, and the attach failed with exactly the error a real user hit:
  47. // "wireguard: allowedIPs entry 10.8.1.21/32 is already used by a client on
  48. // inbound 'awg' (#10)". selfEmails must exclude this identity's own entries
  49. // on sibling inbounds -- safe to do unconditionally because ClientRecord.Email
  50. // is globally unique, so a same-email match can only ever be this identity,
  51. // never a genuine different client.
  52. func TestOtherTunnelAllowedIPsExcludesSelfEmail(t *testing.T) {
  53. setupConflictDB(t)
  54. // Both shared@id (to be excluded) and other@awg (a genuinely different
  55. // client, must still be reported) live on the SAME sibling inbound --
  56. // otherTunnelAllowedIPs already excludes the asking inbound entirely via
  57. // excludeID, so putting other@awg there instead would make it invisible
  58. // to the scan regardless of the selfEmails fix, proving nothing.
  59. seedInboundConflict(t, "awg-1", "0.0.0.0", 443, model.AmneziaWG, ``, `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24},"clients":[{"email":"shared@id","allowedIPs":["10.8.1.21/32"]},{"email":"other@awg","allowedIPs":["10.8.1.5/32"]}]}`)
  60. seedInboundConflict(t, "wg-1", "0.0.0.0", 51820, model.WireGuard, ``, `{"clients":[]}`)
  61. var wgInbound model.Inbound
  62. if err := database.GetDB().Where("tag = ?", "wg-1").First(&wgInbound).Error; err != nil {
  63. t.Fatalf("read seeded wg row: %v", err)
  64. }
  65. svc := &ClientService{}
  66. inboundSvc := &InboundService{}
  67. used, err := svc.otherTunnelAllowedIPs(database.GetDB(), inboundSvc, wgInbound.Id, map[string]struct{}{"shared@id": {}})
  68. if err != nil {
  69. t.Fatalf("otherTunnelAllowedIPs: %v", err)
  70. }
  71. if _, stillThere := used["10.8.1.21/32"]; stillThere {
  72. t.Fatalf("shared@id's own address on the awg inbound must be excluded from used, got %v", used)
  73. }
  74. if _, ok := used["10.8.1.5/32"]; !ok {
  75. t.Fatalf("a genuinely different client's address must still be reported as used, got %v", used)
  76. }
  77. }
  78. func TestOtherTunnelAllowedIPsEmptyWhenNoSiblings(t *testing.T) {
  79. setupConflictDB(t)
  80. seedInboundConflict(t, "wg-1", "0.0.0.0", 51820, model.WireGuard, ``, `{"clients":[{"email":"a@wg","allowedIPs":["10.0.0.5/32"]}]}`)
  81. var wgInbound model.Inbound
  82. if err := database.GetDB().Where("tag = ?", "wg-1").First(&wgInbound).Error; err != nil {
  83. t.Fatalf("read seeded wg row: %v", err)
  84. }
  85. svc := &ClientService{}
  86. inboundSvc := &InboundService{}
  87. used, err := svc.otherTunnelAllowedIPs(database.GetDB(), inboundSvc, wgInbound.Id, nil)
  88. if err != nil {
  89. t.Fatalf("otherTunnelAllowedIPs: %v", err)
  90. }
  91. if len(used) != 0 {
  92. t.Fatalf("expected no cross-inbound addresses with only one tunnel inbound present, got %v", used)
  93. }
  94. }