inbound.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. package controller
  2. import (
  3. "encoding/json"
  4. "net"
  5. "strconv"
  6. "strings"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/util/wirecodec"
  9. "github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
  10. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  11. "github.com/mhsanaei/3x-ui/v3/internal/web/session"
  12. "github.com/mhsanaei/3x-ui/v3/internal/web/websocket"
  13. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  14. "github.com/gin-gonic/gin"
  15. )
  16. // InboundController handles HTTP requests related to Xray inbounds management.
  17. type InboundController struct {
  18. inboundService service.InboundService
  19. clientService service.ClientService
  20. xrayService service.XrayService
  21. fallbackService service.FallbackService
  22. }
  23. // NewInboundController creates a new InboundController and sets up its routes.
  24. func NewInboundController(g *gin.RouterGroup) *InboundController {
  25. a := &InboundController{}
  26. a.initRouter(g)
  27. return a
  28. }
  29. // broadcastInboundsUpdateClientLimit is the threshold past which we skip the
  30. // full-list push over WebSocket and signal the frontend to re-fetch via REST.
  31. // Mirrors the same heuristic used by the periodic traffic job.
  32. const broadcastInboundsUpdateClientLimit = 5000
  33. // broadcastInboundsUpdate fetches and broadcasts the inbound list for userId.
  34. // At scale (10k+ clients) the marshaled JSON exceeds the WS payload ceiling,
  35. // so we send an invalidate signal instead — frontend re-fetches via REST.
  36. // Skipped entirely when no WebSocket clients are connected.
  37. func (a *InboundController) broadcastInboundsUpdate(userId int) {
  38. if !websocket.HasClients() {
  39. return
  40. }
  41. inbounds, err := a.inboundService.GetInbounds(userId)
  42. if err != nil {
  43. return
  44. }
  45. totalClients := 0
  46. for _, ib := range inbounds {
  47. totalClients += len(ib.ClientStats)
  48. }
  49. if totalClients > broadcastInboundsUpdateClientLimit {
  50. websocket.BroadcastInvalidate(websocket.MessageTypeInbounds)
  51. return
  52. }
  53. websocket.BroadcastInbounds(inbounds)
  54. }
  55. // inboundServiceFor tells the service whether this request is a master's
  56. // node-sync push, so the node stores the row instead of re-judging it.
  57. func (a *InboundController) inboundServiceFor(c *gin.Context) *service.InboundService {
  58. svc := a.inboundService
  59. scope, _ := c.Get("api_token_scope")
  60. // A master enrolled with an admin token (the -getApiToken default) has no
  61. // node-sync scope, so it marks every request it sends instead.
  62. svc.FromNodeSync = scope == model.ApiScopeNodeSync || c.GetHeader(wirecodec.MasterPushHeader) != ""
  63. return &svc
  64. }
  65. // initRouter initializes the routes for inbound-related operations.
  66. func (a *InboundController) initRouter(g *gin.RouterGroup) {
  67. g.GET("/list", a.getInbounds)
  68. g.GET("/list/slim", a.getInboundsSlim)
  69. g.GET("/options", a.getInboundOptions)
  70. g.GET("/allLinks", a.getAllInboundLinks)
  71. g.GET("/get/:id", a.getInbound)
  72. g.GET("/:id/fallbacks", a.getFallbacks)
  73. g.POST("/add", a.addInbound)
  74. g.POST("/del/:id", a.delInbound)
  75. g.POST("/bulkDel", a.bulkDelInbounds)
  76. g.POST("/update/:id", a.updateInbound)
  77. g.POST("/setEnable/:id", a.setInboundEnable)
  78. g.POST("/:id/subSortIndex", a.setInboundSubSortIndex)
  79. g.POST("/:id/resetTraffic", a.resetInboundTraffic)
  80. g.POST("/:id/delAllClients", a.delAllInboundClients)
  81. g.POST("/resetAllTraffics", a.resetAllTraffics)
  82. g.POST("/import", a.importInbound)
  83. g.POST("/:id/fallbacks", a.setFallbacks)
  84. g.POST("/pushClientTraffics", a.pushClientTraffics)
  85. }
  86. // getInbounds retrieves the list of inbounds for the logged-in user.
  87. func (a *InboundController) getInbounds(c *gin.Context) {
  88. user := session.GetLoginUser(c)
  89. inbounds, err := a.inboundService.GetInbounds(user.Id)
  90. if err != nil {
  91. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  92. return
  93. }
  94. jsonObj(c, inbounds, nil)
  95. }
  96. // getInboundsSlim is the list-page variant that strips full client
  97. // payloads from settings.clients[]. Detail-view flows still use /get/:id.
  98. func (a *InboundController) getInboundsSlim(c *gin.Context) {
  99. user := session.GetLoginUser(c)
  100. inbounds, err := a.inboundService.GetInboundsSlim(user.Id)
  101. if err != nil {
  102. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  103. return
  104. }
  105. jsonObj(c, inbounds, nil)
  106. }
  107. // getAllInboundLinks returns every inbound's share links across all clients,
  108. // rendered through the same subscription engine the client pages use so the
  109. // remark template (name-only display part) is applied consistently.
  110. func (a *InboundController) getAllInboundLinks(c *gin.Context) {
  111. user := session.GetLoginUser(c)
  112. links, err := a.inboundService.GetAllInboundLinks(resolveHost(c), user.Id)
  113. if err != nil {
  114. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  115. return
  116. }
  117. jsonObj(c, links, nil)
  118. }
  119. // getInboundOptions returns a lightweight projection of the user's inbounds
  120. // (id, remark, protocol, port, tlsFlowCapable) for pickers in the clients UI.
  121. // Avoids shipping per-client settings and traffic stats just to fill a dropdown.
  122. func (a *InboundController) getInboundOptions(c *gin.Context) {
  123. user := session.GetLoginUser(c)
  124. options, err := a.inboundService.GetInboundOptions(user.Id)
  125. if err != nil {
  126. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  127. return
  128. }
  129. jsonObj(c, options, nil)
  130. }
  131. // getInbound retrieves a specific inbound by its ID.
  132. func (a *InboundController) getInbound(c *gin.Context) {
  133. id, err := strconv.Atoi(c.Param("id"))
  134. if err != nil {
  135. jsonMsg(c, I18nWeb(c, "get"), err)
  136. return
  137. }
  138. inbound, err := a.inboundService.GetInboundDetail(id)
  139. if err != nil {
  140. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.obtain"), err)
  141. return
  142. }
  143. jsonObj(c, inbound, nil)
  144. }
  145. // addInbound creates a new inbound configuration.
  146. func (a *InboundController) addInbound(c *gin.Context) {
  147. inbound, ok := middleware.BindAndValidate[model.Inbound](c)
  148. if !ok {
  149. return
  150. }
  151. user := session.GetLoginUser(c)
  152. inbound.UserId = user.Id
  153. // Treat NodeID=0 as "no node" — gin's *int form binding can land on
  154. // 0 when the field is absent or empty, and 0 is never a valid Node
  155. // row id. Without this normalization the runtime layer would try to
  156. // load Node id=0 and surface "record not found".
  157. if inbound.NodeID != nil && *inbound.NodeID == 0 {
  158. inbound.NodeID = nil
  159. }
  160. inbound, needRestart, err := a.inboundServiceFor(c).AddInbound(inbound)
  161. if err != nil {
  162. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  163. return
  164. }
  165. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
  166. if needRestart {
  167. a.xrayService.SetToNeedRestart()
  168. }
  169. a.broadcastInboundsUpdate(user.Id)
  170. notifyClientsChanged()
  171. }
  172. // delInbound deletes an inbound configuration by its ID.
  173. func (a *InboundController) delInbound(c *gin.Context) {
  174. id, err := strconv.Atoi(c.Param("id"))
  175. if err != nil {
  176. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
  177. return
  178. }
  179. needRestart, err := a.inboundService.DelInbound(id)
  180. if err != nil {
  181. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  182. return
  183. }
  184. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), id, nil)
  185. if needRestart {
  186. a.xrayService.SetToNeedRestart()
  187. }
  188. user := session.GetLoginUser(c)
  189. a.broadcastInboundsUpdate(user.Id)
  190. notifyClientsChanged()
  191. }
  192. type bulkDelInboundsRequest struct {
  193. Ids []int `json:"ids"`
  194. }
  195. // bulkDelInbounds deletes several inbounds in one call. Failures are
  196. // reported per id and the rest still proceed; xray restarts at most once.
  197. func (a *InboundController) bulkDelInbounds(c *gin.Context) {
  198. var req bulkDelInboundsRequest
  199. if err := c.ShouldBindJSON(&req); err != nil {
  200. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  201. return
  202. }
  203. result, needRestart, err := a.inboundService.DelInbounds(req.Ids)
  204. if err != nil {
  205. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  206. return
  207. }
  208. jsonObj(c, result, nil)
  209. if needRestart {
  210. a.xrayService.SetToNeedRestart()
  211. }
  212. user := session.GetLoginUser(c)
  213. a.broadcastInboundsUpdate(user.Id)
  214. notifyClientsChanged()
  215. }
  216. // updateInbound updates an existing inbound configuration.
  217. func (a *InboundController) updateInbound(c *gin.Context) {
  218. id, err := strconv.Atoi(c.Param("id"))
  219. if err != nil {
  220. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
  221. return
  222. }
  223. inbound := &model.Inbound{
  224. Id: id,
  225. }
  226. if !middleware.BindAndValidateInto(c, inbound) {
  227. return
  228. }
  229. // Same NodeID=0 → nil normalisation as addInbound. UpdateInbound
  230. // loads the existing row's NodeID from DB anyway (Phase 1 doesn't
  231. // support migrating an inbound between nodes), but normalising here
  232. // keeps the wire shape consistent.
  233. if inbound.NodeID != nil && *inbound.NodeID == 0 {
  234. inbound.NodeID = nil
  235. }
  236. inbound, needRestart, err := a.inboundServiceFor(c).UpdateInbound(inbound)
  237. if err != nil {
  238. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  239. return
  240. }
  241. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), inbound, nil)
  242. if needRestart {
  243. a.xrayService.SetToNeedRestart()
  244. }
  245. user := session.GetLoginUser(c)
  246. a.broadcastInboundsUpdate(user.Id)
  247. notifyClientsChanged()
  248. }
  249. // setInboundSubSortIndex changes only subscription ordering without sending
  250. // the inbound's settings/client payload.
  251. func (a *InboundController) setInboundSubSortIndex(c *gin.Context) {
  252. id, err := strconv.Atoi(c.Param("id"))
  253. if err != nil {
  254. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
  255. return
  256. }
  257. type form struct {
  258. SubSortIndex int `json:"subSortIndex" form:"subSortIndex" binding:"required"`
  259. }
  260. var f form
  261. if err := c.ShouldBind(&f); err != nil {
  262. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  263. return
  264. }
  265. if err := a.inboundService.SetInboundSubSortIndex(id, f.SubSortIndex); err != nil {
  266. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  267. return
  268. }
  269. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
  270. websocket.BroadcastInvalidate(websocket.MessageTypeInbounds)
  271. }
  272. func (a *InboundController) setInboundEnable(c *gin.Context) {
  273. id, err := strconv.Atoi(c.Param("id"))
  274. if err != nil {
  275. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
  276. return
  277. }
  278. type form struct {
  279. Enable bool `json:"enable" form:"enable"`
  280. }
  281. var f form
  282. if err := c.ShouldBind(&f); err != nil {
  283. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  284. return
  285. }
  286. needRestart, err := a.inboundService.SetInboundEnable(id, f.Enable)
  287. if err != nil {
  288. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  289. return
  290. }
  291. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
  292. if needRestart {
  293. a.xrayService.SetToNeedRestart()
  294. }
  295. // Cross-admin sync: lightweight invalidate signal (a few hundred bytes)
  296. // instead of fetching + serialising the whole inbound list. Other open
  297. // sessions re-fetch via REST. The toggling admin's own UI already
  298. // updated optimistically.
  299. websocket.BroadcastInvalidate(websocket.MessageTypeInbounds)
  300. }
  301. // resetInboundTraffic resets traffic counters for a specific inbound.
  302. func (a *InboundController) resetInboundTraffic(c *gin.Context) {
  303. id, err := strconv.Atoi(c.Param("id"))
  304. if err != nil {
  305. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
  306. return
  307. }
  308. err = a.inboundService.ResetInboundTraffic(id)
  309. if err != nil {
  310. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  311. return
  312. } else {
  313. a.xrayService.SetToNeedRestart()
  314. }
  315. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetInboundTrafficSuccess"), nil)
  316. }
  317. // delAllInboundClients removes every client attached to a specific inbound
  318. // while keeping the inbound itself. Internally collects the current email
  319. // list from settings.clients[] and feeds it into ClientService.BulkDelete,
  320. // which handles per-inbound JSON rewriting, runtime user removal, traffic
  321. // row cleanup, and the SyncInbound mapping pass in one optimized cycle.
  322. func (a *InboundController) delAllInboundClients(c *gin.Context) {
  323. id, err := strconv.Atoi(c.Param("id"))
  324. if err != nil {
  325. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  326. return
  327. }
  328. emails, err := a.inboundService.EmailsByInbound(id)
  329. if err != nil {
  330. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  331. return
  332. }
  333. if len(emails) == 0 {
  334. jsonObj(c, service.BulkDeleteResult{}, nil)
  335. return
  336. }
  337. result, needRestart, err := a.clientService.BulkDelete(&a.inboundService, emails, false)
  338. if err != nil {
  339. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  340. return
  341. }
  342. jsonObj(c, result, nil)
  343. if needRestart {
  344. a.xrayService.SetToNeedRestart()
  345. }
  346. user := session.GetLoginUser(c)
  347. a.broadcastInboundsUpdate(user.Id)
  348. notifyClientsChanged()
  349. }
  350. // resetAllTraffics resets all traffic counters across all inbounds.
  351. func (a *InboundController) resetAllTraffics(c *gin.Context) {
  352. err := a.inboundService.ResetAllTraffics()
  353. if err != nil {
  354. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  355. return
  356. } else {
  357. a.xrayService.SetToNeedRestart()
  358. }
  359. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.resetAllTrafficSuccess"), nil)
  360. }
  361. // pushClientTraffics receives a master panel's aggregated per-client usage
  362. // (see InboundService.AcceptGlobalTraffic for the storage semantics).
  363. func (a *InboundController) pushClientTraffics(c *gin.Context) {
  364. var req struct {
  365. MasterGuid string `json:"masterGuid"`
  366. Traffics []*xray.ClientTraffic `json:"traffics"`
  367. }
  368. if err := c.ShouldBindJSON(&req); err != nil {
  369. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  370. return
  371. }
  372. if err := a.inboundService.AcceptGlobalTraffic(req.MasterGuid, req.Traffics); err != nil {
  373. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  374. return
  375. }
  376. jsonMsg(c, "success", nil)
  377. }
  378. // importInbound imports an inbound configuration from provided data.
  379. func (a *InboundController) importInbound(c *gin.Context) {
  380. inbound := &model.Inbound{}
  381. err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
  382. if err != nil {
  383. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  384. return
  385. }
  386. user := session.GetLoginUser(c)
  387. inbound.Id = 0
  388. inbound.UserId = user.Id
  389. // Node IDs are panel-local and not portable across panels. Drop a node
  390. // reference that is zero or that points to a node which doesn't exist on
  391. // this panel, so a cross-panel export imports as a local inbound instead of
  392. // failing with "record not found" when nodePushPlan looks the node up.
  393. if inbound.NodeID != nil {
  394. if *inbound.NodeID == 0 {
  395. inbound.NodeID = nil
  396. } else if exists, err := (&service.NodeService{}).NodeExists(*inbound.NodeID); err == nil && !exists {
  397. inbound.NodeID = nil
  398. }
  399. }
  400. for index := range inbound.ClientStats {
  401. inbound.ClientStats[index].Id = 0
  402. inbound.ClientStats[index].Enable = true
  403. }
  404. inbound, needRestart, err := a.inboundService.AddInbound(inbound)
  405. if err != nil {
  406. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  407. return
  408. }
  409. jsonMsgObj(c, I18nWeb(c, "pages.inbounds.toasts.inboundCreateSuccess"), inbound, nil)
  410. if needRestart {
  411. a.xrayService.SetToNeedRestart()
  412. }
  413. a.broadcastInboundsUpdate(user.Id)
  414. notifyClientsChanged()
  415. }
  416. // resolveHost mirrors SubService.ResolveRequest's host: trusted X-Forwarded-Host,
  417. // else the dialed request Host. X-Real-IP names the visitor, not the panel (#6589).
  418. func resolveHost(c *gin.Context) string {
  419. if isTrustedForwardedRequest(c) {
  420. if h := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); h != "" {
  421. if i := strings.Index(h, ","); i >= 0 {
  422. h = strings.TrimSpace(h[:i])
  423. }
  424. if hp, _, err := net.SplitHostPort(h); err == nil {
  425. return hp
  426. }
  427. return h
  428. }
  429. }
  430. if h, _, err := net.SplitHostPort(c.Request.Host); err == nil {
  431. return h
  432. }
  433. return c.Request.Host
  434. }
  435. // getFallbacks returns the fallback rules attached to the master inbound.
  436. func (a *InboundController) getFallbacks(c *gin.Context) {
  437. id, err := strconv.Atoi(c.Param("id"))
  438. if err != nil {
  439. jsonMsg(c, I18nWeb(c, "get"), err)
  440. return
  441. }
  442. rows, err := a.fallbackService.GetByMaster(id)
  443. if err != nil {
  444. jsonMsg(c, I18nWeb(c, "get"), err)
  445. return
  446. }
  447. jsonObj(c, rows, nil)
  448. }
  449. // setFallbacks atomically replaces the master inbound's fallback list
  450. // and triggers an Xray restart so the new settings.fallbacks take effect.
  451. func (a *InboundController) setFallbacks(c *gin.Context) {
  452. id, err := strconv.Atoi(c.Param("id"))
  453. if err != nil {
  454. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  455. return
  456. }
  457. type body struct {
  458. Fallbacks []service.FallbackInput `json:"fallbacks"`
  459. }
  460. var b body
  461. if err := c.ShouldBindJSON(&b); err != nil {
  462. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  463. return
  464. }
  465. if err := a.fallbackService.SetByMaster(id, b.Fallbacks); err != nil {
  466. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  467. return
  468. }
  469. a.xrayService.SetToNeedRestart()
  470. jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), nil)
  471. }