inbound_amneziawg_relay_window_test.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. package service
  2. import (
  3. "fmt"
  4. "strings"
  5. "testing"
  6. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. wgutil "github.com/mhsanaei/3x-ui/v3/internal/util/wireguard"
  10. )
  11. // awgRelayWindowSettings builds an AmneziaWG settings blob AddInbound accepts:
  12. // real X25519 keys, one enabled peer, and an email unique to tag.
  13. func awgRelayWindowSettings(t *testing.T, tag string) string {
  14. t.Helper()
  15. _, clientPub, err := wgutil.GenerateWireguardKeypair()
  16. if err != nil {
  17. t.Fatalf("generate client keypair: %v", err)
  18. }
  19. return `{"server":{"privateKey":"` + awgTestPrivateKey + `","publicKey":"` + awgTestPublicKey +
  20. `","subnetIp":"10.8.1.0","subnetCidr":24},"clients":[{"email":"` + tag + `@relay-window","enable":true,"publicKey":"` +
  21. clientPub + `","allowedIPs":["10.8.1.2/32"]}]}`
  22. }
  23. // pushInboundIDSequence makes the next inbounds insert land on nextID, standing
  24. // in for a long-lived database whose AUTOINCREMENT counter has climbed there.
  25. func pushInboundIDSequence(t *testing.T, nextID int) {
  26. t.Helper()
  27. // The counter is a sqlite_sequence row, so this has no PostgreSQL equivalent.
  28. if database.IsPostgres() {
  29. t.Skip("the inbounds AUTOINCREMENT counter is a SQLite row")
  30. }
  31. res := database.GetDB().Exec("UPDATE sqlite_sequence SET seq = ? WHERE name = ?", nextID-1, "inbounds")
  32. if res.Error != nil {
  33. t.Fatalf("push the inbounds sequence to %d: %v", nextID, res.Error)
  34. }
  35. if res.RowsAffected != 1 {
  36. t.Fatalf("inbounds has no AUTOINCREMENT counter row to push (%d rows updated)", res.RowsAffected)
  37. }
  38. }
  39. func addAmneziaWGInbound(t *testing.T, tag string, port int, enable bool) *model.Inbound {
  40. t.Helper()
  41. created, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  42. Tag: tag,
  43. Enable: enable,
  44. Listen: "0.0.0.0",
  45. Port: port,
  46. Protocol: model.AmneziaWG,
  47. Settings: awgRelayWindowSettings(t, tag),
  48. })
  49. if err != nil {
  50. t.Fatalf("AddInbound(%s): %v", tag, err)
  51. }
  52. return created
  53. }
  54. // An id past the slot count used to be refused outright, which capped a
  55. // database at 435 AmneziaWG inbounds for its entire life (#6537).
  56. func TestAddInbound_AmneziawgPastTheRelayPortWindowStillCreates(t *testing.T) {
  57. setupConflictDB(t)
  58. // Self-check: the low-id path must work, or the assertion below could pass
  59. // because the fixture never created an AmneziaWG inbound at all.
  60. addAmneziaWGInbound(t, "awg-low-id", 51820, true)
  61. pushInboundIDSequence(t, 70001)
  62. created := addAmneziaWGInbound(t, "awg-past-window", 51821, true)
  63. if created.Id < 436 {
  64. t.Fatalf("fixture: inbound id %d is still inside the old window", created.Id)
  65. }
  66. if port := amneziawgnet.SOCKSPortForInbound(created.Id); port < amneziawgnet.SOCKSBasePort+1 || port > 65535 {
  67. t.Fatalf("inbound %d derived relay port %d, outside %d..65535", created.Id, port, amneziawgnet.SOCKSBasePort+1)
  68. }
  69. }
  70. // Wrapping ids makes the id -> relay-port map non-injective, so a create can
  71. // land on a port an existing inbound's relay already owns.
  72. func TestAddInbound_AmneziawgRefusesAClaimedRelayPort(t *testing.T) {
  73. for _, blockerEnabled := range []bool{true, false} {
  74. t.Run(fmt.Sprintf("blocker enabled=%t", blockerEnabled), func(t *testing.T) {
  75. setupConflictDB(t)
  76. blocker := addAmneziaWGInbound(t, "awg-blocker", 51820, blockerEnabled)
  77. // One slot-window further on is the id that derives the blocker's port.
  78. collidingID := blocker.Id + 435
  79. pushInboundIDSequence(t, collidingID)
  80. _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  81. Tag: "awg-collides",
  82. Enable: true,
  83. Listen: "0.0.0.0",
  84. Port: 51821,
  85. Protocol: model.AmneziaWG,
  86. Settings: awgRelayWindowSettings(t, "awg-collides"),
  87. })
  88. if err == nil {
  89. t.Fatalf("inbound %d derives relay port %d, already owned by %q; the create must be refused",
  90. collidingID, amneziawgnet.SOCKSPortForInbound(blocker.Id), blocker.Tag)
  91. }
  92. if !strings.Contains(err.Error(), blocker.Tag) {
  93. t.Fatalf("the conflict must name the inbound owning the port, got %v", err)
  94. }
  95. // The blocker's own port is its WireGuard one, so without this the
  96. // message reads as if that inbound listened on an unrelated port.
  97. if !strings.Contains(err.Error(), "relay port") {
  98. t.Fatalf("the refusal must say the port is an automatic relay one, got %v", err)
  99. }
  100. })
  101. }
  102. }
  103. // Wrapping makes id -> relay port non-injective, so an edit landing on a slot a
  104. // local inbound already owns has to be refused: the create guard never sees it.
  105. func TestCheckPortConflict_LocalAmneziawgRelayCollisionBlocksTheEdit(t *testing.T) {
  106. setupConflictDB(t)
  107. blocker := addAmneziaWGInbound(t, "awg-blocker", 51820, true)
  108. local := &model.Inbound{
  109. Tag: "awg-edited",
  110. Enable: true,
  111. Listen: "0.0.0.0",
  112. Port: 51821,
  113. Protocol: model.AmneziaWG,
  114. Settings: awgRelayWindowSettings(t, "awg-edited"),
  115. }
  116. collidingID := blocker.Id + 435
  117. got, err := (&InboundService{}).checkPortConflict(local, collidingID)
  118. if err != nil {
  119. t.Fatalf("checkPortConflict: %v", err)
  120. }
  121. if got == nil {
  122. t.Fatalf("id %d derives relay port %d, already owned by %q; the save must be refused",
  123. collidingID, amneziawgnet.SOCKSPortForInbound(blocker.Id), blocker.Tag)
  124. }
  125. if !strings.Contains(got.String(), blocker.Tag) {
  126. t.Fatalf("the conflict must name the inbound owning the port, got %q", got.String())
  127. }
  128. }
  129. // A row adopted from a node keeps the protocol it arrived with and its central
  130. // id (inbound_node.go:737), but gets no relay -- so its slot can never be taken.
  131. func TestCheckPortConflict_NodeAssignedAmneziawgOwnsNoRelaySlot(t *testing.T) {
  132. setupConflictDB(t)
  133. blocker := addAmneziaWGInbound(t, "awg-blocker", 51820, true)
  134. nodeID := 7
  135. adopted := &model.Inbound{
  136. Tag: "awg-adopted",
  137. Enable: true,
  138. Listen: "0.0.0.0",
  139. Port: 51821,
  140. Protocol: model.AmneziaWG,
  141. Settings: awgRelayWindowSettings(t, "awg-adopted"),
  142. NodeID: &nodeID,
  143. }
  144. collidingID := blocker.Id + 435
  145. if amneziawgnet.SOCKSPortForInbound(collidingID) != amneziawgnet.SOCKSPortForInbound(blocker.Id) {
  146. t.Fatalf("fixture: id %d does not derive the blocker's relay port", collidingID)
  147. }
  148. got, err := (&InboundService{}).checkPortConflict(adopted, collidingID)
  149. if err != nil {
  150. t.Fatalf("checkPortConflict: %v", err)
  151. }
  152. if got != nil {
  153. t.Fatalf("id %d is node-assigned and binds no relay, so it cannot collide; got %q",
  154. collidingID, got.String())
  155. }
  156. }