node_origin_guid_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  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. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  7. )
  8. // #4983: a synced inbound's OriginNodeGuid must point at the panel that
  9. // physically hosts it. A node's own local inbound (empty origin in its
  10. // snapshot) is attributed to the node's own GUID; an inbound the node forwards
  11. // from its own sub-node (non-empty origin) keeps that deeper GUID across the
  12. // hop — so a chained Node1->Node2->Node3 attributes Node3's inbounds to Node3.
  13. func TestSetRemoteTraffic_AttributesOriginNodeGuid(t *testing.T) {
  14. setupConflictDB(t)
  15. db := database.GetDB()
  16. const nodeID = 1
  17. if err := db.Create(&model.Node{
  18. Id: nodeID,
  19. Name: "node2",
  20. Address: "10.0.0.2",
  21. Port: 2053,
  22. ApiToken: "t",
  23. Guid: "node2-guid",
  24. }).Error; err != nil {
  25. t.Fatalf("create node: %v", err)
  26. }
  27. snap := &runtime.TrafficSnapshot{
  28. Inbounds: []*model.Inbound{
  29. { // node2's own local inbound — reports no origin
  30. Tag: "in-443-tcp",
  31. Enable: true,
  32. Port: 443,
  33. Protocol: model.VLESS,
  34. Settings: `{"clients":[]}`,
  35. },
  36. { // forwarded from node2's sub-node (node3) — carries node3's guid
  37. Tag: "in-8443-tcp",
  38. Enable: true,
  39. Port: 8443,
  40. Protocol: model.VLESS,
  41. Settings: `{"clients":[]}`,
  42. OriginNodeGuid: "node3-guid",
  43. },
  44. },
  45. }
  46. svc := InboundService{}
  47. if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
  48. t.Fatalf("setRemoteTrafficLocked: %v", err)
  49. }
  50. origin := func(tag string) string {
  51. var ib model.Inbound
  52. if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
  53. t.Fatalf("load inbound %q: %v", tag, err)
  54. }
  55. return ib.OriginNodeGuid
  56. }
  57. if og := origin("in-443-tcp"); og != "node2-guid" {
  58. t.Fatalf("local inbound origin = %q, want node2-guid (the node's own GUID)", og)
  59. }
  60. if og := origin("in-8443-tcp"); og != "node3-guid" {
  61. t.Fatalf("forwarded inbound origin = %q, want node3-guid (kept across the hop)", og)
  62. }
  63. }
  64. // A cloned node reports its OWN inbound with its own (duplicated) panelGuid as
  65. // the origin. That must be remapped to the node-unique key, not stored verbatim
  66. // — otherwise origin_node_guid keeps the shared GUID while online is keyed by
  67. // the node-unique key, and the inbound page reads an empty bucket (shows
  68. // offline). A genuinely forwarded sub-node GUID is still kept across the hop.
  69. func TestSetRemoteTraffic_RemapsClonedNodeOwnGuidOrigin(t *testing.T) {
  70. setupConflictDB(t)
  71. db := database.GetDB()
  72. // Two nodes share one panelGuid (cloned servers).
  73. for _, n := range []*model.Node{
  74. {Id: 1, Name: "a", Address: "10.0.0.1", Port: 2053, ApiToken: "t", Guid: "dup"},
  75. {Id: 2, Name: "b", Address: "10.0.0.2", Port: 2053, ApiToken: "t", Guid: "dup"},
  76. } {
  77. if err := db.Create(n).Error; err != nil {
  78. t.Fatalf("create node %s: %v", n.Name, err)
  79. }
  80. }
  81. snap := &runtime.TrafficSnapshot{
  82. Inbounds: []*model.Inbound{
  83. { // node 1's OWN inbound, reporting its own (shared) panelGuid as origin
  84. Tag: "own-443-tcp",
  85. Enable: true,
  86. Port: 443,
  87. Protocol: model.VLESS,
  88. Settings: `{"clients":[]}`,
  89. OriginNodeGuid: "dup",
  90. },
  91. { // forwarded from a sub-node with a distinct guid — kept across the hop
  92. Tag: "fwd-8443-tcp",
  93. Enable: true,
  94. Port: 8443,
  95. Protocol: model.VLESS,
  96. Settings: `{"clients":[]}`,
  97. OriginNodeGuid: "child-guid",
  98. },
  99. },
  100. }
  101. svc := InboundService{}
  102. if _, err := svc.setRemoteTrafficLocked(1, snap, false); err != nil {
  103. t.Fatalf("setRemoteTrafficLocked: %v", err)
  104. }
  105. origin := func(tag string) string {
  106. var ib model.Inbound
  107. if err := db.Where("tag = ?", tag).First(&ib).Error; err != nil {
  108. t.Fatalf("load inbound %q: %v", tag, err)
  109. }
  110. return ib.OriginNodeGuid
  111. }
  112. if og := origin("own-443-tcp"); og != "node:1" {
  113. t.Fatalf("cloned node's own inbound origin = %q, want node:1 (remapped from shared GUID)", og)
  114. }
  115. if og := origin("fwd-8443-tcp"); og != "child-guid" {
  116. t.Fatalf("forwarded inbound origin = %q, want child-guid (kept across the hop)", og)
  117. }
  118. }
  119. // A node mid-restart can return an empty inbound list with success=true. The
  120. // sync must NOT treat that as "delete all my inbounds" — otherwise a blip wipes
  121. // the node's central inbounds and every client on them (what happened to the
  122. // Germany node: 0 clients but still online).
  123. func TestSetRemoteTraffic_EmptySnapshotKeepsCentralInbounds(t *testing.T) {
  124. setupConflictDB(t)
  125. db := database.GetDB()
  126. const nodeID = 1
  127. if err := db.Create(&model.Node{
  128. Id: nodeID, Name: "n", Address: "10.0.0.1", Port: 2053, ApiToken: "t", Guid: "g",
  129. }).Error; err != nil {
  130. t.Fatalf("create node: %v", err)
  131. }
  132. nidPtr := nodeID
  133. if err := db.Create(&model.Inbound{
  134. UserId: 1, NodeID: &nidPtr, Tag: "remote-in", Enable: true,
  135. Port: 443, Protocol: model.VLESS, Settings: `{"clients":[]}`,
  136. }).Error; err != nil {
  137. t.Fatalf("create central inbound: %v", err)
  138. }
  139. // Empty snapshot — the node reported no inbounds this cycle.
  140. svc := InboundService{}
  141. if _, err := svc.setRemoteTrafficLocked(nodeID, &runtime.TrafficSnapshot{}, false); err != nil {
  142. t.Fatalf("setRemoteTrafficLocked: %v", err)
  143. }
  144. var count int64
  145. if err := db.Model(&model.Inbound{}).Where("tag = ?", "remote-in").Count(&count).Error; err != nil {
  146. t.Fatalf("count inbounds: %v", err)
  147. }
  148. if count != 1 {
  149. t.Fatalf("empty snapshot must not delete the central inbound; got count = %d", count)
  150. }
  151. }
  152. func TestSetRemoteTraffic_PreservesLocalShareAddressStrategy(t *testing.T) {
  153. setupConflictDB(t)
  154. db := database.GetDB()
  155. const nodeID = 1
  156. if err := db.Create(&model.Node{
  157. Id: nodeID,
  158. Name: "node2",
  159. Address: "10.0.0.2",
  160. Port: 2053,
  161. ApiToken: "t",
  162. Guid: "node2-guid",
  163. }).Error; err != nil {
  164. t.Fatalf("create node: %v", err)
  165. }
  166. nodeIDPtr := nodeID
  167. if err := db.Create(&model.Inbound{
  168. UserId: 1,
  169. NodeID: &nodeIDPtr,
  170. Tag: "remote-in",
  171. Enable: true,
  172. Port: 443,
  173. Protocol: model.VLESS,
  174. Settings: `{"clients":[]}`,
  175. ShareAddrStrategy: "custom",
  176. ShareAddr: "edge.example.com",
  177. }).Error; err != nil {
  178. t.Fatalf("create central inbound: %v", err)
  179. }
  180. snap := &runtime.TrafficSnapshot{
  181. Inbounds: []*model.Inbound{{
  182. Tag: "remote-in",
  183. Enable: true,
  184. Port: 8443,
  185. Protocol: model.VLESS,
  186. Settings: `{"clients":[]}`,
  187. }},
  188. }
  189. svc := InboundService{}
  190. if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
  191. t.Fatalf("setRemoteTrafficLocked: %v", err)
  192. }
  193. var ib model.Inbound
  194. if err := db.Where("tag = ?", "remote-in").First(&ib).Error; err != nil {
  195. t.Fatalf("load inbound: %v", err)
  196. }
  197. if ib.ShareAddrStrategy != "custom" || ib.ShareAddr != "edge.example.com" {
  198. t.Fatalf("share address fields were overwritten: strategy=%q addr=%q", ib.ShareAddrStrategy, ib.ShareAddr)
  199. }
  200. if ib.Port != 8443 {
  201. t.Fatalf("sync should still update regular remote fields; port = %d, want 8443", ib.Port)
  202. }
  203. }
  204. func TestSetRemoteTraffic_DefaultsShareAddressFieldsForNewCentralInbound(t *testing.T) {
  205. setupConflictDB(t)
  206. db := database.GetDB()
  207. const nodeID = 1
  208. if err := db.Create(&model.Node{
  209. Id: nodeID,
  210. Name: "node2",
  211. Address: "10.0.0.2",
  212. Port: 2053,
  213. ApiToken: "t",
  214. Guid: "node2-guid",
  215. }).Error; err != nil {
  216. t.Fatalf("create node: %v", err)
  217. }
  218. snap := &runtime.TrafficSnapshot{
  219. Inbounds: []*model.Inbound{{
  220. Tag: "remote-in",
  221. Enable: true,
  222. Port: 8443,
  223. Protocol: model.VLESS,
  224. Settings: `{"clients":[]}`,
  225. ShareAddrStrategy: "custom",
  226. ShareAddr: "remote.example.com",
  227. }},
  228. }
  229. svc := InboundService{}
  230. if _, err := svc.setRemoteTrafficLocked(nodeID, snap, false); err != nil {
  231. t.Fatalf("setRemoteTrafficLocked: %v", err)
  232. }
  233. var ib model.Inbound
  234. if err := db.Where("tag = ?", "remote-in").First(&ib).Error; err != nil {
  235. t.Fatalf("load inbound: %v", err)
  236. }
  237. if ib.ShareAddrStrategy != "node" || ib.ShareAddr != "" {
  238. t.Fatalf("new central inbound share fields = (%q, %q), want (node, empty)", ib.ShareAddrStrategy, ib.ShareAddr)
  239. }
  240. }