model.go 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928
  1. // Package model defines the database models and data structures used by the 3x-ui panel.
  2. package model
  3. import (
  4. "bytes"
  5. "crypto/rand"
  6. "encoding/hex"
  7. "encoding/json"
  8. "fmt"
  9. "strings"
  10. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  11. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  12. )
  13. // Protocol represents the protocol type for Xray inbounds.
  14. type Protocol string
  15. // Protocol constants for different Xray inbound protocols.
  16. // Hysteria v2 is not a distinct protocol — it is plain "hysteria"
  17. // with streamSettings.version = 2. The share-link URI scheme
  18. // "hysteria2://" is independent of this and is still emitted by the
  19. // link generator when the stream version is 2.
  20. const (
  21. VMESS Protocol = "vmess"
  22. VLESS Protocol = "vless"
  23. Tunnel Protocol = "tunnel"
  24. HTTP Protocol = "http"
  25. Trojan Protocol = "trojan"
  26. Shadowsocks Protocol = "shadowsocks"
  27. Mixed Protocol = "mixed"
  28. WireGuard Protocol = "wireguard"
  29. Hysteria Protocol = "hysteria"
  30. MTProto Protocol = "mtproto"
  31. )
  32. // User represents a user account in the 3x-ui panel.
  33. type User struct {
  34. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  35. Username string `json:"username"`
  36. Password string `json:"password"`
  37. LoginEpoch int64 `json:"-" gorm:"default:0"`
  38. }
  39. // Inbound represents an Xray inbound configuration with traffic statistics and settings.
  40. type Inbound struct {
  41. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"` // Unique identifier
  42. UserId int `json:"-"` // Associated user ID
  43. Up int64 `json:"up" form:"up"` // Upload traffic in bytes
  44. Down int64 `json:"down" form:"down"` // Download traffic in bytes
  45. Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
  46. Remark string `json:"remark" form:"remark" example:"VLESS-443"` // Human-readable remark
  47. SubSortIndex int `json:"subSortIndex" form:"subSortIndex" gorm:"default:1" validate:"omitempty,gte=1" example:"1"` // 1-based sort order of this inbound's links in subscription output only (lower first; ties by id)
  48. Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1" example:"true"` // Whether the inbound is enabled
  49. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  50. TrafficReset string `json:"trafficReset" form:"trafficReset" gorm:"default:never;index:idx_enable_traffic_reset,priority:2" validate:"omitempty,oneof=never hourly daily weekly monthly"` // Traffic reset schedule
  51. LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
  52. ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
  53. // Xray configuration fields
  54. Listen string `json:"listen" form:"listen"`
  55. Port int `json:"port" form:"port" validate:"gte=0,lte=65535" example:"443"`
  56. Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel tun mtproto" example:"vless"`
  57. Settings string `json:"settings" form:"settings"`
  58. StreamSettings string `json:"streamSettings" form:"streamSettings"`
  59. Tag string `json:"tag" form:"tag" gorm:"unique" example:"in-443-tcp"`
  60. Sniffing string `json:"sniffing" form:"sniffing"`
  61. NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
  62. ShareAddrStrategy string `json:"shareAddrStrategy" form:"shareAddrStrategy" gorm:"column:share_addr_strategy;default:node" validate:"omitempty,oneof=node listen custom"`
  63. ShareAddr string `json:"shareAddr" form:"shareAddr" gorm:"column:share_addr"`
  64. // OriginNodeGuid is the panelGuid of the node that physically hosts this
  65. // inbound, propagated up across hops (#4983). Empty for an inbound that
  66. // lives on this panel's own xray; set to the originating node's GUID when
  67. // the inbound was synced from a node (kept as-is across further hops). Lets
  68. // the master attribute a deeply nested inbound to the real node instead of
  69. // the intermediate one it was fetched through.
  70. OriginNodeGuid string `json:"originNodeGuid,omitempty" form:"originNodeGuid" gorm:"column:origin_node_guid;index"`
  71. // FallbackParent is populated by the API layer when this inbound is
  72. // attached as a fallback child of a VLESS/Trojan TCP-TLS master.
  73. // The frontend uses it to rewrite client-share links so they advertise
  74. // the master's externally reachable endpoint instead of the child's
  75. // loopback listen. Not persisted.
  76. FallbackParent *FallbackParentInfo `json:"fallbackParent,omitempty" gorm:"-"`
  77. }
  78. // FallbackParentInfo carries everything the frontend needs to rewrite a
  79. // child inbound's client link: where to connect (the master's address
  80. // and port) and which path matched on the master's fallbacks array.
  81. // The frontend already has the master inbound in its dbInbounds list,
  82. // so we only ship identifiers + the match path here.
  83. type FallbackParentInfo struct {
  84. MasterId int `json:"masterId"`
  85. Path string `json:"path,omitempty"`
  86. }
  87. // OutboundTraffics tracks traffic statistics for Xray outbound connections.
  88. type OutboundTraffics struct {
  89. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  90. Tag string `json:"tag" form:"tag" gorm:"unique"`
  91. Up int64 `json:"up" form:"up" gorm:"default:0"`
  92. Down int64 `json:"down" form:"down" gorm:"default:0"`
  93. Total int64 `json:"total" form:"total" gorm:"default:0"`
  94. }
  95. // InboundClientIps stores IP addresses associated with inbound clients for access control.
  96. type InboundClientIps struct {
  97. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  98. ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
  99. Ips string `json:"ips" form:"ips"`
  100. }
  101. // MarshalJSON emits the Ips column as a real JSON array instead of an escaped
  102. // JSON-text string. Empty or unparseable storage renders as null so API
  103. // consumers don't have to special-case the legacy double-encoded shape.
  104. func (ic InboundClientIps) MarshalJSON() ([]byte, error) {
  105. type alias InboundClientIps
  106. return json.Marshal(struct {
  107. alias
  108. Ips json.RawMessage `json:"ips"`
  109. }{
  110. alias: alias(ic),
  111. Ips: jsonStringFieldToRaw(ic.Ips),
  112. })
  113. }
  114. // UnmarshalJSON accepts ips as either a JSON array (modern shape) or a
  115. // JSON-encoded string (legacy shape), normalising back to the JSON-text the
  116. // column stores.
  117. func (ic *InboundClientIps) UnmarshalJSON(data []byte) error {
  118. type alias InboundClientIps
  119. aux := struct {
  120. *alias
  121. Ips json.RawMessage `json:"ips"`
  122. }{
  123. alias: (*alias)(ic),
  124. }
  125. if err := json.Unmarshal(data, &aux); err != nil {
  126. return err
  127. }
  128. ic.Ips = jsonStringFieldFromRaw(aux.Ips)
  129. return nil
  130. }
  131. // HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
  132. type HistoryOfSeeders struct {
  133. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  134. SeederName string `json:"seederName"`
  135. }
  136. type ApiToken struct {
  137. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  138. Name string `json:"name" gorm:"uniqueIndex;not null"`
  139. Token string `json:"token" gorm:"not null"` // SHA-256 hash; the plaintext is shown only once at creation
  140. Enabled bool `json:"enabled" gorm:"default:true"`
  141. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  142. }
  143. // MarshalJSON emits settings, streamSettings, and sniffing as nested JSON
  144. // objects rather than escaped strings, so API consumers don't need to JSON.parse
  145. // a string inside a string. Empty fields render as null; fields whose stored
  146. // text isn't valid JSON fall back to a JSON-encoded string so no data is lost.
  147. func (i Inbound) MarshalJSON() ([]byte, error) {
  148. type alias Inbound
  149. return json.Marshal(struct {
  150. alias
  151. Settings json.RawMessage `json:"settings"`
  152. StreamSettings json.RawMessage `json:"streamSettings"`
  153. Sniffing json.RawMessage `json:"sniffing"`
  154. }{
  155. alias: alias(i),
  156. Settings: jsonStringFieldToRaw(i.Settings),
  157. StreamSettings: jsonStringFieldToRaw(i.StreamSettings),
  158. Sniffing: jsonStringFieldToRaw(i.Sniffing),
  159. })
  160. }
  161. // UnmarshalJSON accepts settings, streamSettings, and sniffing as either a raw
  162. // JSON object/array (the modern shape MarshalJSON emits) or a JSON-encoded
  163. // string (the legacy shape). Either form is normalised back to the JSON-text
  164. // string the DB column stores.
  165. func (i *Inbound) UnmarshalJSON(data []byte) error {
  166. type alias Inbound
  167. aux := struct {
  168. *alias
  169. Settings json.RawMessage `json:"settings"`
  170. StreamSettings json.RawMessage `json:"streamSettings"`
  171. Sniffing json.RawMessage `json:"sniffing"`
  172. }{
  173. alias: (*alias)(i),
  174. }
  175. if err := json.Unmarshal(data, &aux); err != nil {
  176. return err
  177. }
  178. i.Settings = jsonStringFieldFromRaw(aux.Settings)
  179. i.StreamSettings = jsonStringFieldFromRaw(aux.StreamSettings)
  180. i.Sniffing = jsonStringFieldFromRaw(aux.Sniffing)
  181. return nil
  182. }
  183. func jsonStringFieldToRaw(s string) json.RawMessage {
  184. trimmed := strings.TrimSpace(s)
  185. if trimmed == "" {
  186. return json.RawMessage("null")
  187. }
  188. if json.Valid([]byte(trimmed)) {
  189. return json.RawMessage(trimmed)
  190. }
  191. b, _ := json.Marshal(s)
  192. return b
  193. }
  194. func jsonStringFieldFromRaw(r json.RawMessage) string {
  195. trimmed := bytes.TrimSpace(r)
  196. if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
  197. return ""
  198. }
  199. if trimmed[0] == '"' {
  200. var s string
  201. if err := json.Unmarshal(trimmed, &s); err == nil {
  202. return s
  203. }
  204. }
  205. return string(trimmed)
  206. }
  207. // StripInboundXhttpClientFields removes xHTTP knobs that belong on the
  208. // client dialer and subscription share-link extras only. xray-core's XHTTP
  209. // inbound listener does not consume them; the panel still stores them on
  210. // the inbound row so buildXhttpExtra can push defaults to clients.
  211. func StripInboundXhttpClientFields(streamSettings string) (string, bool) {
  212. if streamSettings == "" {
  213. return streamSettings, false
  214. }
  215. var stream map[string]any
  216. if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
  217. return streamSettings, false
  218. }
  219. if stream["network"] != "xhttp" {
  220. return streamSettings, false
  221. }
  222. xhttp, ok := stream["xhttpSettings"].(map[string]any)
  223. if !ok || len(xhttp) == 0 {
  224. return streamSettings, false
  225. }
  226. clientOnly := []string{
  227. "xmux",
  228. "downloadSettings",
  229. "scMinPostsIntervalMs",
  230. "uplinkChunkSize",
  231. "noGRPCHeader",
  232. }
  233. changed := false
  234. for _, key := range clientOnly {
  235. if _, has := xhttp[key]; has {
  236. delete(xhttp, key)
  237. changed = true
  238. }
  239. }
  240. if !changed {
  241. return streamSettings, false
  242. }
  243. out, err := json.MarshalIndent(stream, "", " ")
  244. if err != nil {
  245. return streamSettings, false
  246. }
  247. return string(out), true
  248. }
  249. // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
  250. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
  251. listen := i.Listen
  252. if listen == "" {
  253. listen = "0.0.0.0"
  254. }
  255. listen = fmt.Sprintf("\"%v\"", listen)
  256. protocol := string(i.Protocol)
  257. settings := i.Settings
  258. switch i.Protocol {
  259. case Shadowsocks:
  260. if healed, ok := HealShadowsocksClientMethods(settings); ok {
  261. settings = healed
  262. }
  263. case VMESS:
  264. if stripped, ok := StripVmessClientSecurity(settings); ok {
  265. settings = stripped
  266. }
  267. case VLESS:
  268. if stripped, ok := StripVlessInboundEncryption(settings); ok {
  269. settings = stripped
  270. }
  271. }
  272. streamSettings := i.StreamSettings
  273. if stripped, ok := StripInboundXhttpClientFields(streamSettings); ok {
  274. streamSettings = stripped
  275. }
  276. return &xray.InboundConfig{
  277. Listen: json_util.RawMessage(listen),
  278. Port: i.Port,
  279. Protocol: protocol,
  280. Settings: json_util.RawMessage(settings),
  281. StreamSettings: json_util.RawMessage(streamSettings),
  282. Tag: i.Tag,
  283. Sniffing: json_util.RawMessage(i.Sniffing),
  284. }
  285. }
  286. func StripVmessClientSecurity(settings string) (string, bool) {
  287. if settings == "" {
  288. return settings, false
  289. }
  290. var parsed map[string]any
  291. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  292. return settings, false
  293. }
  294. clients, ok := parsed["clients"].([]any)
  295. if !ok {
  296. return settings, false
  297. }
  298. changed := false
  299. for i := range clients {
  300. cm, ok := clients[i].(map[string]any)
  301. if !ok {
  302. continue
  303. }
  304. if _, has := cm["security"]; has {
  305. delete(cm, "security")
  306. clients[i] = cm
  307. changed = true
  308. }
  309. }
  310. if !changed {
  311. return settings, false
  312. }
  313. out, err := json.MarshalIndent(parsed, "", " ")
  314. if err != nil {
  315. return settings, false
  316. }
  317. return string(out), true
  318. }
  319. func StripVlessInboundEncryption(settings string) (string, bool) {
  320. if settings == "" {
  321. return settings, false
  322. }
  323. var parsed map[string]any
  324. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  325. return settings, false
  326. }
  327. if _, has := parsed["encryption"]; !has {
  328. return settings, false
  329. }
  330. delete(parsed, "encryption")
  331. out, err := json.MarshalIndent(parsed, "", " ")
  332. if err != nil {
  333. return settings, false
  334. }
  335. return string(out), true
  336. }
  337. // HealShadowsocksClientMethods normalises the per-client `method` field
  338. // on a shadowsocks inbound's settings JSON before it leaves for xray-core:
  339. // - Legacy ciphers (aes-*, chacha20-*): every client must carry a
  340. // per-user `method` matching the inbound's top-level method, otherwise
  341. // xray fails with "unsupported cipher method:".
  342. // - Shadowsocks 2022 (2022-blake3-*): xray's multi-user code rejects the
  343. // inbound with "users must have empty method" when a client carries
  344. // one — strip stale entries left over from a switch off a legacy
  345. // cipher.
  346. //
  347. // Returns the rewritten settings string and true when anything changed.
  348. func HealShadowsocksClientMethods(settings string) (string, bool) {
  349. if settings == "" {
  350. return settings, false
  351. }
  352. var parsed map[string]any
  353. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  354. return settings, false
  355. }
  356. method, _ := parsed["method"].(string)
  357. clients, ok := parsed["clients"].([]any)
  358. if !ok {
  359. return settings, false
  360. }
  361. is2022 := strings.HasPrefix(method, "2022-blake3-")
  362. changed := false
  363. for i := range clients {
  364. cm, ok := clients[i].(map[string]any)
  365. if !ok {
  366. continue
  367. }
  368. if is2022 {
  369. if _, hasKey := cm["method"]; hasKey {
  370. delete(cm, "method")
  371. clients[i] = cm
  372. changed = true
  373. }
  374. continue
  375. }
  376. if method == "" {
  377. continue
  378. }
  379. existing, _ := cm["method"].(string)
  380. if existing == method {
  381. continue
  382. }
  383. cm["method"] = method
  384. clients[i] = cm
  385. changed = true
  386. }
  387. if !changed {
  388. return settings, false
  389. }
  390. out, err := json.MarshalIndent(parsed, "", " ")
  391. if err != nil {
  392. return settings, false
  393. }
  394. return string(out), true
  395. }
  396. // GenerateFakeTLSSecret builds an MTProto FakeTLS secret for the given domain:
  397. // the "ee" FakeTLS marker, 16 random bytes, then the domain encoded as hex.
  398. // This single value is what mtg's config and the client tg:// link both use.
  399. func GenerateFakeTLSSecret(domain string) string {
  400. return "ee" + mtprotoRandomMiddle() + hex.EncodeToString([]byte(domain))
  401. }
  402. func mtprotoRandomMiddle() string {
  403. buf := make([]byte, 16)
  404. if _, err := rand.Read(buf); err != nil {
  405. panic(fmt.Errorf("mtproto: crypto/rand read failed: %w", err))
  406. }
  407. return hex.EncodeToString(buf)
  408. }
  409. // mtprotoSecretMiddle returns the 16-byte random middle of an existing secret
  410. // when it is well-formed, otherwise a freshly generated one. Reusing the middle
  411. // keeps the secret stable when only the FakeTLS domain changes.
  412. func mtprotoSecretMiddle(secret string) string {
  413. s := secret
  414. if strings.HasPrefix(s, "ee") || strings.HasPrefix(s, "dd") {
  415. s = s[2:]
  416. }
  417. if len(s) >= 32 {
  418. mid := s[:32]
  419. if _, err := hex.DecodeString(mid); err == nil {
  420. return mid
  421. }
  422. }
  423. return mtprotoRandomMiddle()
  424. }
  425. // HealMtprotoSecret normalises an mtproto inbound's settings JSON before the
  426. // value leaves for the mtg sidecar or a share link: it rebuilds `secret` so it
  427. // is always a valid FakeTLS secret whose trailing domain matches
  428. // `fakeTlsDomain`, generating the random middle when one is missing and
  429. // rewriting the domain suffix when the domain changed. Returns the rewritten
  430. // settings and true when anything changed.
  431. func HealMtprotoSecret(settings string) (string, bool) {
  432. if settings == "" {
  433. return settings, false
  434. }
  435. var parsed map[string]any
  436. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  437. return settings, false
  438. }
  439. domain, _ := parsed["fakeTlsDomain"].(string)
  440. domain = strings.TrimSpace(domain)
  441. if domain == "" {
  442. return settings, false
  443. }
  444. secret, _ := parsed["secret"].(string)
  445. expected := "ee" + mtprotoSecretMiddle(secret) + hex.EncodeToString([]byte(domain))
  446. if secret == expected {
  447. return settings, false
  448. }
  449. parsed["secret"] = expected
  450. out, err := json.MarshalIndent(parsed, "", " ")
  451. if err != nil {
  452. return settings, false
  453. }
  454. return string(out), true
  455. }
  456. // Setting stores key-value configuration settings for the 3x-ui panel.
  457. type Setting struct {
  458. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  459. Key string `json:"key" form:"key"`
  460. Value string `json:"value" form:"value"`
  461. }
  462. // Node represents a remote 3x-ui panel registered with the central panel.
  463. // The central panel polls each node's existing /panel/api/server/status
  464. // endpoint over HTTP using the per-node ApiToken to populate the runtime
  465. // status fields below.
  466. type Node struct {
  467. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"`
  468. Name string `json:"name" form:"name" gorm:"uniqueIndex" validate:"required" example:"de-fra-1"`
  469. Remark string `json:"remark" form:"remark"`
  470. Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https" example:"https"`
  471. Address string `json:"address" form:"address" validate:"required" example:"node1.example.com"`
  472. Port int `json:"port" form:"port" validate:"gte=1,lte=65535" example:"2053"`
  473. BasePath string `json:"basePath" form:"basePath" example:"/"`
  474. ApiToken string `json:"apiToken" form:"apiToken" validate:"required_unless=TlsVerifyMode mtls" example:"abcdef0123456789"`
  475. Enable bool `json:"enable" form:"enable" gorm:"default:true" example:"true"`
  476. AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
  477. TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" gorm:"column:tls_verify_mode;default:verify" validate:"omitempty,oneof=verify skip pin mtls"`
  478. PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256" gorm:"column:pinned_cert_sha256"`
  479. InboundSyncMode string `json:"inboundSyncMode" form:"inboundSyncMode" gorm:"column:inbound_sync_mode;default:all" validate:"omitempty,oneof=all selected"`
  480. InboundTags []string `json:"inboundTags" form:"inboundTags" gorm:"serializer:json;column:inbound_tags"`
  481. OutboundTag string `json:"outboundTag" form:"outboundTag" gorm:"column:outbound_tag"`
  482. // Guid is the remote panel's stable self-identifier (its panelGuid),
  483. // learned from each heartbeat. It is the globally stable node identity used
  484. // to attribute online clients/inbounds to the physical node across a chain
  485. // of nodes (#4983); panel-local autoincrement ids don't survive a hop.
  486. // Observed-state only — never user-edited.
  487. Guid string `json:"guid" gorm:"column:guid;index"`
  488. // Heartbeat-updated fields. UpdatedAt advances on every probe even when
  489. // the row is otherwise unchanged so the UI's "last seen" tooltip is
  490. // truthful without us having to read LastHeartbeat separately.
  491. Status string `json:"status" gorm:"default:unknown" example:"online"` // online|offline|unknown
  492. LastHeartbeat int64 `json:"lastHeartbeat" example:"1700000000"` // unix seconds, 0 = never
  493. LatencyMs int `json:"latencyMs" example:"42"`
  494. XrayVersion string `json:"xrayVersion" example:"25.10.31"`
  495. PanelVersion string `json:"panelVersion" gorm:"column:panel_version" example:"v3.x.x"`
  496. CpuPct float64 `json:"cpuPct" example:"23.5"`
  497. MemPct float64 `json:"memPct" example:"45.1"`
  498. UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
  499. NetUp uint64 `json:"netUp" gorm:"column:net_up" example:"1048576"`
  500. NetDown uint64 `json:"netDown" gorm:"column:net_down" example:"2097152"`
  501. LastError string `json:"lastError"`
  502. // XrayState and XrayError are captured from the remote node's /panel/api/server/status
  503. // during heartbeats. They let the central panel distinguish "panel API reachable"
  504. // (status=online) from "Xray core itself has failed on the node" for monitoring.
  505. XrayState string `json:"xrayState" gorm:"column:xray_state"`
  506. XrayError string `json:"xrayError" gorm:"column:xray_error"`
  507. ConfigDirty bool `json:"configDirty" gorm:"default:false"`
  508. ConfigDirtyAt int64 `json:"configDirtyAt"`
  509. InboundCount int `json:"inboundCount" gorm:"-" example:"5"`
  510. ClientCount int `json:"clientCount" gorm:"-" example:"27"`
  511. OnlineCount int `json:"onlineCount" gorm:"-" example:"3"`
  512. DepletedCount int `json:"depletedCount" gorm:"-" example:"1"`
  513. // ParentGuid + Transitive are set only when a node is surfaced as part of a
  514. // node tree (#4983): direct nodes carry the master panel's own GUID, a
  515. // transitive sub-node carries its parent node's GUID. Transitive nodes are
  516. // read-only projections (Id == 0, not persisted) — never edited or deployed.
  517. ParentGuid string `json:"parentGuid,omitempty" gorm:"-"`
  518. Transitive bool `json:"transitive,omitempty" gorm:"-"`
  519. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli" example:"1700000000"`
  520. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli" example:"1700000000"`
  521. }
  522. // NodeSummary is the read-only identity of a node as published one hop up: the
  523. // view a panel exposes about the nodes it directly manages, so a master can
  524. // surface transitive sub-nodes in a chained topology (#4983). Counts are
  525. // computed by the consuming master from its own per-GUID data, never trusted
  526. // from the child, so this carries identity/health only.
  527. type NodeSummary struct {
  528. Guid string `json:"guid"`
  529. ParentGuid string `json:"parentGuid"`
  530. Name string `json:"name"`
  531. Address string `json:"address"`
  532. Scheme string `json:"scheme"`
  533. Port int `json:"port"`
  534. Status string `json:"status"`
  535. LastHeartbeat int64 `json:"lastHeartbeat"`
  536. LatencyMs int `json:"latencyMs"`
  537. PanelVersion string `json:"panelVersion"`
  538. XrayVersion string `json:"xrayVersion"`
  539. // XrayState/XrayError forwarded so masters can surface xray failure on transitive sub-nodes too.
  540. XrayState string `json:"xrayState"`
  541. XrayError string `json:"xrayError,omitempty"`
  542. }
  543. type ClientReverse struct {
  544. Tag string `json:"tag"`
  545. }
  546. // Client represents a client configuration for Xray inbounds with traffic limits and settings.
  547. type Client struct {
  548. ID string `json:"id,omitempty"` // Unique client identifier
  549. Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
  550. Password string `json:"password,omitempty"` // Client password
  551. Flow string `json:"flow,omitempty"` // Flow control (XTLS)
  552. Reverse *ClientReverse `json:"reverse,omitempty"` // VLESS simple reverse proxy settings
  553. Auth string `json:"auth,omitempty"` // Auth password (Hysteria)
  554. Email string `json:"email"` // Client email identifier
  555. LimitIP int `json:"limitIp"` // IP limit for this client
  556. TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
  557. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  558. Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
  559. TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
  560. SubID string `json:"subId" form:"subId"` // Subscription identifier
  561. Group string `json:"group,omitempty" form:"group"` // Logical grouping label
  562. Comment string `json:"comment" form:"comment"` // Client comment
  563. Reset int `json:"reset" form:"reset"` // Reset period in days
  564. CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
  565. UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
  566. }
  567. type ClientRecord struct {
  568. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  569. Email string `json:"email" gorm:"uniqueIndex;not null"`
  570. SubID string `json:"subId" gorm:"index;column:sub_id"`
  571. UUID string `json:"uuid" gorm:"column:uuid"`
  572. Password string `json:"password"`
  573. Auth string `json:"auth"`
  574. Flow string `json:"flow"`
  575. Security string `json:"security"`
  576. Reverse string `json:"reverse" gorm:"column:reverse"`
  577. LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
  578. TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
  579. ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
  580. Enable bool `json:"enable" gorm:"default:true"`
  581. TgID int64 `json:"tgId" gorm:"column:tg_id"`
  582. Group string `json:"group" gorm:"column:group_name;default:'';index:idx_client_record_group"`
  583. Comment string `json:"comment"`
  584. Reset int `json:"reset" gorm:"default:0"`
  585. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  586. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  587. }
  588. func (ClientRecord) TableName() string { return "clients" }
  589. type ClientGroup struct {
  590. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  591. Name string `json:"name" gorm:"uniqueIndex;not null"`
  592. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  593. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  594. }
  595. func (ClientGroup) TableName() string { return "client_groups" }
  596. // MarshalJSON emits the reverse column as a nested JSON object rather than an
  597. // escaped JSON-text string, matching the same convention Inbound uses for its
  598. // JSON-text columns. Empty storage renders as null.
  599. func (r ClientRecord) MarshalJSON() ([]byte, error) {
  600. type alias ClientRecord
  601. return json.Marshal(struct {
  602. alias
  603. Reverse json.RawMessage `json:"reverse"`
  604. }{
  605. alias: alias(r),
  606. Reverse: jsonStringFieldToRaw(r.Reverse),
  607. })
  608. }
  609. // UnmarshalJSON accepts reverse as either a JSON object (modern shape) or a
  610. // JSON-encoded string (legacy shape).
  611. func (r *ClientRecord) UnmarshalJSON(data []byte) error {
  612. type alias ClientRecord
  613. aux := struct {
  614. *alias
  615. Reverse json.RawMessage `json:"reverse"`
  616. }{
  617. alias: (*alias)(r),
  618. }
  619. if err := json.Unmarshal(data, &aux); err != nil {
  620. return err
  621. }
  622. r.Reverse = jsonStringFieldFromRaw(aux.Reverse)
  623. return nil
  624. }
  625. type ClientInbound struct {
  626. ClientId int `json:"clientId" gorm:"primaryKey;column:client_id;index"`
  627. InboundId int `json:"inboundId" gorm:"primaryKey;column:inbound_id;index"`
  628. FlowOverride string `json:"flowOverride" gorm:"column:flow_override"`
  629. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  630. }
  631. func (ClientInbound) TableName() string { return "client_inbounds" }
  632. // ClientExternalLink is a per-client entry surfaced in the client's
  633. // subscription. Two kinds:
  634. // - "link": a single third-party share link (vless://, vmess://, trojan://,
  635. // ss://, hysteria2://, wireguard://). Emitted verbatim in raw subs; parsed
  636. // into an outbound/proxy for JSON and Clash.
  637. // - "subscription": a remote subscription URL. The panel fetches it (cached),
  638. // decodes its links, and merges them into the client's subscription.
  639. type ClientExternalLink struct {
  640. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  641. ClientId int `json:"clientId" gorm:"index;column:client_id"`
  642. Kind string `json:"kind" gorm:"column:kind"`
  643. Value string `json:"value" gorm:"column:value"`
  644. Remark string `json:"remark" gorm:"column:remark"`
  645. SortIndex int `json:"sortIndex" gorm:"column:sort_index"`
  646. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  647. }
  648. func (ClientExternalLink) TableName() string { return "client_external_links" }
  649. // External link kinds.
  650. const (
  651. ExternalLinkKindLink = "link"
  652. ExternalLinkKindSubscription = "subscription"
  653. )
  654. type InboundFallback struct {
  655. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  656. MasterId int `json:"masterId" gorm:"index;not null;column:master_id"`
  657. ChildId int `json:"childId" gorm:"index;not null;column:child_id"`
  658. Name string `json:"name"`
  659. Alpn string `json:"alpn"`
  660. Path string `json:"path"`
  661. Dest string `json:"dest"`
  662. Xver int `json:"xver"`
  663. SortOrder int `json:"sortOrder" gorm:"default:0;column:sort_order"`
  664. }
  665. func (InboundFallback) TableName() string { return "inbound_fallbacks" }
  666. func (c *Client) ToRecord() *ClientRecord {
  667. rec := &ClientRecord{
  668. Email: c.Email,
  669. SubID: c.SubID,
  670. UUID: c.ID,
  671. Password: c.Password,
  672. Auth: c.Auth,
  673. Flow: c.Flow,
  674. Security: c.Security,
  675. LimitIP: c.LimitIP,
  676. TotalGB: c.TotalGB,
  677. ExpiryTime: c.ExpiryTime,
  678. Enable: c.Enable,
  679. TgID: c.TgID,
  680. Group: c.Group,
  681. Comment: c.Comment,
  682. Reset: c.Reset,
  683. CreatedAt: c.CreatedAt,
  684. UpdatedAt: c.UpdatedAt,
  685. }
  686. if c.Reverse != nil {
  687. if b, err := json.Marshal(c.Reverse); err == nil {
  688. rec.Reverse = string(b)
  689. }
  690. }
  691. return rec
  692. }
  693. func (r *ClientRecord) ToClient() *Client {
  694. c := &Client{
  695. ID: r.UUID,
  696. Email: r.Email,
  697. SubID: r.SubID,
  698. Password: r.Password,
  699. Auth: r.Auth,
  700. Flow: r.Flow,
  701. Security: r.Security,
  702. LimitIP: r.LimitIP,
  703. TotalGB: r.TotalGB,
  704. ExpiryTime: r.ExpiryTime,
  705. Enable: r.Enable,
  706. TgID: r.TgID,
  707. Group: r.Group,
  708. Comment: r.Comment,
  709. Reset: r.Reset,
  710. CreatedAt: r.CreatedAt,
  711. UpdatedAt: r.UpdatedAt,
  712. }
  713. if r.Reverse != "" {
  714. var rev ClientReverse
  715. if err := json.Unmarshal([]byte(r.Reverse), &rev); err == nil {
  716. c.Reverse = &rev
  717. }
  718. }
  719. return c
  720. }
  721. type ClientMergeConflict struct {
  722. Field string
  723. Old any
  724. New any
  725. Kept any
  726. }
  727. type OutboundSubscription struct {
  728. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  729. Remark string `json:"remark" form:"remark"`
  730. Url string `json:"url" form:"url"`
  731. Enabled bool `json:"enabled" form:"enabled" gorm:"default:true"`
  732. AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
  733. TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
  734. UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
  735. Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
  736. Prepend bool `json:"prepend" form:"prepend" gorm:"default:false"` // place this subscription's outbounds before the manual template outbounds
  737. LastUpdated int64 `json:"lastUpdated" form:"lastUpdated"`
  738. LastError string `json:"lastError" form:"lastError"`
  739. LastFetchedOutbounds string `json:"lastFetchedOutbounds" form:"lastFetchedOutbounds" gorm:"type:text"`
  740. LinkIdentities string `json:"-" gorm:"type:text;column:link_identities"`
  741. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  742. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  743. OutboundCount int `json:"outboundCount" gorm:"-"`
  744. }
  745. func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientMergeConflict {
  746. var conflicts []ClientMergeConflict
  747. keep := func(field string, oldV, newV, kept any) {
  748. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: oldV, New: newV, Kept: kept})
  749. }
  750. const redacted = "<redacted>"
  751. keepSecret := func(field string) {
  752. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: redacted, New: redacted, Kept: redacted})
  753. }
  754. incomingNewer := incoming.UpdatedAt > existing.UpdatedAt ||
  755. (incoming.UpdatedAt == existing.UpdatedAt && incoming.CreatedAt > existing.CreatedAt)
  756. if existing.UUID != incoming.UUID && incoming.UUID != "" {
  757. if incomingNewer || existing.UUID == "" {
  758. existing.UUID = incoming.UUID
  759. }
  760. keepSecret("uuid")
  761. }
  762. if existing.Password != incoming.Password && incoming.Password != "" {
  763. if incomingNewer || existing.Password == "" {
  764. existing.Password = incoming.Password
  765. keepSecret("password")
  766. }
  767. }
  768. if existing.Auth != incoming.Auth && incoming.Auth != "" {
  769. if incomingNewer || existing.Auth == "" {
  770. existing.Auth = incoming.Auth
  771. keepSecret("auth")
  772. }
  773. }
  774. if existing.Flow != incoming.Flow && incoming.Flow != "" {
  775. if incomingNewer || existing.Flow == "" {
  776. keep("flow", existing.Flow, incoming.Flow, incoming.Flow)
  777. existing.Flow = incoming.Flow
  778. }
  779. }
  780. if existing.Security != incoming.Security && incoming.Security != "" {
  781. if incomingNewer || existing.Security == "" {
  782. keep("security", existing.Security, incoming.Security, incoming.Security)
  783. existing.Security = incoming.Security
  784. }
  785. }
  786. if existing.SubID != incoming.SubID && incoming.SubID != "" {
  787. if incomingNewer || existing.SubID == "" {
  788. existing.SubID = incoming.SubID
  789. keepSecret("subId")
  790. }
  791. }
  792. if existing.TotalGB != incoming.TotalGB {
  793. picked := existing.TotalGB
  794. if existing.TotalGB == 0 || (incoming.TotalGB != 0 && incoming.TotalGB > existing.TotalGB) {
  795. picked = incoming.TotalGB
  796. }
  797. if picked != existing.TotalGB {
  798. keep("totalGB", existing.TotalGB, incoming.TotalGB, picked)
  799. existing.TotalGB = picked
  800. }
  801. }
  802. if existing.ExpiryTime != incoming.ExpiryTime {
  803. picked := existing.ExpiryTime
  804. if existing.ExpiryTime == 0 || (incoming.ExpiryTime != 0 && incoming.ExpiryTime > existing.ExpiryTime) {
  805. picked = incoming.ExpiryTime
  806. }
  807. if picked != existing.ExpiryTime {
  808. keep("expiryTime", existing.ExpiryTime, incoming.ExpiryTime, picked)
  809. existing.ExpiryTime = picked
  810. }
  811. }
  812. if existing.LimitIP != incoming.LimitIP && incoming.LimitIP != 0 {
  813. picked := existing.LimitIP
  814. if existing.LimitIP == 0 || incoming.LimitIP > existing.LimitIP {
  815. picked = incoming.LimitIP
  816. }
  817. if picked != existing.LimitIP {
  818. keep("limitIp", existing.LimitIP, incoming.LimitIP, picked)
  819. existing.LimitIP = picked
  820. }
  821. }
  822. if existing.TgID != incoming.TgID && incoming.TgID != 0 {
  823. if incomingNewer || existing.TgID == 0 {
  824. keep("tgId", existing.TgID, incoming.TgID, incoming.TgID)
  825. existing.TgID = incoming.TgID
  826. }
  827. }
  828. if existing.Reset != incoming.Reset && incoming.Reset != 0 {
  829. if incomingNewer || existing.Reset == 0 {
  830. keep("reset", existing.Reset, incoming.Reset, incoming.Reset)
  831. existing.Reset = incoming.Reset
  832. }
  833. }
  834. if existing.Reverse != incoming.Reverse && incoming.Reverse != "" {
  835. if incomingNewer || existing.Reverse == "" {
  836. keep("reverse", existing.Reverse, incoming.Reverse, incoming.Reverse)
  837. existing.Reverse = incoming.Reverse
  838. }
  839. }
  840. if existing.Comment != incoming.Comment && incoming.Comment != "" {
  841. if incomingNewer || existing.Comment == "" {
  842. keep("comment", existing.Comment, incoming.Comment, incoming.Comment)
  843. existing.Comment = incoming.Comment
  844. }
  845. }
  846. if existing.Group != incoming.Group && incoming.Group != "" {
  847. if incomingNewer || existing.Group == "" {
  848. keep("group", existing.Group, incoming.Group, incoming.Group)
  849. existing.Group = incoming.Group
  850. }
  851. }
  852. if existing.Enable != incoming.Enable {
  853. if incoming.Enable {
  854. if !existing.Enable {
  855. keep("enable", existing.Enable, incoming.Enable, true)
  856. existing.Enable = true
  857. }
  858. }
  859. }
  860. if incoming.CreatedAt != 0 && (existing.CreatedAt == 0 || incoming.CreatedAt < existing.CreatedAt) {
  861. existing.CreatedAt = incoming.CreatedAt
  862. }
  863. if incoming.UpdatedAt > existing.UpdatedAt {
  864. existing.UpdatedAt = incoming.UpdatedAt
  865. }
  866. return conflicts
  867. }