client_attach_test.go 4.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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. // TestHasTunnelAttachmentDetectsWireguardOrAmneziaWG backs the fix for a
  8. // real production bug: Attach copies an identity's stored AllowedIPs into
  9. // every inbound it processes (so the same person keeps the same tunnel
  10. // address across protocols), but when an identity has been fully detached
  11. // from every WireGuard/AmneziaWG inbound, that stored address is a leftover
  12. // nothing reserves anymore -- reusing it can skip past address space that's
  13. // genuinely free (a real user's own case: address .21 resurrected instead
  14. // of the actually-free .3). hasTunnelAttachment is what Attach checks to
  15. // decide whether to clear the stored address before its loop, so it needs
  16. // to correctly tell "still has an active tunnel elsewhere" (preserve) apart
  17. // from "no tunnel attachment at all" (clear, allocate fresh).
  18. func TestHasTunnelAttachmentDetectsWireguardOrAmneziaWG(t *testing.T) {
  19. setupConflictDB(t)
  20. seedInboundConflict(t, "awg-1", "0.0.0.0", 443, model.AmneziaWG, ``, `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24},"clients":[]}`)
  21. seedInboundConflict(t, "wg-1", "0.0.0.0", 51820, model.WireGuard, ``, `{"clients":[]}`)
  22. seedInboundConflict(t, "vless-1", "0.0.0.0", 8443, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`)
  23. var awgInbound, wgInbound, vlessInbound model.Inbound
  24. if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
  25. t.Fatalf("read seeded awg row: %v", err)
  26. }
  27. if err := database.GetDB().Where("tag = ?", "wg-1").First(&wgInbound).Error; err != nil {
  28. t.Fatalf("read seeded wg row: %v", err)
  29. }
  30. if err := database.GetDB().Where("tag = ?", "vless-1").First(&vlessInbound).Error; err != nil {
  31. t.Fatalf("read seeded vless row: %v", err)
  32. }
  33. s := &ClientService{}
  34. inboundSvc := &InboundService{}
  35. if s.hasTunnelAttachment(inboundSvc, nil) {
  36. t.Error("empty inboundIds must report no tunnel attachment")
  37. }
  38. if s.hasTunnelAttachment(inboundSvc, []int{vlessInbound.Id}) {
  39. t.Error("a VLESS-only attachment must not count as a tunnel attachment")
  40. }
  41. if s.hasTunnelAttachment(inboundSvc, []int{99999}) {
  42. t.Error("a nonexistent inbound id must not count as a tunnel attachment")
  43. }
  44. if !s.hasTunnelAttachment(inboundSvc, []int{vlessInbound.Id, wgInbound.Id}) {
  45. t.Error("a WireGuard inbound among others must count as a tunnel attachment")
  46. }
  47. if !s.hasTunnelAttachment(inboundSvc, []int{awgInbound.Id}) {
  48. t.Error("an AmneziaWG inbound must count as a tunnel attachment")
  49. }
  50. }
  51. // TestAddressesFitAmneziaWGInbound is a regression test for a real
  52. // production bug: hasTunnelAttachment only asked "does this identity have
  53. // ANY tunnel attachment", not "is the address it would inherit actually
  54. // valid for THIS inbound" -- so an identity whose stored address came from
  55. // WireGuard's own fallback subnet (10.0.0.0/24, used when that inbound has
  56. // no other clients to infer a base from) got that exact address silently
  57. // carried over onto a second, AmneziaWG inbound configured for a completely
  58. // different subnet (10.8.1.0/24). defaultAmneziaWGClients's already-set
  59. // branch only checks for collisions, not subnet membership, so the mismatch
  60. // was accepted with no error -- producing a peer that can never actually
  61. // connect (an AmneziaWG address must fall inside the kernel interface's own
  62. // configured subnet to be routable at all). addressesFitAmneziaWGInbound is
  63. // the check Attach now runs per inbound before deciding whether to keep an
  64. // inherited address or force a fresh allocation.
  65. func TestAddressesFitAmneziaWGInbound(t *testing.T) {
  66. setupConflictDB(t)
  67. seedInboundConflict(t, "awg-1", "0.0.0.0", 443, model.AmneziaWG, ``, `{"server":{"subnetIp":"10.8.1.0","subnetCidr":24},"clients":[]}`)
  68. seedInboundConflict(t, "wg-1", "0.0.0.0", 51820, model.WireGuard, ``, `{"clients":[]}`)
  69. var awgInbound, wgInbound model.Inbound
  70. if err := database.GetDB().Where("tag = ?", "awg-1").First(&awgInbound).Error; err != nil {
  71. t.Fatalf("read seeded awg row: %v", err)
  72. }
  73. if err := database.GetDB().Where("tag = ?", "wg-1").First(&wgInbound).Error; err != nil {
  74. t.Fatalf("read seeded wg row: %v", err)
  75. }
  76. if !addressesFitAmneziaWGInbound(nil, &awgInbound) {
  77. t.Error("no addresses at all must trivially fit (Attach's own fresh-allocate path)")
  78. }
  79. if !addressesFitAmneziaWGInbound([]string{"10.0.0.2/32"}, &wgInbound) {
  80. t.Error("WireGuard has no strict subnet requirement -- must never be rejected here")
  81. }
  82. if addressesFitAmneziaWGInbound([]string{"10.0.0.2/32"}, &awgInbound) {
  83. t.Fatal("the real bug: a WireGuard-fallback-subnet address must NOT be accepted as fitting an AmneziaWG inbound configured for a different subnet")
  84. }
  85. if !addressesFitAmneziaWGInbound([]string{"10.8.1.21/32"}, &awgInbound) {
  86. t.Error("an address genuinely inside the awg inbound's own configured subnet must fit")
  87. }
  88. }