model.go 47 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  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. // ApiTokenUnixMillisecondsThreshold separates legacy millisecond timestamps
  137. // from the seconds-based API token timestamp contract.
  138. const ApiTokenUnixMillisecondsThreshold int64 = 100_000_000_000
  139. type ApiToken struct {
  140. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  141. Name string `json:"name" gorm:"uniqueIndex;not null"`
  142. Token string `json:"token" gorm:"not null"` // SHA-256 hash; the plaintext is shown only once at creation
  143. Enabled bool `json:"enabled" gorm:"default:true"`
  144. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"`
  145. }
  146. // MarshalJSON emits settings, streamSettings, and sniffing as nested JSON
  147. // objects rather than escaped strings, so API consumers don't need to JSON.parse
  148. // a string inside a string. Empty fields render as null; fields whose stored
  149. // text isn't valid JSON fall back to a JSON-encoded string so no data is lost.
  150. func (i Inbound) MarshalJSON() ([]byte, error) {
  151. type alias Inbound
  152. return json.Marshal(struct {
  153. alias
  154. Settings json.RawMessage `json:"settings"`
  155. StreamSettings json.RawMessage `json:"streamSettings"`
  156. Sniffing json.RawMessage `json:"sniffing"`
  157. }{
  158. alias: alias(i),
  159. Settings: jsonStringFieldToRaw(i.Settings),
  160. StreamSettings: jsonStringFieldToRaw(i.StreamSettings),
  161. Sniffing: jsonStringFieldToRaw(i.Sniffing),
  162. })
  163. }
  164. // UnmarshalJSON accepts settings, streamSettings, and sniffing as either a raw
  165. // JSON object/array (the modern shape MarshalJSON emits) or a JSON-encoded
  166. // string (the legacy shape). Either form is normalised back to the JSON-text
  167. // string the DB column stores.
  168. func (i *Inbound) UnmarshalJSON(data []byte) error {
  169. type alias Inbound
  170. aux := struct {
  171. *alias
  172. Settings json.RawMessage `json:"settings"`
  173. StreamSettings json.RawMessage `json:"streamSettings"`
  174. Sniffing json.RawMessage `json:"sniffing"`
  175. }{
  176. alias: (*alias)(i),
  177. }
  178. if err := json.Unmarshal(data, &aux); err != nil {
  179. return err
  180. }
  181. i.Settings = jsonStringFieldFromRaw(aux.Settings)
  182. i.StreamSettings = jsonStringFieldFromRaw(aux.StreamSettings)
  183. i.Sniffing = jsonStringFieldFromRaw(aux.Sniffing)
  184. return nil
  185. }
  186. func jsonStringFieldToRaw(s string) json.RawMessage {
  187. trimmed := strings.TrimSpace(s)
  188. if trimmed == "" {
  189. return json.RawMessage("null")
  190. }
  191. if json.Valid([]byte(trimmed)) {
  192. return json.RawMessage(trimmed)
  193. }
  194. b, _ := json.Marshal(s)
  195. return b
  196. }
  197. func jsonStringFieldFromRaw(r json.RawMessage) string {
  198. trimmed := bytes.TrimSpace(r)
  199. if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
  200. return ""
  201. }
  202. if trimmed[0] == '"' {
  203. var s string
  204. if err := json.Unmarshal(trimmed, &s); err == nil {
  205. return s
  206. }
  207. }
  208. return string(trimmed)
  209. }
  210. // StripInboundXhttpClientFields removes xHTTP knobs that belong on the
  211. // client dialer and subscription share-link extras only. xray-core's XHTTP
  212. // inbound listener does not consume them; the panel still stores them on
  213. // the inbound row so buildXhttpExtra can push defaults to clients.
  214. func StripInboundXhttpClientFields(streamSettings string) (string, bool) {
  215. if streamSettings == "" {
  216. return streamSettings, false
  217. }
  218. var stream map[string]any
  219. if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
  220. return streamSettings, false
  221. }
  222. if stream["network"] != "xhttp" {
  223. return streamSettings, false
  224. }
  225. xhttp, ok := stream["xhttpSettings"].(map[string]any)
  226. if !ok || len(xhttp) == 0 {
  227. return streamSettings, false
  228. }
  229. clientOnly := []string{
  230. "xmux",
  231. "downloadSettings",
  232. "scMinPostsIntervalMs",
  233. "uplinkChunkSize",
  234. "noGRPCHeader",
  235. }
  236. changed := false
  237. for _, key := range clientOnly {
  238. if _, has := xhttp[key]; has {
  239. delete(xhttp, key)
  240. changed = true
  241. }
  242. }
  243. if !changed {
  244. return streamSettings, false
  245. }
  246. out, err := json.MarshalIndent(stream, "", " ")
  247. if err != nil {
  248. return streamSettings, false
  249. }
  250. return string(out), true
  251. }
  252. // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
  253. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
  254. listen := i.Listen
  255. if listen == "" {
  256. listen = "0.0.0.0"
  257. }
  258. listen = fmt.Sprintf("\"%v\"", listen)
  259. protocol := string(i.Protocol)
  260. settings := i.Settings
  261. switch i.Protocol {
  262. case Shadowsocks:
  263. if healed, ok := HealShadowsocksClientMethods(settings); ok {
  264. settings = healed
  265. }
  266. case VMESS:
  267. if stripped, ok := StripVmessClientSecurity(settings); ok {
  268. settings = stripped
  269. }
  270. case VLESS:
  271. if stripped, ok := StripVlessInboundEncryption(settings); ok {
  272. settings = stripped
  273. }
  274. }
  275. streamSettings := i.StreamSettings
  276. if stripped, ok := StripInboundXhttpClientFields(streamSettings); ok {
  277. streamSettings = stripped
  278. }
  279. return &xray.InboundConfig{
  280. Listen: json_util.RawMessage(listen),
  281. Port: i.Port,
  282. Protocol: protocol,
  283. Settings: json_util.RawMessage(settings),
  284. StreamSettings: json_util.RawMessage(streamSettings),
  285. Tag: i.Tag,
  286. Sniffing: json_util.RawMessage(i.Sniffing),
  287. }
  288. }
  289. func StripVmessClientSecurity(settings string) (string, bool) {
  290. if settings == "" {
  291. return settings, false
  292. }
  293. var parsed map[string]any
  294. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  295. return settings, false
  296. }
  297. clients, ok := parsed["clients"].([]any)
  298. if !ok {
  299. return settings, false
  300. }
  301. changed := false
  302. for i := range clients {
  303. cm, ok := clients[i].(map[string]any)
  304. if !ok {
  305. continue
  306. }
  307. if _, has := cm["security"]; has {
  308. delete(cm, "security")
  309. clients[i] = cm
  310. changed = true
  311. }
  312. }
  313. if !changed {
  314. return settings, false
  315. }
  316. out, err := json.MarshalIndent(parsed, "", " ")
  317. if err != nil {
  318. return settings, false
  319. }
  320. return string(out), true
  321. }
  322. func StripVlessInboundEncryption(settings string) (string, bool) {
  323. if settings == "" {
  324. return settings, false
  325. }
  326. var parsed map[string]any
  327. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  328. return settings, false
  329. }
  330. if _, has := parsed["encryption"]; !has {
  331. return settings, false
  332. }
  333. delete(parsed, "encryption")
  334. out, err := json.MarshalIndent(parsed, "", " ")
  335. if err != nil {
  336. return settings, false
  337. }
  338. return string(out), true
  339. }
  340. // HealShadowsocksClientMethods normalises the per-client `method` field
  341. // on a shadowsocks inbound's settings JSON before it leaves for xray-core:
  342. // - Legacy ciphers (aes-*, chacha20-*): every client must carry a
  343. // per-user `method` matching the inbound's top-level method, otherwise
  344. // xray fails with "unsupported cipher method:".
  345. // - Shadowsocks 2022 (2022-blake3-*): xray's multi-user code rejects the
  346. // inbound with "users must have empty method" when a client carries
  347. // one — strip stale entries left over from a switch off a legacy
  348. // cipher.
  349. //
  350. // Returns the rewritten settings string and true when anything changed.
  351. func HealShadowsocksClientMethods(settings string) (string, bool) {
  352. if settings == "" {
  353. return settings, false
  354. }
  355. var parsed map[string]any
  356. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  357. return settings, false
  358. }
  359. method, _ := parsed["method"].(string)
  360. clients, ok := parsed["clients"].([]any)
  361. if !ok {
  362. return settings, false
  363. }
  364. is2022 := strings.HasPrefix(method, "2022-blake3-")
  365. changed := false
  366. for i := range clients {
  367. cm, ok := clients[i].(map[string]any)
  368. if !ok {
  369. continue
  370. }
  371. if is2022 {
  372. if _, hasKey := cm["method"]; hasKey {
  373. delete(cm, "method")
  374. clients[i] = cm
  375. changed = true
  376. }
  377. continue
  378. }
  379. if method == "" {
  380. continue
  381. }
  382. existing, _ := cm["method"].(string)
  383. if existing == method {
  384. continue
  385. }
  386. cm["method"] = method
  387. clients[i] = cm
  388. changed = true
  389. }
  390. if !changed {
  391. return settings, false
  392. }
  393. out, err := json.MarshalIndent(parsed, "", " ")
  394. if err != nil {
  395. return settings, false
  396. }
  397. return string(out), true
  398. }
  399. // GenerateFakeTLSSecret builds an MTProto FakeTLS secret for the given domain:
  400. // the "ee" FakeTLS marker, 16 random bytes, then the domain encoded as hex.
  401. // MTProto is multi-client, so this value belongs to one client: mtg's [secrets]
  402. // config and that client's tg:// link both read it per client.
  403. func GenerateFakeTLSSecret(domain string) string {
  404. return "ee" + mtprotoRandomMiddle() + hex.EncodeToString([]byte(domain))
  405. }
  406. func mtprotoRandomMiddle() string {
  407. buf := make([]byte, 16)
  408. if _, err := rand.Read(buf); err != nil {
  409. panic(fmt.Errorf("mtproto: crypto/rand read failed: %w", err))
  410. }
  411. return hex.EncodeToString(buf)
  412. }
  413. // mtprotoSecretMiddle returns the 16-byte random middle of an existing secret
  414. // when it is well-formed, otherwise a freshly generated one. Reusing the middle
  415. // keeps the secret stable when only the FakeTLS domain changes.
  416. func mtprotoSecretMiddle(secret string) string {
  417. s := secret
  418. if strings.HasPrefix(s, "ee") || strings.HasPrefix(s, "dd") {
  419. s = s[2:]
  420. }
  421. if len(s) >= 32 {
  422. mid := s[:32]
  423. if _, err := hex.DecodeString(mid); err == nil {
  424. return mid
  425. }
  426. }
  427. return mtprotoRandomMiddle()
  428. }
  429. // ValidMtprotoAdTag reports whether a Telegram advertising tag from
  430. // @MTProxybot is well-formed: exactly 16 bytes as 32 hex characters. mtg
  431. // refuses to start (or rejects a live update) on a malformed tag, so every
  432. // write path validates before the tag can reach a generated config.
  433. func ValidMtprotoAdTag(tag string) bool {
  434. if len(tag) != 32 {
  435. return false
  436. }
  437. _, err := hex.DecodeString(tag)
  438. return err == nil
  439. }
  440. // StripMtprotoInboundSecret removes the vestigial inbound-level `secret` from an
  441. // mtproto inbound's settings JSON. MTProto is multi-client: every secret lives on
  442. // a client, and mtg's [secrets] config plus every share link read only the
  443. // per-client secrets. A lingering inbound-level secret is dead data — it once
  444. // leaked into stale links that mtg rejected as "incorrect client random". Returns
  445. // the rewritten settings and true when a `secret` key was removed.
  446. func StripMtprotoInboundSecret(settings string) (string, bool) {
  447. if settings == "" {
  448. return settings, false
  449. }
  450. var parsed map[string]any
  451. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  452. return settings, false
  453. }
  454. if _, ok := parsed["secret"]; !ok {
  455. return settings, false
  456. }
  457. delete(parsed, "secret")
  458. out, err := json.MarshalIndent(parsed, "", " ")
  459. if err != nil {
  460. return settings, false
  461. }
  462. return string(out), true
  463. }
  464. // mtprotoSecretDomain extracts the FakeTLS domain embedded in the tail of a
  465. // secret, returning an empty string when the secret is malformed. Each mtproto
  466. // client carries its own domain inside its secret, so healing preserves it
  467. // instead of forcing every client onto the inbound-level default.
  468. func mtprotoSecretDomain(secret string) string {
  469. s := secret
  470. if strings.HasPrefix(s, "ee") || strings.HasPrefix(s, "dd") {
  471. s = s[2:]
  472. }
  473. if len(s) <= 32 {
  474. return ""
  475. }
  476. decoded, err := hex.DecodeString(s[32:])
  477. if err != nil || len(decoded) == 0 {
  478. return ""
  479. }
  480. return string(decoded)
  481. }
  482. // HealMtprotoClientSecrets normalises every client's FakeTLS secret in an
  483. // mtproto inbound's settings JSON: each secret is rebuilt so it stays a valid
  484. // FakeTLS value, keeping the client's own embedded domain when present and
  485. // falling back to the inbound-level fakeTlsDomain otherwise. Returns the
  486. // rewritten settings and true when anything changed.
  487. func HealMtprotoClientSecrets(settings string) (string, bool) {
  488. if settings == "" {
  489. return settings, false
  490. }
  491. var parsed map[string]any
  492. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  493. return settings, false
  494. }
  495. clients, ok := parsed["clients"].([]any)
  496. if !ok || len(clients) == 0 {
  497. return settings, false
  498. }
  499. defaultDomain, _ := parsed["fakeTlsDomain"].(string)
  500. defaultDomain = strings.TrimSpace(defaultDomain)
  501. changed := false
  502. for _, raw := range clients {
  503. client, ok := raw.(map[string]any)
  504. if !ok {
  505. continue
  506. }
  507. secret, _ := client["secret"].(string)
  508. domain := mtprotoSecretDomain(secret)
  509. if domain == "" {
  510. domain = defaultDomain
  511. }
  512. if domain == "" {
  513. continue
  514. }
  515. expected := "ee" + mtprotoSecretMiddle(secret) + hex.EncodeToString([]byte(domain))
  516. if secret != expected {
  517. client["secret"] = expected
  518. changed = true
  519. }
  520. }
  521. if !changed {
  522. return settings, false
  523. }
  524. out, err := json.MarshalIndent(parsed, "", " ")
  525. if err != nil {
  526. return settings, false
  527. }
  528. return string(out), true
  529. }
  530. // Setting stores key-value configuration settings for the 3x-ui panel.
  531. type Setting struct {
  532. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  533. Key string `json:"key" form:"key" gorm:"index:idx_settings_key"`
  534. Value string `json:"value" form:"value"`
  535. }
  536. // Node represents a remote 3x-ui panel registered with the central panel.
  537. // The central panel polls each node's existing /panel/api/server/status
  538. // endpoint over HTTP using the per-node ApiToken to populate the runtime
  539. // status fields below.
  540. type Node struct {
  541. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"`
  542. Name string `json:"name" form:"name" gorm:"uniqueIndex" validate:"required" example:"de-fra-1"`
  543. Remark string `json:"remark" form:"remark"`
  544. Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https" example:"https"`
  545. Address string `json:"address" form:"address" validate:"required" example:"node1.example.com"`
  546. Port int `json:"port" form:"port" validate:"gte=1,lte=65535" example:"2053"`
  547. BasePath string `json:"basePath" form:"basePath" example:"/"`
  548. ApiToken string `json:"apiToken" form:"apiToken" validate:"required_unless=TlsVerifyMode mtls" example:"abcdef0123456789"`
  549. Enable bool `json:"enable" form:"enable" gorm:"default:true" example:"true"`
  550. AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
  551. TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" gorm:"column:tls_verify_mode;default:verify" validate:"omitempty,oneof=verify skip pin mtls"`
  552. PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256" gorm:"column:pinned_cert_sha256"`
  553. InboundSyncMode string `json:"inboundSyncMode" form:"inboundSyncMode" gorm:"column:inbound_sync_mode;default:all" validate:"omitempty,oneof=all selected"`
  554. InboundTags []string `json:"inboundTags" form:"inboundTags" gorm:"serializer:json;column:inbound_tags"`
  555. OutboundTag string `json:"outboundTag" form:"outboundTag" gorm:"column:outbound_tag"`
  556. // Guid is the remote panel's stable self-identifier (its panelGuid),
  557. // learned from each heartbeat. It is the globally stable node identity used
  558. // to attribute online clients/inbounds to the physical node across a chain
  559. // of nodes (#4983); panel-local autoincrement ids don't survive a hop.
  560. // Observed-state only — never user-edited.
  561. Guid string `json:"guid" gorm:"column:guid;index"`
  562. // Heartbeat-updated fields. UpdatedAt advances on every probe even when
  563. // the row is otherwise unchanged so the UI's "last seen" tooltip is
  564. // truthful without us having to read LastHeartbeat separately.
  565. Status string `json:"status" gorm:"default:unknown" example:"online"` // online|offline|unknown
  566. LastHeartbeat int64 `json:"lastHeartbeat" example:"1700000000"` // unix seconds, 0 = never
  567. LatencyMs int `json:"latencyMs" example:"42"`
  568. XrayVersion string `json:"xrayVersion" example:"25.10.31"`
  569. PanelVersion string `json:"panelVersion" gorm:"column:panel_version" example:"v3.x.x"`
  570. CpuPct float64 `json:"cpuPct" example:"23.5"`
  571. MemPct float64 `json:"memPct" example:"45.1"`
  572. UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
  573. NetUp uint64 `json:"netUp" gorm:"column:net_up" example:"1048576"`
  574. NetDown uint64 `json:"netDown" gorm:"column:net_down" example:"2097152"`
  575. LastError string `json:"lastError"`
  576. // XrayState and XrayError are captured from the remote node's /panel/api/server/status
  577. // during heartbeats. They let the central panel distinguish "panel API reachable"
  578. // (status=online) from "Xray core itself has failed on the node" for monitoring.
  579. XrayState string `json:"xrayState" gorm:"column:xray_state"`
  580. XrayError string `json:"xrayError" gorm:"column:xray_error"`
  581. ConfigDirty bool `json:"configDirty" gorm:"default:false"`
  582. ConfigDirtyAt int64 `json:"configDirtyAt"`
  583. InboundCount int `json:"inboundCount" gorm:"-" example:"5"`
  584. ClientCount int `json:"clientCount" gorm:"-" example:"27"`
  585. OnlineCount int `json:"onlineCount" gorm:"-" example:"3"`
  586. ActiveCount int `json:"activeCount" gorm:"-" example:"23"`
  587. DisabledCount int `json:"disabledCount" gorm:"-" example:"3"`
  588. DepletedCount int `json:"depletedCount" gorm:"-" example:"1"`
  589. // ParentGuid + Transitive are set only when a node is surfaced as part of a
  590. // node tree (#4983): direct nodes carry the master panel's own GUID, a
  591. // transitive sub-node carries its parent node's GUID. Transitive nodes are
  592. // read-only projections (Id == 0, not persisted) — never edited or deployed.
  593. ParentGuid string `json:"parentGuid,omitempty" gorm:"-"`
  594. Transitive bool `json:"transitive,omitempty" gorm:"-"`
  595. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli" example:"1700000000"`
  596. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli" example:"1700000000"`
  597. }
  598. // NodeSummary is the read-only identity of a node as published one hop up: the
  599. // view a panel exposes about the nodes it directly manages, so a master can
  600. // surface transitive sub-nodes in a chained topology (#4983). Counts are
  601. // computed by the consuming master from its own per-GUID data, never trusted
  602. // from the child, so this carries identity/health only.
  603. type NodeSummary struct {
  604. Guid string `json:"guid"`
  605. ParentGuid string `json:"parentGuid"`
  606. Name string `json:"name"`
  607. Address string `json:"address"`
  608. Scheme string `json:"scheme"`
  609. Port int `json:"port"`
  610. Status string `json:"status"`
  611. LastHeartbeat int64 `json:"lastHeartbeat"`
  612. LatencyMs int `json:"latencyMs"`
  613. PanelVersion string `json:"panelVersion"`
  614. XrayVersion string `json:"xrayVersion"`
  615. // XrayState/XrayError forwarded so masters can surface xray failure on transitive sub-nodes too.
  616. XrayState string `json:"xrayState"`
  617. XrayError string `json:"xrayError,omitempty"`
  618. }
  619. type ClientReverse struct {
  620. Tag string `json:"tag"`
  621. }
  622. // Client represents a client configuration for Xray inbounds with traffic limits and settings.
  623. type Client struct {
  624. ID string `json:"id,omitempty"` // Unique client identifier
  625. Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
  626. Password string `json:"password,omitempty"` // Client password
  627. Flow string `json:"flow,omitempty"` // Flow control (XTLS)
  628. Reverse *ClientReverse `json:"reverse,omitempty"` // VLESS simple reverse proxy settings
  629. Auth string `json:"auth,omitempty"` // Auth password (Hysteria)
  630. PrivateKey string `json:"privateKey,omitempty"`
  631. PublicKey string `json:"publicKey,omitempty"`
  632. AllowedIPs []string `json:"allowedIPs,omitempty"`
  633. PreSharedKey string `json:"preSharedKey,omitempty"`
  634. KeepAlive int `json:"keepAlive,omitempty"`
  635. Secret string `json:"secret,omitempty" example:"ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d"`
  636. AdTag string `json:"adTag,omitempty" example:"0123456789abcdef0123456789abcdef"`
  637. Email string `json:"email"` // Client email identifier
  638. LimitIP int `json:"limitIp"` // IP limit for this client
  639. TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
  640. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  641. Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
  642. TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
  643. SubID string `json:"subId" form:"subId"` // Subscription identifier
  644. Group string `json:"group,omitempty" form:"group"` // Logical grouping label
  645. Comment string `json:"comment" form:"comment"` // Client comment
  646. Reset int `json:"reset" form:"reset"` // Reset period in days
  647. CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
  648. UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
  649. }
  650. type ClientRecord struct {
  651. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  652. Email string `json:"email" gorm:"uniqueIndex;not null"`
  653. SubID string `json:"subId" gorm:"index;column:sub_id"`
  654. UUID string `json:"uuid" gorm:"column:uuid"`
  655. Password string `json:"password"`
  656. Auth string `json:"auth"`
  657. Flow string `json:"flow"`
  658. Security string `json:"security"`
  659. Reverse string `json:"reverse" gorm:"column:reverse"`
  660. PrivateKey string `json:"privateKey" gorm:"column:wg_private_key"`
  661. PublicKey string `json:"publicKey" gorm:"column:wg_public_key"`
  662. AllowedIPs string `json:"allowedIPs" gorm:"column:wg_allowed_ips"`
  663. PreSharedKey string `json:"preSharedKey" gorm:"column:wg_pre_shared_key"`
  664. KeepAlive int `json:"keepAlive" gorm:"column:wg_keep_alive;default:0"`
  665. Secret string `json:"secret" gorm:"column:secret"`
  666. AdTag string `json:"adTag" gorm:"column:ad_tag;default:''"`
  667. LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
  668. TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
  669. ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
  670. Enable bool `json:"enable" gorm:"default:true"`
  671. TgID int64 `json:"tgId" gorm:"column:tg_id"`
  672. Group string `json:"group" gorm:"column:group_name;default:'';index:idx_client_record_group"`
  673. Comment string `json:"comment"`
  674. Reset int `json:"reset" gorm:"default:0"`
  675. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  676. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  677. }
  678. func (ClientRecord) TableName() string { return "clients" }
  679. type ClientGroup struct {
  680. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  681. Name string `json:"name" gorm:"uniqueIndex;not null"`
  682. ResetUp int64 `json:"resetUp" gorm:"column:reset_up;default:0"`
  683. ResetDown int64 `json:"resetDown" gorm:"column:reset_down;default:0"`
  684. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  685. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  686. }
  687. func (ClientGroup) TableName() string { return "client_groups" }
  688. // MarshalJSON emits the reverse column as a nested JSON object rather than an
  689. // escaped JSON-text string, matching the same convention Inbound uses for its
  690. // JSON-text columns. Empty storage renders as null.
  691. func (r ClientRecord) MarshalJSON() ([]byte, error) {
  692. type alias ClientRecord
  693. return json.Marshal(struct {
  694. alias
  695. Reverse json.RawMessage `json:"reverse"`
  696. }{
  697. alias: alias(r),
  698. Reverse: jsonStringFieldToRaw(r.Reverse),
  699. })
  700. }
  701. // UnmarshalJSON accepts reverse as either a JSON object (modern shape) or a
  702. // JSON-encoded string (legacy shape).
  703. func (r *ClientRecord) UnmarshalJSON(data []byte) error {
  704. type alias ClientRecord
  705. aux := struct {
  706. *alias
  707. Reverse json.RawMessage `json:"reverse"`
  708. }{
  709. alias: (*alias)(r),
  710. }
  711. if err := json.Unmarshal(data, &aux); err != nil {
  712. return err
  713. }
  714. r.Reverse = jsonStringFieldFromRaw(aux.Reverse)
  715. return nil
  716. }
  717. type ClientInbound struct {
  718. ClientId int `json:"clientId" gorm:"primaryKey;column:client_id;index"`
  719. InboundId int `json:"inboundId" gorm:"primaryKey;column:inbound_id;index"`
  720. FlowOverride string `json:"flowOverride" gorm:"column:flow_override"`
  721. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  722. }
  723. func (ClientInbound) TableName() string { return "client_inbounds" }
  724. // ClientExternalLink is a per-client entry surfaced in the client's
  725. // subscription. Two kinds:
  726. // - "link": a single third-party share link (vless://, vmess://, trojan://,
  727. // ss://, hysteria2://, wireguard://). Emitted verbatim in raw subs; parsed
  728. // into an outbound/proxy for JSON and Clash.
  729. // - "subscription": a remote subscription URL. The panel fetches it (cached),
  730. // decodes its links, and merges them into the client's subscription.
  731. type ClientExternalLink struct {
  732. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  733. ClientId int `json:"clientId" gorm:"index;column:client_id"`
  734. Kind string `json:"kind" gorm:"column:kind"`
  735. Value string `json:"value" gorm:"column:value"`
  736. Remark string `json:"remark" gorm:"column:remark"`
  737. SortIndex int `json:"sortIndex" gorm:"column:sort_index"`
  738. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  739. }
  740. func (ClientExternalLink) TableName() string { return "client_external_links" }
  741. // External link kinds.
  742. const (
  743. ExternalLinkKindLink = "link"
  744. ExternalLinkKindSubscription = "subscription"
  745. )
  746. type InboundFallback struct {
  747. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  748. MasterId int `json:"masterId" gorm:"index;not null;column:master_id"`
  749. ChildId int `json:"childId" gorm:"index;not null;column:child_id"`
  750. Name string `json:"name"`
  751. Alpn string `json:"alpn"`
  752. Path string `json:"path"`
  753. Dest string `json:"dest"`
  754. Xver int `json:"xver"`
  755. SortOrder int `json:"sortOrder" gorm:"default:0;column:sort_order"`
  756. }
  757. func (InboundFallback) TableName() string { return "inbound_fallbacks" }
  758. // Host is an override endpoint attached to an inbound: at subscription time each
  759. // enabled host renders one share link/proxy with its own address/port/TLS/etc.,
  760. // superseding the legacy externalProxy array. Free-JSON fields are stored as
  761. // text and parsed in the sub layer; slice fields use the json serializer.
  762. type Host struct {
  763. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"`
  764. InboundId int `json:"inboundId" form:"inboundId" gorm:"index;not null;column:inbound_id" validate:"required" example:"1"`
  765. SortOrder int `json:"sortOrder" form:"sortOrder" gorm:"default:0;column:sort_order"`
  766. Remark string `json:"remark" form:"remark" validate:"required,max=256" example:"cdn-front"`
  767. ServerDescription string `json:"serverDescription" form:"serverDescription" gorm:"column:server_description" validate:"omitempty,max=64"`
  768. IsDisabled bool `json:"isDisabled" form:"isDisabled" gorm:"default:false;column:is_disabled"`
  769. IsHidden bool `json:"isHidden" form:"isHidden" gorm:"default:false;column:is_hidden"`
  770. Tags []string `json:"tags" form:"tags" gorm:"serializer:json"`
  771. Address string `json:"address" form:"address" example:"cdn.example.com"`
  772. Port int `json:"port" form:"port" gorm:"default:0" validate:"gte=0,lte=65535" example:"8443"`
  773. Security string `json:"security" form:"security" gorm:"default:same" validate:"omitempty,oneof=same tls none reality" example:"same"`
  774. Sni string `json:"sni" form:"sni"`
  775. HostHeader string `json:"hostHeader" form:"hostHeader" gorm:"column:host_header"`
  776. Path string `json:"path" form:"path"`
  777. Alpn []string `json:"alpn" form:"alpn" gorm:"serializer:json"`
  778. Fingerprint string `json:"fingerprint" form:"fingerprint"`
  779. OverrideSniFromAddress bool `json:"overrideSniFromAddress" form:"overrideSniFromAddress" gorm:"column:override_sni_from_address"`
  780. KeepSniBlank bool `json:"keepSniBlank" form:"keepSniBlank" gorm:"column:keep_sni_blank"`
  781. PinnedPeerCertSha256 []string `json:"pinnedPeerCertSha256" form:"pinnedPeerCertSha256" gorm:"serializer:json;column:pinned_peer_cert_sha256"`
  782. VerifyPeerCertByName string `json:"verifyPeerCertByName" form:"verifyPeerCertByName" gorm:"column:verify_peer_cert_by_name"`
  783. AllowInsecure bool `json:"allowInsecure" form:"allowInsecure" gorm:"column:allow_insecure"`
  784. EchConfigList string `json:"echConfigList" form:"echConfigList" gorm:"column:ech_config_list"`
  785. MuxParams string `json:"muxParams" form:"muxParams" gorm:"type:text;column:mux_params"`
  786. SockoptParams string `json:"sockoptParams" form:"sockoptParams" gorm:"type:text;column:sockopt_params"`
  787. // FinalMask is a JSON object of xray finalmask masks (tcp/udp/quicParams),
  788. // merged into this host's JSON-subscription stream. Empty = no override.
  789. FinalMask string `json:"finalMask" form:"finalMask" gorm:"type:text;column:final_mask"`
  790. // Single VLESS route value (0-65535) baked into the subscription UUID's 3rd
  791. // group (bytes 6-7), which xray reads via net.PortFromBytes(id[6:8]). Empty = none.
  792. VlessRoute string `json:"vlessRoute" form:"vlessRoute" gorm:"column:vless_route" example:"443"`
  793. ExcludeFromSubTypes []string `json:"excludeFromSubTypes" form:"excludeFromSubTypes" gorm:"serializer:json;column:exclude_from_sub_types"`
  794. MihomoIpVersion string `json:"mihomoIpVersion" form:"mihomoIpVersion" gorm:"column:mihomo_ip_version" validate:"omitempty,oneof=dual ipv4 ipv6 ipv4-prefer ipv6-prefer"`
  795. MihomoX25519 bool `json:"mihomoX25519" form:"mihomoX25519" gorm:"column:mihomo_x25519"`
  796. ShuffleHost bool `json:"shuffleHost" form:"shuffleHost" gorm:"column:shuffle_host"`
  797. NodeGuids []string `json:"nodeGuids,omitempty" form:"nodeGuids" gorm:"serializer:json;column:node_guids"`
  798. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  799. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  800. }
  801. func (Host) TableName() string { return "hosts" }
  802. func (c *Client) ToRecord() *ClientRecord {
  803. rec := &ClientRecord{
  804. Email: c.Email,
  805. SubID: c.SubID,
  806. UUID: c.ID,
  807. Password: c.Password,
  808. Auth: c.Auth,
  809. Flow: c.Flow,
  810. Security: c.Security,
  811. LimitIP: c.LimitIP,
  812. TotalGB: c.TotalGB,
  813. ExpiryTime: c.ExpiryTime,
  814. Enable: c.Enable,
  815. TgID: c.TgID,
  816. Group: c.Group,
  817. Comment: c.Comment,
  818. Reset: c.Reset,
  819. CreatedAt: c.CreatedAt,
  820. UpdatedAt: c.UpdatedAt,
  821. PrivateKey: c.PrivateKey,
  822. PublicKey: c.PublicKey,
  823. AllowedIPs: strings.Join(c.AllowedIPs, ","),
  824. PreSharedKey: c.PreSharedKey,
  825. KeepAlive: c.KeepAlive,
  826. Secret: c.Secret,
  827. AdTag: c.AdTag,
  828. }
  829. if c.Reverse != nil {
  830. if b, err := json.Marshal(c.Reverse); err == nil {
  831. rec.Reverse = string(b)
  832. }
  833. }
  834. return rec
  835. }
  836. func splitWireguardAllowedIPs(csv string) []string {
  837. if csv == "" {
  838. return nil
  839. }
  840. parts := strings.Split(csv, ",")
  841. out := make([]string, 0, len(parts))
  842. for _, p := range parts {
  843. if trimmed := strings.TrimSpace(p); trimmed != "" {
  844. out = append(out, trimmed)
  845. }
  846. }
  847. if len(out) == 0 {
  848. return nil
  849. }
  850. return out
  851. }
  852. func (r *ClientRecord) ToClient() *Client {
  853. c := &Client{
  854. ID: r.UUID,
  855. Email: r.Email,
  856. SubID: r.SubID,
  857. Password: r.Password,
  858. Auth: r.Auth,
  859. Flow: r.Flow,
  860. Security: r.Security,
  861. LimitIP: r.LimitIP,
  862. TotalGB: r.TotalGB,
  863. ExpiryTime: r.ExpiryTime,
  864. Enable: r.Enable,
  865. TgID: r.TgID,
  866. Group: r.Group,
  867. Comment: r.Comment,
  868. Reset: r.Reset,
  869. CreatedAt: r.CreatedAt,
  870. UpdatedAt: r.UpdatedAt,
  871. PrivateKey: r.PrivateKey,
  872. PublicKey: r.PublicKey,
  873. AllowedIPs: splitWireguardAllowedIPs(r.AllowedIPs),
  874. PreSharedKey: r.PreSharedKey,
  875. KeepAlive: r.KeepAlive,
  876. Secret: r.Secret,
  877. AdTag: r.AdTag,
  878. }
  879. if r.Reverse != "" {
  880. var rev ClientReverse
  881. if err := json.Unmarshal([]byte(r.Reverse), &rev); err == nil {
  882. c.Reverse = &rev
  883. }
  884. }
  885. return c
  886. }
  887. type ClientMergeConflict struct {
  888. Field string
  889. Old any
  890. New any
  891. Kept any
  892. }
  893. type OutboundSubscription struct {
  894. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  895. Remark string `json:"remark" form:"remark"`
  896. Url string `json:"url" form:"url"`
  897. Enabled bool `json:"enabled" form:"enabled" gorm:"default:true"`
  898. AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
  899. TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
  900. UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
  901. Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
  902. Prepend bool `json:"prepend" form:"prepend" gorm:"default:false"` // place this subscription's outbounds before the manual template outbounds
  903. LastUpdated int64 `json:"lastUpdated" form:"lastUpdated"`
  904. LastError string `json:"lastError" form:"lastError"`
  905. LastFetchedOutbounds string `json:"lastFetchedOutbounds" form:"lastFetchedOutbounds" gorm:"type:text"`
  906. LinkIdentities string `json:"-" gorm:"type:text;column:link_identities"`
  907. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  908. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  909. OutboundCount int `json:"outboundCount" gorm:"-"`
  910. }
  911. func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientMergeConflict {
  912. var conflicts []ClientMergeConflict
  913. keep := func(field string, oldV, newV, kept any) {
  914. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: oldV, New: newV, Kept: kept})
  915. }
  916. const redacted = "<redacted>"
  917. keepSecret := func(field string) {
  918. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: redacted, New: redacted, Kept: redacted})
  919. }
  920. incomingNewer := incoming.UpdatedAt > existing.UpdatedAt ||
  921. (incoming.UpdatedAt == existing.UpdatedAt && incoming.CreatedAt > existing.CreatedAt)
  922. if existing.UUID != incoming.UUID && incoming.UUID != "" {
  923. if incomingNewer || existing.UUID == "" {
  924. existing.UUID = incoming.UUID
  925. }
  926. keepSecret("uuid")
  927. }
  928. if existing.Password != incoming.Password && incoming.Password != "" {
  929. if incomingNewer || existing.Password == "" {
  930. existing.Password = incoming.Password
  931. keepSecret("password")
  932. }
  933. }
  934. if existing.Auth != incoming.Auth && incoming.Auth != "" {
  935. if incomingNewer || existing.Auth == "" {
  936. existing.Auth = incoming.Auth
  937. keepSecret("auth")
  938. }
  939. }
  940. if existing.Flow != incoming.Flow && incoming.Flow != "" {
  941. if incomingNewer || existing.Flow == "" {
  942. keep("flow", existing.Flow, incoming.Flow, incoming.Flow)
  943. existing.Flow = incoming.Flow
  944. }
  945. }
  946. if existing.Security != incoming.Security && incoming.Security != "" {
  947. if incomingNewer || existing.Security == "" {
  948. keep("security", existing.Security, incoming.Security, incoming.Security)
  949. existing.Security = incoming.Security
  950. }
  951. }
  952. if existing.SubID != incoming.SubID && incoming.SubID != "" {
  953. if incomingNewer || existing.SubID == "" {
  954. existing.SubID = incoming.SubID
  955. keepSecret("subId")
  956. }
  957. }
  958. if existing.TotalGB != incoming.TotalGB {
  959. picked := existing.TotalGB
  960. if existing.TotalGB == 0 || (incoming.TotalGB != 0 && incoming.TotalGB > existing.TotalGB) {
  961. picked = incoming.TotalGB
  962. }
  963. if picked != existing.TotalGB {
  964. keep("totalGB", existing.TotalGB, incoming.TotalGB, picked)
  965. existing.TotalGB = picked
  966. }
  967. }
  968. if existing.ExpiryTime != incoming.ExpiryTime {
  969. picked := existing.ExpiryTime
  970. if existing.ExpiryTime == 0 || (incoming.ExpiryTime != 0 && incoming.ExpiryTime > existing.ExpiryTime) {
  971. picked = incoming.ExpiryTime
  972. }
  973. if picked != existing.ExpiryTime {
  974. keep("expiryTime", existing.ExpiryTime, incoming.ExpiryTime, picked)
  975. existing.ExpiryTime = picked
  976. }
  977. }
  978. if existing.LimitIP != incoming.LimitIP && incoming.LimitIP != 0 {
  979. picked := existing.LimitIP
  980. if existing.LimitIP == 0 || incoming.LimitIP > existing.LimitIP {
  981. picked = incoming.LimitIP
  982. }
  983. if picked != existing.LimitIP {
  984. keep("limitIp", existing.LimitIP, incoming.LimitIP, picked)
  985. existing.LimitIP = picked
  986. }
  987. }
  988. if existing.TgID != incoming.TgID && incoming.TgID != 0 {
  989. if incomingNewer || existing.TgID == 0 {
  990. keep("tgId", existing.TgID, incoming.TgID, incoming.TgID)
  991. existing.TgID = incoming.TgID
  992. }
  993. }
  994. if existing.Reset != incoming.Reset && incoming.Reset != 0 {
  995. if incomingNewer || existing.Reset == 0 {
  996. keep("reset", existing.Reset, incoming.Reset, incoming.Reset)
  997. existing.Reset = incoming.Reset
  998. }
  999. }
  1000. if existing.Reverse != incoming.Reverse && incoming.Reverse != "" {
  1001. if incomingNewer || existing.Reverse == "" {
  1002. keep("reverse", existing.Reverse, incoming.Reverse, incoming.Reverse)
  1003. existing.Reverse = incoming.Reverse
  1004. }
  1005. }
  1006. if existing.PrivateKey != incoming.PrivateKey && incoming.PrivateKey != "" {
  1007. if incomingNewer || existing.PrivateKey == "" {
  1008. existing.PrivateKey = incoming.PrivateKey
  1009. keepSecret("privateKey")
  1010. }
  1011. }
  1012. if existing.PublicKey != incoming.PublicKey && incoming.PublicKey != "" {
  1013. if incomingNewer || existing.PublicKey == "" {
  1014. existing.PublicKey = incoming.PublicKey
  1015. keepSecret("publicKey")
  1016. }
  1017. }
  1018. if existing.PreSharedKey != incoming.PreSharedKey && incoming.PreSharedKey != "" {
  1019. if incomingNewer || existing.PreSharedKey == "" {
  1020. existing.PreSharedKey = incoming.PreSharedKey
  1021. keepSecret("preSharedKey")
  1022. }
  1023. }
  1024. if existing.Secret != incoming.Secret && incoming.Secret != "" {
  1025. if incomingNewer || existing.Secret == "" {
  1026. existing.Secret = incoming.Secret
  1027. keepSecret("secret")
  1028. }
  1029. }
  1030. if existing.AllowedIPs != incoming.AllowedIPs && incoming.AllowedIPs != "" {
  1031. if incomingNewer || existing.AllowedIPs == "" {
  1032. keep("allowedIPs", existing.AllowedIPs, incoming.AllowedIPs, incoming.AllowedIPs)
  1033. existing.AllowedIPs = incoming.AllowedIPs
  1034. }
  1035. }
  1036. if existing.KeepAlive != incoming.KeepAlive && incoming.KeepAlive != 0 {
  1037. if incomingNewer || existing.KeepAlive == 0 {
  1038. keep("keepAlive", existing.KeepAlive, incoming.KeepAlive, incoming.KeepAlive)
  1039. existing.KeepAlive = incoming.KeepAlive
  1040. }
  1041. }
  1042. if existing.Comment != incoming.Comment && incoming.Comment != "" {
  1043. if incomingNewer || existing.Comment == "" {
  1044. keep("comment", existing.Comment, incoming.Comment, incoming.Comment)
  1045. existing.Comment = incoming.Comment
  1046. }
  1047. }
  1048. if existing.Group != incoming.Group && incoming.Group != "" {
  1049. if incomingNewer || existing.Group == "" {
  1050. keep("group", existing.Group, incoming.Group, incoming.Group)
  1051. existing.Group = incoming.Group
  1052. }
  1053. }
  1054. if existing.Enable != incoming.Enable {
  1055. if incoming.Enable {
  1056. if !existing.Enable {
  1057. keep("enable", existing.Enable, incoming.Enable, true)
  1058. existing.Enable = true
  1059. }
  1060. }
  1061. }
  1062. if incoming.CreatedAt != 0 && (existing.CreatedAt == 0 || incoming.CreatedAt < existing.CreatedAt) {
  1063. existing.CreatedAt = incoming.CreatedAt
  1064. }
  1065. if incoming.UpdatedAt > existing.UpdatedAt {
  1066. existing.UpdatedAt = incoming.UpdatedAt
  1067. }
  1068. return conflicts
  1069. }