1
0

inbound_node_sidecar_test.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. package service
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. )
  11. func seedVersionedNode(t *testing.T, panelVersion string) *model.Node {
  12. t.Helper()
  13. node := &model.Node{
  14. Name: "n-" + panelVersion, Address: "127.0.0.1", Port: 2096, Scheme: "https",
  15. Enable: true, Status: "online", PanelVersion: panelVersion,
  16. }
  17. seedNodeRow(t, database.GetDB(), node)
  18. return node
  19. }
  20. func sidecarNodeInbound(t *testing.T, protocol model.Protocol, tag string, nodeID int) *model.Inbound {
  21. t.Helper()
  22. ib := &model.Inbound{Tag: tag, Enable: true, Listen: "0.0.0.0", Port: 44300, Protocol: protocol, NodeID: &nodeID}
  23. switch protocol {
  24. case model.AmneziaWG:
  25. ib.Settings = awgRelayWindowSettings(t, tag)
  26. case model.TUIC:
  27. ib.Settings = `{"clients":[{"id":"8a4f0c7e-1d2b-4c3a-9e5f-6a7b8c9d0e1f","password":"pass","email":"` + tag + `@tuic","enable":true}]}`
  28. case model.MTProto:
  29. ib.Settings = `{"clients":[{"email":"` + tag + `@mt","enable":true,"secret":"ee0123456789abcdef0123456789abcdef"}]}`
  30. default:
  31. t.Fatalf("no fixture for %s", protocol)
  32. }
  33. return ib
  34. }
  35. // The node's own panel runs the sidecar for a pushed row, so a sidecar protocol
  36. // is deployable to any node new enough to know it -- the release that added it.
  37. func TestAddInbound_SidecarProtocolDeploysToANodeAtItsFirstRelease(t *testing.T) {
  38. cases := []struct {
  39. protocol model.Protocol
  40. firstRelease string
  41. }{
  42. {model.MTProto, "v3.5.0"},
  43. {model.AmneziaWG, "v3.7.0"},
  44. {model.TUIC, "v3.8.0"},
  45. }
  46. for _, tc := range cases {
  47. t.Run(string(tc.protocol), func(t *testing.T) {
  48. setupConflictDB(t)
  49. node := seedVersionedNode(t, tc.firstRelease)
  50. created, _, err := (&InboundService{}).AddInbound(sidecarNodeInbound(t, tc.protocol, "side-"+string(tc.protocol), node.Id))
  51. if err != nil {
  52. t.Fatalf("AddInbound(%s on a %s node): %v", tc.protocol, tc.firstRelease, err)
  53. }
  54. var stored model.Inbound
  55. if err := database.GetDB().First(&stored, created.Id).Error; err != nil {
  56. t.Fatalf("read created row: %v", err)
  57. }
  58. if stored.NodeID == nil || *stored.NodeID != node.Id {
  59. t.Fatalf("stored nodeId = %v, want %d", stored.NodeID, node.Id)
  60. }
  61. })
  62. }
  63. }
  64. // A node that predates a protocol stores it as an Xray inbound Xray cannot load,
  65. // so the assignment is refused while the version is too old or still unknown.
  66. func TestAddInbound_SidecarProtocolNodeVersionGate(t *testing.T) {
  67. cases := []struct {
  68. name string
  69. panelVersion string
  70. wantErr string
  71. }{
  72. {"one release too old", "v3.6.9", "v3.7.0"},
  73. {"version not reported yet", "", "has not reported its panel version"},
  74. {"dev build", "dev+1d1128cf", ""},
  75. }
  76. for _, tc := range cases {
  77. t.Run(tc.name, func(t *testing.T) {
  78. setupConflictDB(t)
  79. node := seedVersionedNode(t, tc.panelVersion)
  80. _, _, err := (&InboundService{}).AddInbound(sidecarNodeInbound(t, model.AmneziaWG, "awg-gate", node.Id))
  81. if tc.wantErr == "" {
  82. if err != nil {
  83. t.Fatalf("a dev build tracks main and knows every protocol; got %v", err)
  84. }
  85. return
  86. }
  87. if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
  88. t.Fatalf("err = %v, want a refusal mentioning %q", err, tc.wantErr)
  89. }
  90. })
  91. }
  92. }
  93. // The egress port is a loopback port on the host running mtg; one picked or
  94. // copied on the master can be taken on the node, so the node must allocate it.
  95. func TestAddInbound_NodeMtprotoLeavesTheEgressPortToTheNode(t *testing.T) {
  96. setupConflictDB(t)
  97. node := seedVersionedNode(t, "v3.8.0")
  98. ib := sidecarNodeInbound(t, model.MTProto, "mt-routed", node.Id)
  99. ib.Settings = `{"routeThroughXray":true,"routeXrayPort":4444,` + strings.TrimPrefix(ib.Settings, "{")
  100. created, _, err := (&InboundService{}).AddInbound(ib)
  101. if err != nil {
  102. t.Fatalf("AddInbound: %v", err)
  103. }
  104. var stored model.Inbound
  105. if err := database.GetDB().First(&stored, created.Id).Error; err != nil {
  106. t.Fatalf("read created row: %v", err)
  107. }
  108. var settings map[string]any
  109. if err := json.Unmarshal([]byte(stored.Settings), &settings); err != nil {
  110. t.Fatalf("decode settings: %v", err)
  111. }
  112. if port, ok := settings["routeXrayPort"]; ok {
  113. t.Fatalf("routeXrayPort = %v reached the node row; the node must allocate its own", port)
  114. }
  115. if settings["routeThroughXray"] != true {
  116. t.Fatalf("routeThroughXray = %v, want true kept", settings["routeThroughXray"])
  117. }
  118. }
  119. // A peer's forward listener binds on the host running the AmneziaWG row, so a
  120. // node row is checked against that node's inbounds, never the master's.
  121. func TestAddInbound_NodeAmneziaWGForwardedPortsCheckTheNodesHost(t *testing.T) {
  122. cases := []struct {
  123. name string
  124. onNode bool
  125. wantErr bool
  126. }{
  127. {"port of a master-local inbound", false, false},
  128. {"port of an inbound on the same node", true, true},
  129. }
  130. for _, tc := range cases {
  131. t.Run(tc.name, func(t *testing.T) {
  132. setupConflictDB(t)
  133. node := seedVersionedNode(t, "v3.8.0")
  134. var owner *int
  135. if tc.onNode {
  136. owner = &node.Id
  137. }
  138. seedInboundConflictNode(t, "holder", "0.0.0.0", 8443, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`, owner)
  139. ib := sidecarNodeInbound(t, model.AmneziaWG, "awg-fwd", node.Id)
  140. ib.Settings = awgRelayWindowSettingsWithForward(t, "awg-fwd", "8443")
  141. _, _, err := (&InboundService{}).AddInbound(ib)
  142. if !tc.wantErr {
  143. if err != nil {
  144. t.Fatalf("8443 is bound on the master, not on the node; the create must be allowed: %v", err)
  145. }
  146. return
  147. }
  148. if err == nil || !strings.Contains(err.Error(), "forwardedPorts collides with inbound 'holder'") {
  149. t.Fatalf("err = %v, want a forwardedPorts refusal naming 'holder'", err)
  150. }
  151. })
  152. }
  153. }
  154. // awgClientsPayload is the clients-only body the client endpoints take: one
  155. // fresh peer of tag at address, forwarding forwardedPorts.
  156. func awgClientsPayload(t *testing.T, tag, address, forwardedPorts string) string {
  157. t.Helper()
  158. settings := replaceFirst(t, awgRelayWindowSettingsWithForward(t, tag, forwardedPorts),
  159. `"allowedIPs":["10.8.1.2/32"]`, `"allowedIPs":["`+address+`"]`)
  160. var parsed map[string]json.RawMessage
  161. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  162. t.Fatalf("decode fixture: %v", err)
  163. }
  164. return `{"clients":` + string(parsed["clients"]) + `}`
  165. }
  166. // seedNodeAmneziaWGBesideLocalHolder seeds a node AmneziaWG row and a local
  167. // inbound on 8443, the port a forward on the node is free to take.
  168. func seedNodeAmneziaWGBesideLocalHolder(t *testing.T) *model.Inbound {
  169. t.Helper()
  170. setupConflictDB(t)
  171. node := seedVersionedNode(t, "v3.8.0")
  172. seedInboundConflict(t, "holder", "0.0.0.0", 8443, model.VLESS, `{"network":"tcp"}`, `{"clients":[]}`)
  173. seedInboundConflictNode(t, "awg-node", "0.0.0.0", 51820, model.AmneziaWG, ``, awgRelayWindowSettings(t, "awg-node"), &node.Id)
  174. var row model.Inbound
  175. if err := database.GetDB().Where("tag = ?", "awg-node").First(&row).Error; err != nil {
  176. t.Fatalf("read seeded row: %v", err)
  177. }
  178. return &row
  179. }
  180. // The edit payload carries no reliable nodeId, so the forward check must use
  181. // the stored one or it judges a node row against the master's own ports.
  182. func TestUpdateInbound_NodeAmneziaWGForwardIgnoresMasterPorts(t *testing.T) {
  183. row := seedNodeAmneziaWGBesideLocalHolder(t)
  184. update := *row
  185. update.NodeID = nil
  186. update.Settings = replaceFirst(t, row.Settings, `"enable":true`, `"enable":true,"forwardedPorts":"8443"`)
  187. if _, _, err := (&InboundService{}).UpdateInbound(&update); err != nil {
  188. t.Fatalf("8443 is bound on the master, not on the node; the edit must be allowed: %v", err)
  189. }
  190. }
  191. func TestAddInboundClient_NodeAmneziaWGForwardIgnoresMasterPorts(t *testing.T) {
  192. row := seedNodeAmneziaWGBesideLocalHolder(t)
  193. data := &model.Inbound{Id: row.Id, Settings: awgClientsPayload(t, "awg-new", "10.8.1.3/32", "8443")}
  194. if _, err := (&ClientService{}).AddInboundClient(&InboundService{}, data); err != nil {
  195. t.Fatalf("8443 is bound on the master, not on the node; the add must be allowed: %v", err)
  196. }
  197. }
  198. func TestUpdateInboundClient_NodeAmneziaWGForwardIgnoresMasterPorts(t *testing.T) {
  199. row := seedNodeAmneziaWGBesideLocalHolder(t)
  200. var stored struct {
  201. Clients []json.RawMessage `json:"clients"`
  202. }
  203. if err := json.Unmarshal([]byte(row.Settings), &stored); err != nil {
  204. t.Fatalf("decode seeded settings: %v", err)
  205. }
  206. edited := replaceFirst(t, string(stored.Clients[0]), `"enable":true`, `"enable":true,"forwardedPorts":"8443"`)
  207. data := &model.Inbound{Id: row.Id, Settings: `{"clients":[` + edited + `]}`}
  208. if _, err := (&ClientService{}).UpdateInboundClient(&InboundService{}, data, "awg-node@relay-window"); err != nil {
  209. t.Fatalf("8443 is bound on the master, not on the node; the edit must be allowed: %v", err)
  210. }
  211. }
  212. // A relay port derives from the inbound id on the panel running it; the node's
  213. // id differs from the master's, so a master-side derivation names a wrong port.
  214. func TestAddInbound_NodeAmneziaWGForwardIgnoresMasterDerivedRelayPorts(t *testing.T) {
  215. row := seedNodeAmneziaWGBesideLocalHolder(t)
  216. ib := sidecarNodeInbound(t, model.AmneziaWG, "awg-fwd", *row.NodeID)
  217. ib.Port = 51821
  218. ib.Settings = awgRelayWindowSettingsWithForward(t, "awg-fwd", fmt.Sprintf("%d", amneziawgnet.SOCKSPortForInbound(row.Id)))
  219. if _, _, err := (&InboundService{}).AddInbound(ib); err != nil {
  220. t.Fatalf("the master's id for a node row derives no port on the node; the create must be allowed: %v", err)
  221. }
  222. }