node.go 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352
  1. package service
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "crypto/tls"
  6. "encoding/base64"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "net"
  11. "net/http"
  12. "net/url"
  13. "slices"
  14. "strconv"
  15. "strings"
  16. "sync"
  17. "time"
  18. "github.com/mhsanaei/3x-ui/v3/internal/crypto/nodetoken"
  19. "github.com/mhsanaei/3x-ui/v3/internal/database"
  20. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  21. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  22. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  23. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  24. "github.com/mhsanaei/3x-ui/v3/internal/util/netsafe"
  25. "github.com/mhsanaei/3x-ui/v3/internal/web/runtime"
  26. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  27. "gorm.io/gorm"
  28. )
  29. type HeartbeatPatch struct {
  30. Status string
  31. LastHeartbeat int64
  32. LatencyMs int
  33. XrayVersion string
  34. PanelVersion string
  35. Guid string
  36. CpuPct float64
  37. MemPct float64
  38. UptimeSecs uint64
  39. // NetUp/NetDown are the node's current interface throughput (bytes/sec),
  40. // summed over non-virtual interfaces, read from its status response.
  41. NetUp uint64
  42. NetDown uint64
  43. LastError string
  44. // XrayState and XrayError come from the remote /panel/api/server/status when the
  45. // panel API is reachable. They allow distinguishing panel connectivity from
  46. // Xray core health on the node.
  47. XrayState string
  48. XrayError string
  49. }
  50. type NodeService struct{}
  51. // FetchCertFingerprint connects to the node over HTTPS without verifying the
  52. // certificate and returns the leaf certificate's SHA-256 as base64, so the UI
  53. // can offer a "fetch and pin current certificate" action.
  54. func (s *NodeService) FetchCertFingerprint(ctx context.Context, n *model.Node) (string, error) {
  55. addr, err := netsafe.NormalizeHost(n.Address)
  56. if err != nil {
  57. return "", err
  58. }
  59. scheme := n.Scheme
  60. if scheme != "http" && scheme != "https" {
  61. scheme = "https"
  62. }
  63. if scheme != "https" {
  64. return "", common.NewError("certificate pinning is only available for https nodes")
  65. }
  66. if n.Port <= 0 || n.Port > 65535 {
  67. return "", common.NewError("node port must be 1-65535")
  68. }
  69. probeURL := &url.URL{
  70. Scheme: scheme,
  71. Host: net.JoinHostPort(addr, strconv.Itoa(n.Port)),
  72. Path: normalizeBasePath(n.BasePath) + "panel/api/server/status",
  73. }
  74. req, err := http.NewRequestWithContext(
  75. netsafe.ContextWithAllowPrivate(ctx, n.AllowPrivateAddress),
  76. http.MethodGet, probeURL.String(), nil)
  77. if err != nil {
  78. return "", err
  79. }
  80. client := &http.Client{
  81. Transport: &http.Transport{
  82. DialContext: netsafe.SSRFGuardedDialContext,
  83. TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // lgtm[go/disabled-certificate-check]
  84. },
  85. }
  86. resp, err := client.Do(req)
  87. if err != nil {
  88. return "", err
  89. }
  90. defer resp.Body.Close()
  91. if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
  92. return "", common.NewError("node did not present a TLS certificate")
  93. }
  94. sum := sha256.Sum256(resp.TLS.PeerCertificates[0].Raw)
  95. return base64.StdEncoding.EncodeToString(sum[:]), nil
  96. }
  97. // decryptToken exposes plaintext to callers. Failures blank only this token
  98. // and surface through LastError instead of dropping the node row.
  99. func decryptToken(n *model.Node) {
  100. if n == nil || n.ApiToken == "" {
  101. return
  102. }
  103. pt, err := nodetoken.Decrypt(n.Id, n.ApiToken)
  104. if err != nil {
  105. n.ApiToken = ""
  106. if n.LastError == "" {
  107. n.LastError = "token decrypt failed: " + err.Error()
  108. }
  109. return
  110. }
  111. n.ApiToken = pt
  112. }
  113. func (s *NodeService) GetAll() ([]*model.Node, error) {
  114. db := database.GetDB()
  115. var nodes []*model.Node
  116. err := db.Model(model.Node{}).Order("id asc").Find(&nodes).Error
  117. if err != nil || len(nodes) == 0 {
  118. return nodes, err
  119. }
  120. for _, n := range nodes {
  121. decryptToken(n)
  122. }
  123. type inboundRow struct {
  124. Id int
  125. NodeID int `gorm:"column:node_id"`
  126. }
  127. var inboundRows []inboundRow
  128. if err := db.Table("inbounds").
  129. Select("id, node_id").
  130. Where("node_id IS NOT NULL").
  131. Scan(&inboundRows).Error; err != nil {
  132. return nodes, nil
  133. }
  134. if len(inboundRows) == 0 {
  135. return nodes, nil
  136. }
  137. inboundsByNode := make(map[int][]int, len(nodes))
  138. for _, row := range inboundRows {
  139. inboundsByNode[row.NodeID] = append(inboundsByNode[row.NodeID], row.Id)
  140. }
  141. type clientCountRow struct {
  142. NodeID int `gorm:"column:node_id"`
  143. Count int `gorm:"column:count"`
  144. }
  145. var clientCounts []clientCountRow
  146. if err := db.Raw(`
  147. SELECT inbounds.node_id AS node_id, COUNT(DISTINCT client_inbounds.client_id) AS count
  148. FROM inbounds
  149. JOIN client_inbounds ON client_inbounds.inbound_id = inbounds.id
  150. WHERE inbounds.node_id IS NOT NULL
  151. GROUP BY inbounds.node_id
  152. `).Scan(&clientCounts).Error; err == nil {
  153. for _, row := range clientCounts {
  154. for _, n := range nodes {
  155. if n.Id == row.NodeID {
  156. n.ClientCount = row.Count
  157. break
  158. }
  159. }
  160. }
  161. }
  162. depletedByNode := make(map[int]int)
  163. disabledByNode := make(map[int]int)
  164. activeByNode := make(map[int]int)
  165. statuses, _ := s.nodeClientStatuses()
  166. seen := make(map[int]map[int]struct{}, len(nodes))
  167. for _, st := range statuses {
  168. clientsSeen := seen[st.NodeID]
  169. if clientsSeen == nil {
  170. clientsSeen = make(map[int]struct{})
  171. seen[st.NodeID] = clientsSeen
  172. }
  173. if _, dup := clientsSeen[st.ClientID]; dup {
  174. // A client attached to several inbounds of one node counts once,
  175. // matching the distinct ClientCount above.
  176. continue
  177. }
  178. clientsSeen[st.ClientID] = struct{}{}
  179. switch {
  180. case st.Depleted:
  181. depletedByNode[st.NodeID]++
  182. case st.Disabled:
  183. disabledByNode[st.NodeID]++
  184. default:
  185. activeByNode[st.NodeID]++
  186. }
  187. }
  188. onlineByGuid := s.onlineEmailsByGuid()
  189. selfGuid, _ := (&SettingService{}).GetPanelGuid()
  190. ambiguous := ambiguousNodeGuids(nodes, selfGuid)
  191. for _, n := range nodes {
  192. n.InboundCount = len(inboundsByNode[n.Id])
  193. n.DepletedCount = depletedByNode[n.Id]
  194. n.DisabledCount = disabledByNode[n.Id]
  195. n.ActiveCount = activeByNode[n.Id]
  196. // Online is attributed to the node that physically hosts the client
  197. // (by GUID): a client on a sub-node counts under the sub-node, not
  198. // the intermediate node it syncs through (#4983).
  199. n.OnlineCount = len(onlineByGuid[effectiveNodeGuid(n, ambiguous)])
  200. }
  201. return nodes, nil
  202. }
  203. // nodeClientStatus is one node-hosted client's classification, carrying enough
  204. // identity for callers to bucket it by node id or by attribution GUID.
  205. type nodeClientStatus struct {
  206. InboundID int
  207. NodeID int
  208. ClientID int
  209. Depleted bool
  210. Disabled bool
  211. }
  212. // nodeClientStatuses classifies every client attached to a node-hosted inbound as
  213. // depleted / disabled / active, matching client_traffics by EMAIL rather than by
  214. // inbound_id. client_traffics.inbound_id goes stale after an inbound is
  215. // delete+recreated, so filtering by it silently drops most rows; the
  216. // client_inbounds -> clients join is the reliable client set and the email join
  217. // pulls each client's live counters. Precedence matches the inbound page:
  218. // depleted (expired/exhausted) wins over disabled.
  219. func (s *NodeService) nodeClientStatuses() ([]nodeClientStatus, error) {
  220. type row struct {
  221. InboundID int `gorm:"column:inbound_id"`
  222. NodeID int `gorm:"column:node_id"`
  223. ClientID int `gorm:"column:client_id"`
  224. Enable bool `gorm:"column:enable"`
  225. Total int64 `gorm:"column:total"`
  226. Up int64 `gorm:"column:up"`
  227. Down int64 `gorm:"column:down"`
  228. ExpiryTime int64 `gorm:"column:expiry_time"`
  229. }
  230. var rows []row
  231. if err := database.GetDB().Table("inbounds").
  232. Select("inbounds.id AS inbound_id, inbounds.node_id AS node_id, clients.id AS client_id, " +
  233. "clients.enable AS enable, ct.total AS total, ct.up AS up, ct.down AS down, ct.expiry_time AS expiry_time").
  234. Joins("JOIN client_inbounds ON client_inbounds.inbound_id = inbounds.id").
  235. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  236. Joins("LEFT JOIN client_traffics ct ON ct.email = clients.email").
  237. Where("inbounds.node_id IS NOT NULL").
  238. Scan(&rows).Error; err != nil {
  239. return nil, err
  240. }
  241. now := time.Now().UnixMilli()
  242. out := make([]nodeClientStatus, 0, len(rows))
  243. for _, r := range rows {
  244. st := nodeClientStatus{InboundID: r.InboundID, NodeID: r.NodeID, ClientID: r.ClientID}
  245. expired := r.ExpiryTime > 0 && r.ExpiryTime <= now
  246. exhausted := r.Total > 0 && r.Up+r.Down >= r.Total
  247. switch {
  248. case expired || exhausted:
  249. st.Depleted = true
  250. case !r.Enable:
  251. st.Disabled = true
  252. }
  253. out = append(out, st)
  254. }
  255. return out, nil
  256. }
  257. func (s *NodeService) onlineEmailsByGuid() map[string]map[string]struct{} {
  258. svc := InboundService{}
  259. byGuid := svc.GetOnlineClientsByGuid()
  260. out := make(map[string]map[string]struct{}, len(byGuid))
  261. for guid, emails := range byGuid {
  262. set := make(map[string]struct{}, len(emails))
  263. for _, email := range emails {
  264. set[email] = struct{}{}
  265. }
  266. out[guid] = set
  267. }
  268. return out
  269. }
  270. // effectiveNodeGuid is a node's stable online/inbound attribution key: its
  271. // reported panelGuid, or a master-local synthetic node-id fallback when the node
  272. // has no GUID yet (old build) or its GUID is ambiguous. ambiguous comes from
  273. // ambiguousNodeGuids.
  274. func effectiveNodeGuid(n *model.Node, ambiguous map[string]struct{}) string {
  275. if n.Guid == "" {
  276. return synthNodeGuid(n.Id)
  277. }
  278. if n.Id > 0 {
  279. if _, bad := ambiguous[n.Guid]; bad {
  280. return synthNodeGuid(n.Id)
  281. }
  282. }
  283. return n.Guid
  284. }
  285. // ambiguousNodeGuids returns the panelGuids a node must not be attributed under
  286. // directly, because doing so would merge two distinct identities: a GUID
  287. // reported by more than one of this master's direct nodes (cloned node servers
  288. // ship the same panelGuid in their copied settings), or a GUID equal to the
  289. // master's own panelGuid (a node cloned from the master). A node holding such a
  290. // GUID falls back to its node-unique synthNodeGuid. Transitive sub-nodes (Id 0)
  291. // carry distinct descendant GUIDs by construction and are excluded.
  292. func ambiguousNodeGuids(nodes []*model.Node, selfGuid string) map[string]struct{} {
  293. counts := make(map[string]int, len(nodes))
  294. for _, n := range nodes {
  295. if n.Id > 0 && n.Guid != "" {
  296. counts[n.Guid]++
  297. }
  298. }
  299. ambiguous := make(map[string]struct{})
  300. for guid, c := range counts {
  301. if c > 1 {
  302. ambiguous[guid] = struct{}{}
  303. }
  304. }
  305. if selfGuid != "" {
  306. if _, ok := counts[selfGuid]; ok {
  307. ambiguous[selfGuid] = struct{}{}
  308. }
  309. }
  310. return ambiguous
  311. }
  312. // effectiveNodeKey returns one node's attribution key without a preloaded node
  313. // list — its panelGuid when that GUID uniquely identifies it among the master's
  314. // nodes and differs from the master's own, otherwise its node-unique
  315. // synthNodeGuid. Same rule as effectiveNodeGuid + ambiguousNodeGuids, for the
  316. // write paths that handle a single node (online tree, IP attribution).
  317. func effectiveNodeKey(node *model.Node) string {
  318. if node == nil {
  319. return ""
  320. }
  321. if node.Guid == "" {
  322. return synthNodeGuid(node.Id)
  323. }
  324. var sameGuid int64
  325. database.GetDB().Model(&model.Node{}).Where("guid = ?", node.Guid).Count(&sameGuid)
  326. masterGuid, _ := (&SettingService{}).GetPanelGuid()
  327. if sameGuid > 1 || node.Guid == masterGuid {
  328. return synthNodeGuid(node.Id)
  329. }
  330. return node.Guid
  331. }
  332. func (s *NodeService) GetById(id int) (*model.Node, error) {
  333. db := database.GetDB()
  334. n := &model.Node{}
  335. if err := db.Model(model.Node{}).Where("id = ?", id).First(n).Error; err != nil {
  336. return nil, err
  337. }
  338. decryptToken(n)
  339. return n, nil
  340. }
  341. func (s *NodeService) GetViewById(id int) (*NodeView, error) {
  342. n, err := s.GetById(id)
  343. if err != nil {
  344. return nil, err
  345. }
  346. return toNodeView(n), nil
  347. }
  348. // NodeExists reports whether a node with the given id exists on this panel.
  349. // Used to drop stale, cross-panel node references on inbound import. A Count
  350. // query distinguishes "no such node" (count 0, no error) from a real DB error.
  351. func (s *NodeService) NodeExists(id int) (bool, error) {
  352. if id <= 0 {
  353. return false, nil
  354. }
  355. var count int64
  356. if err := database.GetDB().Model(model.Node{}).Where("id = ?", id).Count(&count).Error; err != nil {
  357. return false, err
  358. }
  359. return count > 0, nil
  360. }
  361. func normalizeBasePath(p string) string {
  362. p = strings.TrimSpace(p)
  363. if p == "" {
  364. return "/"
  365. }
  366. if !strings.HasPrefix(p, "/") {
  367. p = "/" + p
  368. }
  369. if !strings.HasSuffix(p, "/") {
  370. p = p + "/"
  371. }
  372. return p
  373. }
  374. func (s *NodeService) normalize(n *model.Node) error {
  375. n.Name = strings.TrimSpace(n.Name)
  376. n.ApiToken = strings.TrimSpace(n.ApiToken)
  377. if n.Name == "" {
  378. return common.NewError("node name is required")
  379. }
  380. addr, err := netsafe.NormalizeHost(n.Address)
  381. if err != nil {
  382. return common.NewError(err.Error())
  383. }
  384. n.Address = addr
  385. if n.Port <= 0 || n.Port > 65535 {
  386. return common.NewError("node port must be 1-65535")
  387. }
  388. if n.Scheme != "http" && n.Scheme != "https" {
  389. n.Scheme = "https"
  390. }
  391. if n.TlsVerifyMode != "skip" && n.TlsVerifyMode != "pin" && n.TlsVerifyMode != "mtls" {
  392. n.TlsVerifyMode = "verify"
  393. }
  394. if n.TlsVerifyMode == "mtls" && n.Scheme != "https" {
  395. return common.NewError("mtls requires the node scheme to be https")
  396. }
  397. n.PinnedCertSha256 = strings.TrimSpace(n.PinnedCertSha256)
  398. if n.InboundSyncMode != "selected" {
  399. n.InboundSyncMode = "all"
  400. n.InboundTags = nil
  401. } else {
  402. seen := make(map[string]struct{}, len(n.InboundTags))
  403. tags := make([]string, 0, len(n.InboundTags))
  404. for _, tag := range n.InboundTags {
  405. tag = strings.TrimSpace(tag)
  406. if tag == "" {
  407. continue
  408. }
  409. if _, ok := seen[tag]; ok {
  410. continue
  411. }
  412. seen[tag] = struct{}{}
  413. tags = append(tags, tag)
  414. }
  415. n.InboundTags = tags
  416. }
  417. if n.TlsVerifyMode == "pin" {
  418. if _, err := runtime.DecodeCertPin(n.PinnedCertSha256); err != nil {
  419. return common.NewError(err.Error())
  420. }
  421. }
  422. n.BasePath = normalizeBasePath(n.BasePath)
  423. return nil
  424. }
  425. func (s *NodeService) Create(n *model.Node) error {
  426. if err := s.normalize(n); err != nil {
  427. return err
  428. }
  429. db := database.GetDB()
  430. if !nodetoken.Enabled() {
  431. return db.Create(n).Error
  432. }
  433. plaintext := n.ApiToken
  434. return db.Transaction(func(tx *gorm.DB) error {
  435. // The id-bound ciphertext can only be produced after insertion. Never put
  436. // plaintext in the initial tuple: PostgreSQL WAL would retain it.
  437. n.ApiToken = ""
  438. defer func() { n.ApiToken = plaintext }()
  439. if err := tx.Create(n).Error; err != nil {
  440. return err
  441. }
  442. enc, err := nodetoken.Encrypt(n.Id, plaintext)
  443. if err != nil {
  444. return err
  445. }
  446. if enc == plaintext {
  447. return nil // off-mode / empty token: nothing to rewrite
  448. }
  449. // DB column gets ciphertext; the in-memory struct keeps plaintext so the
  450. // create response echoes the same usable value GetById would return.
  451. return tx.Model(model.Node{}).Where("id = ?", n.Id).Update("api_token", enc).Error
  452. })
  453. }
  454. func (s *NodeService) CreateFromRequest(req *NodeMutationRequest) (*NodeView, error) {
  455. if err := req.validateCredentials(true); err != nil {
  456. return nil, err
  457. }
  458. n := req.toNode()
  459. if err := s.Create(n); err != nil {
  460. return nil, err
  461. }
  462. return toNodeView(n), nil
  463. }
  464. func (s *NodeService) Update(id int, in *model.Node) error {
  465. if err := s.normalize(in); err != nil {
  466. return err
  467. }
  468. inboundTagsJSON, err := json.Marshal(in.InboundTags)
  469. if err != nil {
  470. return err
  471. }
  472. db := database.GetDB()
  473. existing := &model.Node{}
  474. if err := db.Where("id = ?", id).First(existing).Error; err != nil {
  475. return err
  476. }
  477. // Blank means keep the hidden stored token; non-blank values are encrypted.
  478. apiToken := existing.ApiToken
  479. if in.ApiToken != "" {
  480. enc, eerr := nodetoken.Encrypt(id, in.ApiToken)
  481. if eerr != nil {
  482. return eerr
  483. }
  484. apiToken = enc
  485. }
  486. updates := map[string]any{
  487. "name": in.Name,
  488. "remark": in.Remark,
  489. "scheme": in.Scheme,
  490. "address": in.Address,
  491. "port": in.Port,
  492. "base_path": in.BasePath,
  493. "api_token": apiToken,
  494. "enable": in.Enable,
  495. "allow_private_address": in.AllowPrivateAddress,
  496. "tls_verify_mode": in.TlsVerifyMode,
  497. "pinned_cert_sha256": in.PinnedCertSha256,
  498. "inbound_sync_mode": in.InboundSyncMode,
  499. "inbound_tags": string(inboundTagsJSON),
  500. "outbound_tag": in.OutboundTag,
  501. }
  502. if err := db.Transaction(func(tx *gorm.DB) error {
  503. if err := tx.Model(model.Node{}).Where("id = ?", id).Updates(updates).Error; err != nil {
  504. return err
  505. }
  506. return s.MarkNodeDirtyTx(tx, id)
  507. }); err != nil {
  508. return err
  509. }
  510. if mgr := runtime.GetManager(); mgr != nil {
  511. mgr.InvalidateNode(id)
  512. }
  513. return nil
  514. }
  515. func (s *NodeService) UpdateFromRequest(id int, req *NodeMutationRequest) error {
  516. if err := req.validateCredentials(false); err != nil {
  517. return err
  518. }
  519. in := req.toNode()
  520. if err := s.normalize(in); err != nil {
  521. return err
  522. }
  523. inboundTagsJSON, err := json.Marshal(in.InboundTags)
  524. if err != nil {
  525. return err
  526. }
  527. db := database.GetDB()
  528. existing := &model.Node{}
  529. if err := db.Where("id = ?", id).First(existing).Error; err != nil {
  530. return err
  531. }
  532. apiToken := existing.ApiToken
  533. switch {
  534. case req.ClearApiToken:
  535. apiToken = ""
  536. case req.ApiToken != nil:
  537. apiToken, err = nodetoken.Encrypt(id, *req.ApiToken)
  538. if err != nil {
  539. return err
  540. }
  541. }
  542. if apiToken == "" && in.Enable && in.TlsVerifyMode != "mtls" {
  543. return common.NewError("apiToken is required unless mtls is enabled")
  544. }
  545. updates := map[string]any{
  546. "name": in.Name,
  547. "remark": in.Remark,
  548. "scheme": in.Scheme,
  549. "address": in.Address,
  550. "port": in.Port,
  551. "base_path": in.BasePath,
  552. "api_token": apiToken,
  553. "enable": in.Enable,
  554. "allow_private_address": in.AllowPrivateAddress,
  555. "tls_verify_mode": in.TlsVerifyMode,
  556. "pinned_cert_sha256": in.PinnedCertSha256,
  557. "inbound_sync_mode": in.InboundSyncMode,
  558. "inbound_tags": string(inboundTagsJSON),
  559. "outbound_tag": in.OutboundTag,
  560. }
  561. if err := db.Transaction(func(tx *gorm.DB) error {
  562. if err := tx.Model(model.Node{}).Where("id = ?", id).Updates(updates).Error; err != nil {
  563. return err
  564. }
  565. return s.MarkNodeDirtyTx(tx, id)
  566. }); err != nil {
  567. return err
  568. }
  569. if mgr := runtime.GetManager(); mgr != nil {
  570. mgr.InvalidateNode(id)
  571. }
  572. return nil
  573. }
  574. func (s *NodeService) RuntimeNodeFromRequest(id int, req *NodeMutationRequest) (*model.Node, error) {
  575. if err := req.validateCredentials(id == 0); err != nil {
  576. return nil, err
  577. }
  578. var n *model.Node
  579. if id > 0 {
  580. existing, err := s.GetById(id)
  581. if err != nil {
  582. return nil, err
  583. }
  584. n = existing
  585. } else {
  586. n = &model.Node{}
  587. }
  588. overlay := req.toNode()
  589. overlay.Id = id
  590. if req.ApiToken == nil {
  591. overlay.ApiToken = n.ApiToken
  592. }
  593. if req.ClearApiToken {
  594. overlay.ApiToken = ""
  595. }
  596. *n = *overlay
  597. if err := s.normalize(n); err != nil {
  598. return nil, err
  599. }
  600. if n.ApiToken == "" && n.Enable && n.TlsVerifyMode != "mtls" {
  601. return nil, common.NewError("apiToken is required unless mtls is enabled")
  602. }
  603. return n, nil
  604. }
  605. func (s *NodeService) NodeFromRequestForCertificate(req *NodeMutationRequest) (*model.Node, error) {
  606. if req == nil {
  607. return nil, common.NewError("node request is required")
  608. }
  609. n := req.toNode()
  610. if n.Scheme == "" {
  611. n.Scheme = "https"
  612. }
  613. if n.BasePath == "" {
  614. n.BasePath = "/"
  615. }
  616. if err := s.normalize(n); err != nil {
  617. return nil, err
  618. }
  619. return n, nil
  620. }
  621. // MigrateNodeTokensToActiveKey uses compare-and-swap to avoid clobbering live
  622. // changes. Current-key rows are skipped; changed and skipped counts are returned.
  623. func (s *NodeService) MigrateNodeTokensToActiveKey() (int, int, error) {
  624. codec := nodetoken.Active()
  625. if !codec.Enabled() {
  626. return 0, 0, errors.New("node-token encryption is off; set NODE_TOKEN_ENCRYPTION=migration|required and a key first")
  627. }
  628. db := database.GetDB()
  629. var nodes []*model.Node
  630. if err := db.Model(model.Node{}).Order("id asc").Find(&nodes).Error; err != nil {
  631. return 0, 0, err
  632. }
  633. changed, skipped := 0, 0
  634. for _, n := range nodes {
  635. old := n.ApiToken
  636. if old == "" {
  637. skipped++
  638. continue
  639. }
  640. if codec.EncryptedWithActive(old) {
  641. if _, err := codec.Decrypt(n.Id, old); err != nil {
  642. return changed, skipped, fmt.Errorf("node %d validate active ciphertext: %w", n.Id, err)
  643. }
  644. skipped++
  645. continue
  646. }
  647. plain, err := codec.Decrypt(n.Id, old) // plaintext passes through; old-key ciphertext is decrypted
  648. if err != nil {
  649. return changed, skipped, fmt.Errorf("node %d decrypt: %w", n.Id, err)
  650. }
  651. enc, err := codec.Encrypt(n.Id, plain)
  652. if err != nil {
  653. return changed, skipped, fmt.Errorf("node %d encrypt: %w", n.Id, err)
  654. }
  655. res := db.Model(model.Node{}).Where("id = ? AND api_token = ?", n.Id, old).Update("api_token", enc)
  656. if res.Error != nil {
  657. return changed, skipped, res.Error
  658. }
  659. if res.RowsAffected == 1 {
  660. changed++
  661. } else {
  662. skipped++ // raced with a live update; a later run handles it
  663. }
  664. }
  665. return changed, skipped, nil
  666. }
  667. func (s *NodeService) GetRemoteInboundOptions(ctx context.Context, n *model.Node) ([]runtime.RemoteInboundOption, error) {
  668. if err := s.normalize(n); err != nil {
  669. return nil, err
  670. }
  671. if n.OutboundTag == "" {
  672. return runtime.NewRemote(n, nil).ListInboundOptions(ctx)
  673. }
  674. // Mirror ProbeWithOutbound: a node being added/edited has no persistent
  675. // egress bridge yet, so route the list call through a temporary one or the
  676. // remote panel stays unreachable and the request times out.
  677. var options []runtime.RemoteInboundOption
  678. var err error
  679. s.withOutboundBridge(n.Id, n.OutboundTag, func(proxyURL string) {
  680. options, err = runtime.NewRemote(n, staticEgressResolver(proxyURL)).ListInboundOptions(ctx)
  681. })
  682. return options, err
  683. }
  684. // staticEgressResolver hands a fixed proxy URL to runtime.NewRemote. An empty
  685. // string yields a direct connection, so it doubles as the graceful fallback
  686. // when a temporary bridge can't be built.
  687. type staticEgressResolver string
  688. func (r staticEgressResolver) NodeEgressProxyURL(int) string { return string(r) }
  689. // EnsureInboundTagAllowed adds a panel-managed inbound's tag to the node's
  690. // selection when the node syncs in "selected" mode. Without it, the next
  691. // traffic sync would filter the tag out of the snapshot and the orphan sweep
  692. // would silently delete the central row the panel just created or renamed.
  693. // Tags are only ever added (never removed): on a rename the node may keep
  694. // reporting the old tag until the remote update lands, and a leftover entry
  695. // that matches nothing is harmless.
  696. func (s *NodeService) EnsureInboundTagAllowed(nodeID int, tag string) error {
  697. return s.EnsureInboundTagAllowedTx(database.GetDB(), nodeID, tag)
  698. }
  699. func (s *NodeService) EnsureInboundTagAllowedTx(tx *gorm.DB, nodeID int, tag string) error {
  700. tag = strings.TrimSpace(tag)
  701. if nodeID <= 0 || tag == "" {
  702. return nil
  703. }
  704. if tx == nil {
  705. tx = database.GetDB()
  706. }
  707. node := &model.Node{}
  708. if err := tx.Where("id = ?", nodeID).First(node).Error; err != nil {
  709. return err
  710. }
  711. if node.InboundSyncMode != "selected" {
  712. return nil
  713. }
  714. if slices.Contains(node.InboundTags, tag) {
  715. return nil
  716. }
  717. buf, err := json.Marshal(append(node.InboundTags, tag))
  718. if err != nil {
  719. return err
  720. }
  721. return tx.Model(model.Node{}).Where("id = ?", nodeID).
  722. Updates(map[string]any{"inbound_tags": string(buf)}).Error
  723. }
  724. func nodeSelectedTagSet(n *model.Node) map[string]struct{} {
  725. if n == nil || n.InboundSyncMode != "selected" {
  726. return nil
  727. }
  728. prefix := nodeTagPrefix(&n.Id)
  729. allowed := make(map[string]struct{}, len(n.InboundTags)*2)
  730. for _, tag := range n.InboundTags {
  731. allowed[tag] = struct{}{}
  732. if prefix != "" {
  733. if stripped, found := strings.CutPrefix(tag, prefix); found {
  734. allowed[stripped] = struct{}{}
  735. } else {
  736. allowed[prefix+tag] = struct{}{}
  737. }
  738. }
  739. }
  740. return allowed
  741. }
  742. // A deselected tag is still served by the node — FilterNodeSnapshot just stops
  743. // reporting it — so its absence must never be read as "the node deleted it".
  744. func unmanagedTagPredicate(n *model.Node) func(string) bool {
  745. managed := nodeSelectedTagSet(n)
  746. if managed == nil {
  747. return func(string) bool { return false }
  748. }
  749. return func(tag string) bool {
  750. _, ok := managed[tag]
  751. return !ok
  752. }
  753. }
  754. func FilterNodeSnapshot(n *model.Node, snap *runtime.TrafficSnapshot) {
  755. if n == nil || snap == nil || n.InboundSyncMode != "selected" {
  756. return
  757. }
  758. allowed := nodeSelectedTagSet(n)
  759. for _, tag := range snap.ManagedAliases {
  760. allowed[tag] = struct{}{}
  761. }
  762. filtered := make([]*model.Inbound, 0, len(snap.Inbounds))
  763. for _, inbound := range snap.Inbounds {
  764. if inbound == nil {
  765. continue
  766. }
  767. if _, ok := allowed[inbound.Tag]; ok {
  768. filtered = append(filtered, inbound)
  769. }
  770. }
  771. snap.Inbounds = filtered
  772. }
  773. func (s *NodeService) Delete(id int) error {
  774. db := database.GetDB()
  775. // Refuse to delete a node that still owns inbounds: dropping the node row
  776. // while inbounds keep its node_id leaves orphaned, dangling references that
  777. // confuse node sync, subscriptions and cleanup. The operator must detach or
  778. // remove those inbounds first. (DB-002)
  779. var attached int64
  780. if err := db.Model(&model.Inbound{}).Where("node_id = ?", id).Count(&attached).Error; err != nil {
  781. return err
  782. }
  783. if attached > 0 {
  784. return common.NewError(fmt.Sprintf("cannot delete node: %d inbound(s) still attached to it; detach or delete them first", attached))
  785. }
  786. // Capture the node's guid before deleting the row so we can drop its per-node
  787. // IP attribution. NodeClientIp is keyed by the node's attribution key, which
  788. // is its guid normally but its node-unique key for a cloned/ambiguous-guid
  789. // node (see effectiveNodeKey) — so we purge both below.
  790. var guid string
  791. var n model.Node
  792. if err := db.Select("guid").Where("id = ?", id).First(&n).Error; err == nil {
  793. guid = n.Guid
  794. }
  795. // Delete the node row and its per-node child rows atomically. Remove the
  796. // children (traffic baselines, IP attribution) before the parent node row so
  797. // the ordering already matches a future ON DELETE constraint. Delete stays
  798. // tolerant of a missing node row so it can still clean up orphaned baselines.
  799. if err := db.Transaction(func(tx *gorm.DB) error {
  800. if err := tx.Where("node_id = ?", id).Delete(&model.NodeClientTraffic{}).Error; err != nil {
  801. return err
  802. }
  803. guids := []string{synthNodeGuid(id)}
  804. if guid != "" {
  805. guids = append(guids, guid)
  806. }
  807. if err := tx.Where("node_guid IN ?", guids).Delete(&model.NodeClientIp{}).Error; err != nil {
  808. return err
  809. }
  810. return tx.Where("id = ?", id).Delete(&model.Node{}).Error
  811. }); err != nil {
  812. return err
  813. }
  814. if mgr := runtime.GetManager(); mgr != nil {
  815. mgr.InvalidateNode(id)
  816. }
  817. nodeMetrics.drop(nodeMetricKey(id, "cpu"))
  818. nodeMetrics.drop(nodeMetricKey(id, "mem"))
  819. return nil
  820. }
  821. func (s *NodeService) SetEnable(id int, enable bool) error {
  822. db := database.GetDB()
  823. if err := db.Model(model.Node{}).Where("id = ?", id).Update("enable", enable).Error; err != nil {
  824. return err
  825. }
  826. if mgr := runtime.GetManager(); mgr != nil {
  827. mgr.InvalidateNode(id)
  828. }
  829. return nil
  830. }
  831. // GetWebCertFiles asks a node for its own web TLS certificate/key file paths,
  832. // used by "Set Cert from Panel" so a node-assigned inbound gets paths that
  833. // exist on the node rather than the central panel. See issue #4854.
  834. func (s *NodeService) GetWebCertFiles(id int) (*runtime.WebCertFiles, error) {
  835. n, err := s.GetById(id)
  836. if err != nil || n == nil {
  837. return nil, fmt.Errorf("node not found")
  838. }
  839. if !n.Enable {
  840. return nil, fmt.Errorf("node is disabled")
  841. }
  842. mgr := runtime.GetManager()
  843. if mgr == nil {
  844. return nil, fmt.Errorf("runtime manager unavailable")
  845. }
  846. remote, err := mgr.RemoteFor(n)
  847. if err != nil {
  848. return nil, err
  849. }
  850. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  851. defer cancel()
  852. return remote.GetWebCertFiles(ctx)
  853. }
  854. // NodeUpdateResult reports the outcome of triggering a panel self-update on one
  855. // node so the UI can show per-node success/failure for a bulk request.
  856. type NodeUpdateResult struct {
  857. Id int `json:"id"`
  858. Name string `json:"name"`
  859. OK bool `json:"ok"`
  860. Error string `json:"error,omitempty"`
  861. }
  862. // UpdatePanels triggers the official self-updater on each given node. Only
  863. // enabled, online nodes are eligible — an offline node can't be reached, so it
  864. // is reported as skipped rather than silently dropped.
  865. func (s *NodeService) UpdatePanels(ids []int, dev bool) ([]NodeUpdateResult, error) {
  866. mgr := runtime.GetManager()
  867. if mgr == nil {
  868. return nil, fmt.Errorf("runtime manager unavailable")
  869. }
  870. results := make([]NodeUpdateResult, 0, len(ids))
  871. for _, id := range ids {
  872. n, err := s.GetById(id)
  873. if err != nil || n == nil {
  874. results = append(results, NodeUpdateResult{Id: id, OK: false, Error: "node not found"})
  875. continue
  876. }
  877. res := NodeUpdateResult{Id: id, Name: n.Name}
  878. switch {
  879. case !n.Enable:
  880. res.Error = "node is disabled"
  881. case n.Status != "online":
  882. res.Error = "node is offline"
  883. default:
  884. remote, remoteErr := mgr.RemoteFor(n)
  885. if remoteErr != nil {
  886. res.Error = remoteErr.Error()
  887. break
  888. }
  889. ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
  890. updErr := remote.UpdatePanel(ctx, dev)
  891. cancel()
  892. if updErr != nil {
  893. res.Error = updErr.Error()
  894. } else {
  895. res.OK = true
  896. }
  897. }
  898. results = append(results, res)
  899. }
  900. return results, nil
  901. }
  902. func (s *NodeService) UpdateHeartbeat(id int, p HeartbeatPatch) error {
  903. db := database.GetDB()
  904. updates := map[string]any{
  905. "status": p.Status,
  906. "last_heartbeat": p.LastHeartbeat,
  907. "latency_ms": p.LatencyMs,
  908. "xray_version": p.XrayVersion,
  909. "panel_version": p.PanelVersion,
  910. "cpu_pct": p.CpuPct,
  911. "mem_pct": p.MemPct,
  912. "uptime_secs": p.UptimeSecs,
  913. "net_up": p.NetUp,
  914. "net_down": p.NetDown,
  915. "last_error": p.LastError,
  916. "xray_state": p.XrayState,
  917. "xray_error": p.XrayError,
  918. }
  919. // Only learn the GUID; never clear a known one if an old-build node (or a
  920. // failed probe) reports none, so the stable identity survives blips.
  921. if p.Guid != "" {
  922. updates["guid"] = p.Guid
  923. s.warnOnDuplicateGuid(id, p.Guid)
  924. }
  925. if err := db.Model(model.Node{}).Where("id = ?", id).Updates(updates).Error; err != nil {
  926. return err
  927. }
  928. if p.Status == "online" {
  929. now := time.Unix(p.LastHeartbeat, 0)
  930. nodeMetrics.append(nodeMetricKey(id, "cpu"), now, p.CpuPct)
  931. nodeMetrics.append(nodeMetricKey(id, "mem"), now, p.MemPct)
  932. nodeMetrics.append(nodeMetricKey(id, "netUp"), now, float64(p.NetUp))
  933. nodeMetrics.append(nodeMetricKey(id, "netDown"), now, float64(p.NetDown))
  934. }
  935. return nil
  936. }
  937. // warnedDupGuid remembers the (nodeID -> guid) pairs already warned about so a
  938. // cloned-server collision is logged once, not every heartbeat.
  939. var warnedDupGuid sync.Map
  940. // warnOnDuplicateGuid logs once when a node reports a panelGuid already held by
  941. // another node or by the master itself (the cloned-server footgun). Attribution
  942. // still works — it falls back to node-unique keys — but the operator should
  943. // regenerate the duplicate panelGuid to restore real identity and per-node IP
  944. // attribution. Re-arms if the collision later clears.
  945. func (s *NodeService) warnOnDuplicateGuid(id int, guid string) {
  946. var clash int64
  947. database.GetDB().Model(&model.Node{}).Where("guid = ? AND id <> ?", guid, id).Count(&clash)
  948. masterGuid, _ := (&SettingService{}).GetPanelGuid()
  949. if clash == 0 && guid != masterGuid {
  950. warnedDupGuid.Delete(id)
  951. return
  952. }
  953. if prev, ok := warnedDupGuid.Load(id); ok && prev == guid {
  954. return
  955. }
  956. warnedDupGuid.Store(id, guid)
  957. logger.Warningf("node %d reports panelGuid %s already used by another node or the master (cloned server?) — regenerate it on that node so online and IP attribution stay per-node", id, guid)
  958. }
  959. func (s *NodeService) MarkNodeDirty(id int) error {
  960. return s.MarkNodeDirtyTx(database.GetDB(), id)
  961. }
  962. func (s *NodeService) MarkNodeDirtyTx(tx *gorm.DB, id int) error {
  963. if id <= 0 {
  964. return nil
  965. }
  966. if tx == nil {
  967. return errors.New("nil db transaction")
  968. }
  969. return tx.Model(model.Node{}).
  970. Where("id = ?", id).
  971. Updates(map[string]any{
  972. "config_dirty": true,
  973. "config_dirty_at": time.Now().UnixMilli(),
  974. }).Error
  975. }
  976. func (s *NodeService) ClearNodeDirty(id int, dirtyAt int64) error {
  977. if id <= 0 {
  978. return nil
  979. }
  980. return database.GetDB().Model(model.Node{}).
  981. Where("id = ? AND config_dirty_at = ?", id, dirtyAt).
  982. Update("config_dirty", false).Error
  983. }
  984. func (s *NodeService) MarkNodeInboundsAdopted(id int) error {
  985. if id <= 0 {
  986. return nil
  987. }
  988. return database.GetDB().Model(model.Node{}).
  989. Where("id = ? AND inbounds_adopted_at = 0", id).
  990. Update("inbounds_adopted_at", time.Now().Unix()).Error
  991. }
  992. func (s *NodeService) NodeSyncState(id int) (enabled bool, status string, dirty bool, dirtyAt int64, err error) {
  993. if id <= 0 {
  994. return false, "", false, 0, errors.New("invalid node id")
  995. }
  996. var row model.Node
  997. err = database.GetDB().Model(model.Node{}).
  998. Select("enable", "status", "config_dirty", "config_dirty_at").
  999. Where("id = ?", id).
  1000. First(&row).Error
  1001. if err != nil {
  1002. return false, "", false, 0, err
  1003. }
  1004. return row.Enable, row.Status, row.ConfigDirty, row.ConfigDirtyAt, nil
  1005. }
  1006. // IsNodePending reports whether a save targeting this node was deferred because
  1007. // the node is unreachable right now — offline or disabled — so the edit only
  1008. // reaches it on the next reconcile. It deliberately ignores config_dirty: that
  1009. // flag is set on EVERY node-backed edit as the reconcile self-heal marker,
  1010. // including edits pushed live to an online node, so keying the user-facing
  1011. // "saved, node offline, will sync" toast off it fired the warning on every save
  1012. // to a perfectly healthy online node.
  1013. func (s *NodeService) IsNodePending(id int) bool {
  1014. enabled, status, _, _, err := s.NodeSyncState(id)
  1015. if err != nil {
  1016. return false
  1017. }
  1018. return !enabled || status != "online"
  1019. }
  1020. func nodeMetricKey(id int, metric string) string {
  1021. return "node:" + strconv.Itoa(id) + ":" + metric
  1022. }
  1023. func (s *NodeService) AggregateNodeMetric(id int, metric string, bucketSeconds int, maxPoints int) []map[string]any {
  1024. return nodeMetrics.aggregate(nodeMetricKey(id, metric), bucketSeconds, maxPoints)
  1025. }
  1026. func (s *NodeService) Probe(ctx context.Context, n *model.Node) (HeartbeatPatch, error) {
  1027. proxyURL := ""
  1028. if n.OutboundTag != "" {
  1029. if mgr := runtime.GetManager(); mgr != nil {
  1030. proxyURL = mgr.NodeEgressProxyURL(n.Id)
  1031. }
  1032. }
  1033. return s.probe(ctx, n, proxyURL)
  1034. }
  1035. func (s *NodeService) ProbeWithOutbound(ctx context.Context, n *model.Node, outboundTag string) (HeartbeatPatch, error) {
  1036. if outboundTag == "" {
  1037. return s.Probe(ctx, n)
  1038. }
  1039. var patch HeartbeatPatch
  1040. var err error
  1041. s.withOutboundBridge(n.Id, outboundTag, func(proxyURL string) {
  1042. if proxyURL == "" {
  1043. patch, err = s.Probe(ctx, n)
  1044. return
  1045. }
  1046. patch, err = s.probe(ctx, n, proxyURL)
  1047. })
  1048. return patch, err
  1049. }
  1050. // withOutboundBridge stands up a temporary loopback SOCKS5 inbound in the
  1051. // running Xray, routes it through outboundTag, and runs fn with the bridge's
  1052. // proxy URL before tearing it down. It is used to reach a node through its
  1053. // connection outbound before the persistent egress bridge has been injected
  1054. // into the config (e.g. while the node is still being added or edited). When
  1055. // Xray isn't running or the bridge can't be built, fn runs with an empty
  1056. // proxyURL so callers fall back to a direct connection.
  1057. func (s *NodeService) withOutboundBridge(nodeID int, outboundTag string, fn func(proxyURL string)) {
  1058. proc := XrayProcess()
  1059. if proc == nil || !proc.IsRunning() {
  1060. fn("")
  1061. return
  1062. }
  1063. apiPort := proc.GetAPIPort()
  1064. if apiPort <= 0 {
  1065. fn("")
  1066. return
  1067. }
  1068. listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0")
  1069. if err != nil {
  1070. fn("")
  1071. return
  1072. }
  1073. port := listener.Addr().(*net.TCPAddr).Port
  1074. listener.Close()
  1075. tag := fmt.Sprintf("node-test-%d-%d", nodeID, time.Now().UnixNano())
  1076. proxyURL := fmt.Sprintf("socks5://127.0.0.1:%d", port)
  1077. inboundJSON, err := json.Marshal(xray.InboundConfig{
  1078. Listen: json_util.RawMessage(`"127.0.0.1"`),
  1079. Port: port,
  1080. Protocol: "socks",
  1081. Settings: json_util.RawMessage(`{"auth":"noauth","udp":false}`),
  1082. Tag: tag,
  1083. })
  1084. if err != nil {
  1085. fn("")
  1086. return
  1087. }
  1088. cfg := proc.GetConfig()
  1089. routing := map[string]any{}
  1090. if len(cfg.RouterConfig) > 0 {
  1091. _ = json.Unmarshal(cfg.RouterConfig, &routing)
  1092. }
  1093. rules, _ := routing["rules"].([]any)
  1094. rule := map[string]any{
  1095. "type": "field",
  1096. "inboundTag": []any{tag},
  1097. }
  1098. if routingTagIsBalancer(routing, outboundTag) {
  1099. rule["balancerTag"] = outboundTag
  1100. } else {
  1101. rule["outboundTag"] = outboundTag
  1102. }
  1103. routing["rules"] = append([]any{rule}, rules...)
  1104. routingJSON, err := json.Marshal(routing)
  1105. if err != nil {
  1106. fn("")
  1107. return
  1108. }
  1109. originalRoutingJSON := cfg.RouterConfig
  1110. api := xray.XrayAPI{}
  1111. if err := api.Init(apiPort); err != nil {
  1112. fn("")
  1113. return
  1114. }
  1115. defer api.Close()
  1116. if err := api.AddInbound(inboundJSON); err != nil {
  1117. fn("")
  1118. return
  1119. }
  1120. defer func() {
  1121. if err := api.DelInbound(tag); err != nil {
  1122. logger.Warning("remove temp node bridge inbound failed:", err)
  1123. }
  1124. }()
  1125. if err := api.ApplyRoutingConfig(routingJSON); err != nil {
  1126. fn("")
  1127. return
  1128. }
  1129. defer func() {
  1130. restore := originalRoutingJSON
  1131. if len(restore) == 0 {
  1132. restore = []byte("{}")
  1133. }
  1134. if err := api.ApplyRoutingConfig(restore); err != nil {
  1135. logger.Warning("restore routing after node bridge failed:", err)
  1136. }
  1137. }()
  1138. fn(proxyURL)
  1139. }
  1140. func (s *NodeService) probe(ctx context.Context, n *model.Node, proxyURL string) (HeartbeatPatch, error) {
  1141. patch := HeartbeatPatch{LastHeartbeat: time.Now().Unix()}
  1142. addr, err := netsafe.NormalizeHost(n.Address)
  1143. if err != nil {
  1144. patch.LastError = err.Error()
  1145. return patch, err
  1146. }
  1147. scheme := n.Scheme
  1148. if scheme != "http" && scheme != "https" {
  1149. scheme = "https"
  1150. }
  1151. if n.Port <= 0 || n.Port > 65535 {
  1152. patch.LastError = "node port must be 1-65535"
  1153. return patch, errors.New(patch.LastError)
  1154. }
  1155. probeURL := &url.URL{
  1156. Scheme: scheme,
  1157. Host: net.JoinHostPort(addr, strconv.Itoa(n.Port)),
  1158. Path: normalizeBasePath(n.BasePath) + "panel/api/server/status",
  1159. }
  1160. req, err := http.NewRequestWithContext(
  1161. netsafe.ContextWithAllowPrivate(ctx, n.AllowPrivateAddress),
  1162. http.MethodGet, probeURL.String(), nil)
  1163. if err != nil {
  1164. patch.LastError = err.Error()
  1165. return patch, err
  1166. }
  1167. if n.ApiToken != "" {
  1168. token, derr := nodetoken.Decrypt(n.Id, n.ApiToken)
  1169. if derr != nil {
  1170. patch.LastError = derr.Error()
  1171. return patch, derr
  1172. }
  1173. req.Header.Set("Authorization", "Bearer "+token)
  1174. }
  1175. req.Header.Set("Accept", "application/json")
  1176. client, err := runtime.HTTPClientForNode(n, proxyURL)
  1177. if err != nil {
  1178. patch.LastError = err.Error()
  1179. return patch, err
  1180. }
  1181. start := time.Now()
  1182. resp, err := client.Do(req)
  1183. if err != nil {
  1184. patch.LastError = err.Error()
  1185. return patch, err
  1186. }
  1187. defer resp.Body.Close()
  1188. patch.LatencyMs = int(time.Since(start) / time.Millisecond)
  1189. if resp.StatusCode != http.StatusOK {
  1190. patch.LastError = fmt.Sprintf("HTTP %d from remote panel", resp.StatusCode)
  1191. return patch, errors.New(patch.LastError)
  1192. }
  1193. var envelope struct {
  1194. Success bool `json:"success"`
  1195. Msg string `json:"msg"`
  1196. Obj *struct {
  1197. CpuPct float64 `json:"cpu"`
  1198. Mem struct {
  1199. Current uint64 `json:"current"`
  1200. Total uint64 `json:"total"`
  1201. } `json:"mem"`
  1202. Xray struct {
  1203. Version string `json:"version"`
  1204. State string `json:"state"`
  1205. ErrorMsg string `json:"errorMsg"`
  1206. } `json:"xray"`
  1207. PanelVersion string `json:"panelVersion"`
  1208. PanelGuid string `json:"panelGuid"`
  1209. Uptime uint64 `json:"uptime"`
  1210. NetIO struct {
  1211. Up uint64 `json:"up"`
  1212. Down uint64 `json:"down"`
  1213. } `json:"netIO"`
  1214. } `json:"obj"`
  1215. }
  1216. if err := json.NewDecoder(resp.Body).Decode(&envelope); err != nil {
  1217. patch.LastError = "decode response: " + err.Error()
  1218. return patch, err
  1219. }
  1220. if !envelope.Success || envelope.Obj == nil {
  1221. patch.LastError = "remote returned success=false: " + envelope.Msg
  1222. return patch, errors.New(patch.LastError)
  1223. }
  1224. o := envelope.Obj
  1225. patch.CpuPct = o.CpuPct
  1226. if o.Mem.Total > 0 {
  1227. patch.MemPct = float64(o.Mem.Current) * 100.0 / float64(o.Mem.Total)
  1228. }
  1229. patch.XrayVersion = o.Xray.Version
  1230. patch.XrayState = o.Xray.State
  1231. patch.XrayError = o.Xray.ErrorMsg
  1232. patch.PanelVersion = o.PanelVersion
  1233. patch.Guid = o.PanelGuid
  1234. patch.UptimeSecs = o.Uptime
  1235. patch.NetUp = o.NetIO.Up
  1236. patch.NetDown = o.NetIO.Down
  1237. return patch, nil
  1238. }
  1239. type ProbeResultUI struct {
  1240. Status string `json:"status" example:"online"`
  1241. LatencyMs int `json:"latencyMs" example:"42"`
  1242. XrayVersion string `json:"xrayVersion" example:"25.10.31"`
  1243. PanelVersion string `json:"panelVersion" example:"v3.x.x"`
  1244. CpuPct float64 `json:"cpuPct" example:"12.5"`
  1245. MemPct float64 `json:"memPct" example:"45.2"`
  1246. UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
  1247. Error string `json:"error"`
  1248. // XrayState/XrayError are populated on successful probes even when the node's
  1249. // Xray core is not healthy. The UI uses them for a distinct "panel ok, xray failed" indicator.
  1250. XrayState string `json:"xrayState"`
  1251. XrayError string `json:"xrayError"`
  1252. }
  1253. func (p HeartbeatPatch) ToUI(ok bool) ProbeResultUI {
  1254. r := ProbeResultUI{
  1255. LatencyMs: p.LatencyMs,
  1256. XrayVersion: p.XrayVersion,
  1257. PanelVersion: p.PanelVersion,
  1258. CpuPct: p.CpuPct,
  1259. MemPct: p.MemPct,
  1260. UptimeSecs: p.UptimeSecs,
  1261. Error: FriendlyProbeError(p.LastError),
  1262. XrayState: p.XrayState,
  1263. XrayError: p.XrayError,
  1264. }
  1265. if ok {
  1266. r.Status = "online"
  1267. } else {
  1268. r.Status = "offline"
  1269. }
  1270. return r
  1271. }
  1272. func FriendlyProbeError(msg string) string {
  1273. if strings.Contains(msg, "server gave HTTP response to HTTPS client") {
  1274. return "the server speaks HTTP, not HTTPS; set the node scheme to http"
  1275. }
  1276. return msg
  1277. }