inbound_amneziawg_relay_window_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  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. // awgRelayWindowSettingsWithForward is awgRelayWindowSettings with one client's
  24. // forwardedPorts set, the field the create-time guard validates.
  25. func awgRelayWindowSettingsWithForward(t *testing.T, tag, forwardedPorts string) string {
  26. t.Helper()
  27. settings := awgRelayWindowSettings(t, tag)
  28. return strings.Replace(settings, `"enable":true`, `"enable":true,"forwardedPorts":"`+forwardedPorts+`"`, 1)
  29. }
  30. // pushInboundIDSequence makes the next inbounds insert land on nextID, standing
  31. // in for a long-lived database whose AUTOINCREMENT counter has climbed there.
  32. func pushInboundIDSequence(t *testing.T, nextID int) {
  33. t.Helper()
  34. // The counter is a sqlite_sequence row, so this has no PostgreSQL equivalent.
  35. if database.IsPostgres() {
  36. t.Skip("the inbounds AUTOINCREMENT counter is a SQLite row")
  37. }
  38. res := database.GetDB().Exec("UPDATE sqlite_sequence SET seq = ? WHERE name = ?", nextID-1, "inbounds")
  39. if res.Error != nil {
  40. t.Fatalf("push the inbounds sequence to %d: %v", nextID, res.Error)
  41. }
  42. if res.RowsAffected != 1 {
  43. t.Fatalf("inbounds has no AUTOINCREMENT counter row to push (%d rows updated)", res.RowsAffected)
  44. }
  45. }
  46. func addAmneziaWGInbound(t *testing.T, tag string, port int, enable bool) *model.Inbound {
  47. t.Helper()
  48. created, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  49. Tag: tag,
  50. Enable: enable,
  51. Listen: "0.0.0.0",
  52. Port: port,
  53. Protocol: model.AmneziaWG,
  54. Settings: awgRelayWindowSettings(t, tag),
  55. })
  56. if err != nil {
  57. t.Fatalf("AddInbound(%s): %v", tag, err)
  58. }
  59. return created
  60. }
  61. // An id past the slot count used to be refused outright, which capped a
  62. // database at 435 AmneziaWG inbounds for its entire life (#6537).
  63. func TestAddInbound_AmneziawgPastTheRelayPortWindowStillCreates(t *testing.T) {
  64. setupConflictDB(t)
  65. // Self-check: the low-id path must work, or the assertion below could pass
  66. // because the fixture never created an AmneziaWG inbound at all.
  67. addAmneziaWGInbound(t, "awg-low-id", 51820, true)
  68. pushInboundIDSequence(t, 70001)
  69. created := addAmneziaWGInbound(t, "awg-past-window", 51821, true)
  70. if created.Id < 436 {
  71. t.Fatalf("fixture: inbound id %d is still inside the old window", created.Id)
  72. }
  73. if port := amneziawgnet.SOCKSPortForInbound(created.Id); port < amneziawgnet.SOCKSBasePort+1 || port > 65535 {
  74. t.Fatalf("inbound %d derived relay port %d, outside %d..65535", created.Id, port, amneziawgnet.SOCKSBasePort+1)
  75. }
  76. }
  77. // Wrapping ids makes the id -> relay-port map non-injective, so a create can
  78. // land on a port an existing inbound's relay already owns.
  79. func TestAddInbound_AmneziawgRefusesAClaimedRelayPort(t *testing.T) {
  80. for _, blockerEnabled := range []bool{true, false} {
  81. t.Run(fmt.Sprintf("blocker enabled=%t", blockerEnabled), func(t *testing.T) {
  82. setupConflictDB(t)
  83. blocker := addAmneziaWGInbound(t, "awg-blocker", 51820, blockerEnabled)
  84. // One slot-window further on is the id that derives the blocker's port.
  85. collidingID := blocker.Id + 435
  86. pushInboundIDSequence(t, collidingID)
  87. _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  88. Tag: "awg-collides",
  89. Enable: true,
  90. Listen: "0.0.0.0",
  91. Port: 51821,
  92. Protocol: model.AmneziaWG,
  93. Settings: awgRelayWindowSettings(t, "awg-collides"),
  94. })
  95. if err == nil {
  96. t.Fatalf("inbound %d derives relay port %d, already owned by %q; the create must be refused",
  97. collidingID, amneziawgnet.SOCKSPortForInbound(blocker.Id), blocker.Tag)
  98. }
  99. if !strings.Contains(err.Error(), blocker.Tag) {
  100. t.Fatalf("the conflict must name the inbound owning the port, got %v", err)
  101. }
  102. // The blocker's own port is its WireGuard one, so without this the
  103. // message reads as if that inbound listened on an unrelated port.
  104. if !strings.Contains(err.Error(), "relay port") {
  105. t.Fatalf("the refusal must say the port is an automatic relay one, got %v", err)
  106. }
  107. })
  108. }
  109. }
  110. // Wrapping makes id -> relay port non-injective, so an edit landing on a slot a
  111. // local inbound already owns has to be refused: the create guard never sees it.
  112. func TestCheckPortConflict_LocalAmneziawgRelayCollisionBlocksTheEdit(t *testing.T) {
  113. setupConflictDB(t)
  114. blocker := addAmneziaWGInbound(t, "awg-blocker", 51820, true)
  115. local := &model.Inbound{
  116. Tag: "awg-edited",
  117. Enable: true,
  118. Listen: "0.0.0.0",
  119. Port: 51821,
  120. Protocol: model.AmneziaWG,
  121. Settings: awgRelayWindowSettings(t, "awg-edited"),
  122. }
  123. collidingID := blocker.Id + 435
  124. got, err := (&InboundService{}).checkPortConflict(local, collidingID)
  125. if err != nil {
  126. t.Fatalf("checkPortConflict: %v", err)
  127. }
  128. if got == nil {
  129. t.Fatalf("id %d derives relay port %d, already owned by %q; the save must be refused",
  130. collidingID, amneziawgnet.SOCKSPortForInbound(blocker.Id), blocker.Tag)
  131. }
  132. if !strings.Contains(got.String(), blocker.Tag) {
  133. t.Fatalf("the conflict must name the inbound owning the port, got %q", got.String())
  134. }
  135. }
  136. // A disabled row still owns the relay slot its id derives: SetInboundEnable
  137. // flips the column with no port check, so enabling it later would break Xray.
  138. func TestCheckPortConflict_DisabledAmneziawgStillOwnsItsRelaySlot(t *testing.T) {
  139. setupConflictDB(t)
  140. owner := addAmneziaWGInbound(t, "awg-disabled", 51820, false)
  141. relayPort := amneziawgnet.SOCKSPortForInbound(owner.Id)
  142. got, err := (&InboundService{}).checkPortConflict(&model.Inbound{
  143. Tag: "takes-the-slot",
  144. Enable: true,
  145. Listen: "0.0.0.0",
  146. Port: relayPort,
  147. Protocol: model.VLESS,
  148. Settings: `{"clients":[]}`,
  149. }, 0)
  150. if err != nil {
  151. t.Fatalf("checkPortConflict: %v", err)
  152. }
  153. if got == nil {
  154. t.Fatalf("inbound #%d is disabled but still owns relay port %d; the save must be refused",
  155. owner.Id, relayPort)
  156. }
  157. if !strings.Contains(got.String(), owner.Tag) {
  158. t.Fatalf("the conflict must name the inbound owning the port, got %q", got.String())
  159. }
  160. }
  161. // The forwarded-ports guard runs before Save, when the row has no id yet, so a
  162. // client's spec never saw the relay port the row itself derives.
  163. func TestAddInbound_AmneziawgRefusesAClientForwardingItsOwnRelayPort(t *testing.T) {
  164. setupConflictDB(t)
  165. placeholder := addAmneziaWGInbound(t, "awg-placeholder", 51820, true)
  166. ownPort := amneziawgnet.SOCKSPortForInbound(placeholder.Id + 1)
  167. _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  168. Tag: "awg-forward",
  169. Enable: true,
  170. Listen: "0.0.0.0",
  171. Port: 51821,
  172. Protocol: model.AmneziaWG,
  173. Settings: awgRelayWindowSettingsWithForward(t, "awg-forward", fmt.Sprintf("%d", ownPort)),
  174. })
  175. if err == nil {
  176. t.Fatalf("inbound #%d derives relay port %d and its own client forwards that port; the create must be refused",
  177. placeholder.Id+1, ownPort)
  178. }
  179. if !strings.Contains(err.Error(), "forwardedPorts") {
  180. t.Fatalf("the refusal must come from the forwarded-ports guard, got %v", err)
  181. }
  182. }
  183. // The row's own WireGuard port can be the relay port its own id derives, and
  184. // every relay check excludes that id, so nothing else compares the two.
  185. func TestAddInbound_AmneziawgRefusesItsOwnRelayPort(t *testing.T) {
  186. setupConflictDB(t)
  187. // Read the sequence instead of assuming id 1: the victim's own derived port
  188. // has to be known before it is created.
  189. placeholder := addAmneziaWGInbound(t, "awg-placeholder", 51820, true)
  190. selfPort := amneziawgnet.SOCKSPortForInbound(placeholder.Id + 1)
  191. _, _, err := (&InboundService{}).AddInbound(&model.Inbound{
  192. Tag: "awg-self",
  193. Enable: true,
  194. Listen: "0.0.0.0",
  195. Port: selfPort,
  196. Protocol: model.AmneziaWG,
  197. Settings: awgRelayWindowSettings(t, "awg-self"),
  198. })
  199. if err == nil {
  200. t.Fatalf("WireGuard port %d is inbound #%d's own relay port; the create must be refused",
  201. selfPort, placeholder.Id+1)
  202. }
  203. if !strings.Contains(err.Error(), "relay port") {
  204. t.Fatalf("the refusal must say the port is an automatic relay one, got %v", err)
  205. }
  206. }
  207. // The edit path knows the id the relay port comes from, so it has to refuse the
  208. // same self-collision -- the reverse check skips the row it computes for.
  209. func TestUpdateInbound_AmneziawgRefusesItsOwnRelayPort(t *testing.T) {
  210. setupConflictDB(t)
  211. created := addAmneziaWGInbound(t, "awg-self-edit", 51820, true)
  212. edit := *created
  213. edit.Port = amneziawgnet.SOCKSPortForInbound(created.Id)
  214. if edit.Port == created.Port {
  215. t.Fatalf("fixture: inbound #%d already listens on its derived relay port", created.Id)
  216. }
  217. _, _, err := (&InboundService{}).UpdateInbound(&edit)
  218. if err == nil {
  219. t.Fatalf("WireGuard port %d is inbound #%d's own relay port; the save must be refused",
  220. edit.Port, created.Id)
  221. }
  222. if !strings.Contains(err.Error(), "relay port") {
  223. t.Fatalf("the refusal must say the port is an automatic relay one, got %v", err)
  224. }
  225. }
  226. // A row adopted from a node keeps the protocol it arrived with and its central
  227. // id (inbound_node.go:737), but gets no relay -- so its slot can never be taken.
  228. func TestCheckPortConflict_NodeAssignedAmneziawgOwnsNoRelaySlot(t *testing.T) {
  229. setupConflictDB(t)
  230. blocker := addAmneziaWGInbound(t, "awg-blocker", 51820, true)
  231. nodeID := 7
  232. adopted := &model.Inbound{
  233. Tag: "awg-adopted",
  234. Enable: true,
  235. Listen: "0.0.0.0",
  236. Port: 51821,
  237. Protocol: model.AmneziaWG,
  238. Settings: awgRelayWindowSettings(t, "awg-adopted"),
  239. NodeID: &nodeID,
  240. }
  241. collidingID := blocker.Id + 435
  242. if amneziawgnet.SOCKSPortForInbound(collidingID) != amneziawgnet.SOCKSPortForInbound(blocker.Id) {
  243. t.Fatalf("fixture: id %d does not derive the blocker's relay port", collidingID)
  244. }
  245. got, err := (&InboundService{}).checkPortConflict(adopted, collidingID)
  246. if err != nil {
  247. t.Fatalf("checkPortConflict: %v", err)
  248. }
  249. if got != nil {
  250. t.Fatalf("id %d is node-assigned and binds no relay, so it cannot collide; got %q",
  251. collidingID, got.String())
  252. }
  253. // The same rule covers the row's own port: with no relay on this host, its
  254. // WireGuard port may legitimately BE the port its id would derive.
  255. adopted.Port = amneziawgnet.SOCKSPortForInbound(collidingID)
  256. got, err = (&InboundService{}).checkPortConflict(adopted, collidingID)
  257. if err != nil {
  258. t.Fatalf("checkPortConflict: %v", err)
  259. }
  260. if got != nil {
  261. t.Fatalf("id %d is node-assigned and binds no relay, so its own port is not a conflict; got %q",
  262. collidingID, got.String())
  263. }
  264. }