model.go 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458
  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. AmneziaWG Protocol = "amneziawg"
  32. )
  33. // User represents a user account in the 3x-ui panel.
  34. type User struct {
  35. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  36. Username string `json:"username"`
  37. Password string `json:"password"`
  38. LoginEpoch int64 `json:"-" gorm:"default:0"`
  39. }
  40. // Inbound represents an Xray inbound configuration with traffic statistics and settings.
  41. type Inbound struct {
  42. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"` // Unique identifier
  43. UserId int `json:"-"` // Associated user ID
  44. Up int64 `json:"up" form:"up"` // Upload traffic in bytes
  45. Down int64 `json:"down" form:"down"` // Download traffic in bytes
  46. Total int64 `json:"total" form:"total"` // Total traffic limit in bytes
  47. Remark string `json:"remark" form:"remark" example:"VLESS-443"` // Human-readable remark
  48. 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)
  49. Enable bool `json:"enable" form:"enable" gorm:"index:idx_enable_traffic_reset,priority:1" example:"true"` // Whether the inbound is enabled
  50. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  51. 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
  52. TrafficResetDay int `json:"trafficResetDay" form:"trafficResetDay" gorm:"default:1" validate:"omitempty,gte=1,lte=31" example:"1"` // Day of month for monthly traffic resets
  53. LastTrafficResetTime int64 `json:"lastTrafficResetTime" form:"lastTrafficResetTime" gorm:"default:0"` // Last traffic reset timestamp
  54. ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` // Client traffic statistics
  55. // Xray configuration fields
  56. Listen string `json:"listen" form:"listen"`
  57. Port int `json:"port" form:"port" validate:"gte=0,lte=65535" example:"443"`
  58. Protocol Protocol `json:"protocol" form:"protocol" validate:"required,oneof=vmess vless trojan shadowsocks wireguard hysteria http mixed tunnel tun mtproto amneziawg" example:"vless"`
  59. Settings string `json:"settings" form:"settings"`
  60. StreamSettings string `json:"streamSettings" form:"streamSettings"`
  61. Tag string `json:"tag" form:"tag" gorm:"unique" example:"in-443-tcp"`
  62. Sniffing string `json:"sniffing" form:"sniffing"`
  63. NodeID *int `json:"nodeId,omitempty" form:"nodeId" gorm:"index"`
  64. ShareAddrStrategy string `json:"shareAddrStrategy" form:"shareAddrStrategy" gorm:"column:share_addr_strategy;default:node" validate:"omitempty,oneof=node listen custom"`
  65. ShareAddr string `json:"shareAddr" form:"shareAddr" gorm:"column:share_addr"`
  66. DisableFlow bool `json:"disableFlow" form:"disableFlow" gorm:"column:disable_flow;default:false" example:"false"`
  67. // OriginNodeGuid is the panelGuid of the node that physically hosts this
  68. // inbound, propagated up across hops (#4983). Empty for an inbound that
  69. // lives on this panel's own xray; set to the originating node's GUID when
  70. // the inbound was synced from a node (kept as-is across further hops). Lets
  71. // the master attribute a deeply nested inbound to the real node instead of
  72. // the intermediate one it was fetched through.
  73. OriginNodeGuid string `json:"originNodeGuid,omitempty" form:"originNodeGuid" gorm:"column:origin_node_guid;index"`
  74. // FallbackParent is populated by the API layer when this inbound is
  75. // attached as a fallback child of a VLESS/Trojan TCP-TLS master.
  76. // The frontend uses it to rewrite client-share links so they advertise
  77. // the master's externally reachable endpoint instead of the child's
  78. // loopback listen. Not persisted.
  79. FallbackParent *FallbackParentInfo `json:"fallbackParent,omitempty" gorm:"-"`
  80. }
  81. // FallbackParentInfo carries everything the frontend needs to rewrite a
  82. // child inbound's client link: where to connect (the master's address
  83. // and port) and which path matched on the master's fallbacks array.
  84. // The frontend already has the master inbound in its dbInbounds list,
  85. // so we only ship identifiers + the match path here.
  86. type FallbackParentInfo struct {
  87. MasterId int `json:"masterId"`
  88. Path string `json:"path,omitempty"`
  89. }
  90. // OutboundTraffics tracks traffic statistics for Xray outbound connections.
  91. type OutboundTraffics struct {
  92. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  93. Tag string `json:"tag" form:"tag" gorm:"unique"`
  94. Up int64 `json:"up" form:"up" gorm:"default:0"`
  95. Down int64 `json:"down" form:"down" gorm:"default:0"`
  96. Total int64 `json:"total" form:"total" gorm:"default:0"`
  97. }
  98. // InboundClientIps stores IP addresses associated with inbound clients for access control.
  99. type InboundClientIps struct {
  100. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  101. ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
  102. Ips string `json:"ips" form:"ips"`
  103. }
  104. // MarshalJSON emits the Ips column as a real JSON array instead of an escaped
  105. // JSON-text string. Empty or unparseable storage renders as null so API
  106. // consumers don't have to special-case the legacy double-encoded shape.
  107. func (ic InboundClientIps) MarshalJSON() ([]byte, error) {
  108. type alias InboundClientIps
  109. return json.Marshal(struct {
  110. alias
  111. Ips json.RawMessage `json:"ips"`
  112. }{
  113. alias: alias(ic),
  114. Ips: jsonStringFieldToRaw(ic.Ips),
  115. })
  116. }
  117. // UnmarshalJSON accepts ips as either a JSON array (modern shape) or a
  118. // JSON-encoded string (legacy shape), normalising back to the JSON-text the
  119. // column stores.
  120. func (ic *InboundClientIps) UnmarshalJSON(data []byte) error {
  121. type alias InboundClientIps
  122. aux := struct {
  123. *alias
  124. Ips json.RawMessage `json:"ips"`
  125. }{
  126. alias: (*alias)(ic),
  127. }
  128. if err := json.Unmarshal(data, &aux); err != nil {
  129. return err
  130. }
  131. ic.Ips = jsonStringFieldFromRaw(aux.Ips)
  132. return nil
  133. }
  134. // HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.
  135. type HistoryOfSeeders struct {
  136. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  137. SeederName string `json:"seederName"`
  138. }
  139. // ApiTokenUnixMillisecondsThreshold separates legacy millisecond timestamps
  140. // from the seconds-based API token timestamp contract.
  141. const ApiTokenUnixMillisecondsThreshold int64 = 100_000_000_000
  142. const (
  143. ApiScopeAdmin = "admin"
  144. ApiScopeMonitor = "monitor"
  145. ApiScopeNodeSync = "node-sync"
  146. )
  147. func IsKnownApiScope(s string) bool {
  148. return s == ApiScopeAdmin || s == ApiScopeMonitor || s == ApiScopeNodeSync
  149. }
  150. type ApiToken struct {
  151. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  152. Name string `json:"name" gorm:"uniqueIndex;not null"`
  153. Token string `json:"token" gorm:"not null"` // SHA-256 hash; the plaintext is shown only once at creation
  154. Enabled bool `json:"enabled" gorm:"default:true"`
  155. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime"`
  156. Scope string `json:"scope" gorm:"not null;default:admin"`
  157. ExpiresAt int64 `json:"expiresAt" gorm:"not null;default:0"`
  158. }
  159. // MarshalJSON emits settings, streamSettings, and sniffing as nested JSON
  160. // objects rather than escaped strings, so API consumers don't need to JSON.parse
  161. // a string inside a string. Empty fields render as null; fields whose stored
  162. // text isn't valid JSON fall back to a JSON-encoded string so no data is lost.
  163. func (i Inbound) MarshalJSON() ([]byte, error) {
  164. type alias Inbound
  165. return json.Marshal(struct {
  166. alias
  167. Settings json.RawMessage `json:"settings"`
  168. StreamSettings json.RawMessage `json:"streamSettings"`
  169. Sniffing json.RawMessage `json:"sniffing"`
  170. }{
  171. alias: alias(i),
  172. Settings: jsonStringFieldToRaw(i.Settings),
  173. StreamSettings: jsonStringFieldToRaw(i.StreamSettings),
  174. Sniffing: jsonStringFieldToRaw(i.Sniffing),
  175. })
  176. }
  177. // UnmarshalJSON accepts settings, streamSettings, and sniffing as either a raw
  178. // JSON object/array (the modern shape MarshalJSON emits) or a JSON-encoded
  179. // string (the legacy shape). Either form is normalised back to the JSON-text
  180. // string the DB column stores.
  181. func (i *Inbound) UnmarshalJSON(data []byte) error {
  182. type alias Inbound
  183. aux := struct {
  184. *alias
  185. Settings json.RawMessage `json:"settings"`
  186. StreamSettings json.RawMessage `json:"streamSettings"`
  187. Sniffing json.RawMessage `json:"sniffing"`
  188. }{
  189. alias: (*alias)(i),
  190. }
  191. if err := json.Unmarshal(data, &aux); err != nil {
  192. return err
  193. }
  194. i.Settings = jsonStringFieldFromRaw(aux.Settings)
  195. i.StreamSettings = jsonStringFieldFromRaw(aux.StreamSettings)
  196. i.Sniffing = jsonStringFieldFromRaw(aux.Sniffing)
  197. return nil
  198. }
  199. func jsonStringFieldToRaw(s string) json.RawMessage {
  200. trimmed := strings.TrimSpace(s)
  201. if trimmed == "" {
  202. return json.RawMessage("null")
  203. }
  204. if json.Valid([]byte(trimmed)) {
  205. return json.RawMessage(trimmed)
  206. }
  207. b, _ := json.Marshal(s)
  208. return b
  209. }
  210. func jsonStringFieldFromRaw(r json.RawMessage) string {
  211. trimmed := bytes.TrimSpace(r)
  212. if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
  213. return ""
  214. }
  215. if trimmed[0] == '"' {
  216. var s string
  217. if err := json.Unmarshal(trimmed, &s); err == nil {
  218. return s
  219. }
  220. }
  221. return string(trimmed)
  222. }
  223. // hysteriaConfigVersion is the only hysteria version xray-core builds. Both
  224. // the protocol settings and the transport settings answer anything else with
  225. // "version != 2", and that error rejects the whole config — every other
  226. // inbound on the server goes down with it, not just the hysteria one.
  227. const hysteriaConfigVersion = 2
  228. // HealHysteriaVersion pins a hysteria inbound's settings.version to the
  229. // version xray-core accepts. Rows written before the panel settled on v2, or
  230. // through the API and the raw JSON editor, can still carry the legacy 1 or no
  231. // version at all, either of which stops the core from starting.
  232. func HealHysteriaVersion(settings string) (string, bool) {
  233. return healVersionField(settings, nil)
  234. }
  235. // HealHysteriaStreamVersion does the same for the transport half,
  236. // streamSettings.hysteriaSettings.version, which xray-core validates
  237. // separately. An absent hysteriaSettings object is left alone.
  238. func HealHysteriaStreamVersion(streamSettings string) (string, bool) {
  239. return healVersionField(streamSettings, []string{"hysteriaSettings"})
  240. }
  241. // healVersionField rewrites the "version" key of the object reached by path to
  242. // hysteriaConfigVersion, reporting whether anything changed. A path that does
  243. // not resolve to an object leaves the input untouched.
  244. func healVersionField(raw string, path []string) (string, bool) {
  245. if raw == "" {
  246. return raw, false
  247. }
  248. var parsed map[string]any
  249. if err := json.Unmarshal([]byte(raw), &parsed); err != nil {
  250. return raw, false
  251. }
  252. target := parsed
  253. for _, key := range path {
  254. next, ok := target[key].(map[string]any)
  255. if !ok {
  256. return raw, false
  257. }
  258. target = next
  259. }
  260. if version, ok := target["version"].(float64); ok && version == hysteriaConfigVersion {
  261. return raw, false
  262. }
  263. target["version"] = hysteriaConfigVersion
  264. out, err := json.MarshalIndent(parsed, "", " ")
  265. if err != nil {
  266. return raw, false
  267. }
  268. return string(out), true
  269. }
  270. // StripInboundXhttpClientFields removes xHTTP knobs that belong on the
  271. // client dialer and subscription share-link extras only. xray-core's XHTTP
  272. // inbound listener does not consume them; the panel still stores them on
  273. // the inbound row so buildXhttpExtra can push defaults to clients.
  274. func StripInboundXhttpClientFields(streamSettings string) (string, bool) {
  275. if streamSettings == "" {
  276. return streamSettings, false
  277. }
  278. var stream map[string]any
  279. if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil {
  280. return streamSettings, false
  281. }
  282. if stream["network"] != "xhttp" {
  283. return streamSettings, false
  284. }
  285. xhttp, ok := stream["xhttpSettings"].(map[string]any)
  286. if !ok || len(xhttp) == 0 {
  287. return streamSettings, false
  288. }
  289. clientOnly := []string{
  290. "xmux",
  291. "downloadSettings",
  292. "scMinPostsIntervalMs",
  293. "uplinkChunkSize",
  294. "noGRPCHeader",
  295. }
  296. changed := false
  297. for _, key := range clientOnly {
  298. if _, has := xhttp[key]; has {
  299. delete(xhttp, key)
  300. changed = true
  301. }
  302. }
  303. if !changed {
  304. return streamSettings, false
  305. }
  306. out, err := json.MarshalIndent(stream, "", " ")
  307. if err != nil {
  308. return streamSettings, false
  309. }
  310. return string(out), true
  311. }
  312. // GenXrayInboundConfig generates an Xray inbound configuration from the Inbound model.
  313. func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
  314. listen := i.Listen
  315. if listen == "" {
  316. listen = "0.0.0.0"
  317. }
  318. listen = fmt.Sprintf("\"%v\"", listen)
  319. protocol := string(i.Protocol)
  320. settings := i.Settings
  321. switch i.Protocol {
  322. case Shadowsocks:
  323. if healed, ok := HealShadowsocksClientMethods(settings); ok {
  324. settings = healed
  325. }
  326. case VMESS:
  327. if stripped, ok := StripVmessClientSecurity(settings); ok {
  328. settings = stripped
  329. }
  330. case VLESS:
  331. if stripped, ok := StripVlessInboundEncryption(settings); ok {
  332. settings = stripped
  333. }
  334. case WireGuard:
  335. if converted, ok := WireguardClientsToPeers(settings); ok {
  336. settings = converted
  337. }
  338. case Hysteria:
  339. if healed, ok := HealHysteriaVersion(settings); ok {
  340. settings = healed
  341. }
  342. }
  343. streamSettings := i.StreamSettings
  344. if stripped, ok := StripInboundXhttpClientFields(streamSettings); ok {
  345. streamSettings = stripped
  346. }
  347. if i.Protocol == Hysteria {
  348. if healed, ok := HealHysteriaStreamVersion(streamSettings); ok {
  349. streamSettings = healed
  350. }
  351. }
  352. return &xray.InboundConfig{
  353. Listen: json_util.RawMessage(listen),
  354. Port: i.Port,
  355. Protocol: protocol,
  356. Settings: json_util.RawMessage(settings),
  357. StreamSettings: json_util.RawMessage(streamSettings),
  358. Tag: i.Tag,
  359. Sniffing: json_util.RawMessage(i.Sniffing),
  360. }
  361. }
  362. func StripVmessClientSecurity(settings string) (string, bool) {
  363. if settings == "" {
  364. return settings, false
  365. }
  366. var parsed map[string]any
  367. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  368. return settings, false
  369. }
  370. clients, ok := parsed["clients"].([]any)
  371. if !ok {
  372. return settings, false
  373. }
  374. changed := false
  375. for i := range clients {
  376. cm, ok := clients[i].(map[string]any)
  377. if !ok {
  378. continue
  379. }
  380. if _, has := cm["security"]; has {
  381. delete(cm, "security")
  382. clients[i] = cm
  383. changed = true
  384. }
  385. }
  386. if !changed {
  387. return settings, false
  388. }
  389. out, err := json.MarshalIndent(parsed, "", " ")
  390. if err != nil {
  391. return settings, false
  392. }
  393. return string(out), true
  394. }
  395. // WireguardPeerFromClient builds the xray wireguard inbound peer object for one
  396. // WireGuard client. It is the single definition of the peer shape, shared by the
  397. // full-config path (XrayService.GetXrayConfig) and the live AddInbound path
  398. // (WireguardClientsToPeers), so both emit identical peers. The client's
  399. // privateKey is intentionally omitted — it is the client's secret, not part of
  400. // the server-side peer.
  401. func WireguardPeerFromClient(c Client) map[string]any {
  402. peer := map[string]any{"email": c.Email, "level": 0}
  403. if c.PublicKey != "" {
  404. peer["publicKey"] = c.PublicKey
  405. }
  406. if len(c.AllowedIPs) > 0 {
  407. peer["allowedIPs"] = c.AllowedIPs
  408. }
  409. if c.PreSharedKey != "" {
  410. peer["preSharedKey"] = c.PreSharedKey
  411. }
  412. if c.KeepAlive > 0 {
  413. peer["keepAlive"] = c.KeepAlive
  414. }
  415. return peer
  416. }
  417. // WireguardClientsToPeers rewrites a WireGuard inbound's settings JSON from the
  418. // panel's client representation into the peers array xray-core's wireguard
  419. // inbound expects. The panel stores WireGuard clients under "clients" (the shape
  420. // every other protocol uses); xray is configured with "peers". GetXrayConfig
  421. // already does this conversion when it builds the full config, but the live
  422. // gRPC AddInbound paths (inbound create/edit and node reconcile) go through
  423. // GenXrayInboundConfig directly — without the conversion they re-add the
  424. // wireguard inbound with no peers, dropping every connected client until the
  425. // next full restart. Clients are the source of truth and are always rebuilt
  426. // into peers (matching GetXrayConfig), so the panel's empty "peers" placeholder
  427. // never blocks the conversion. Idempotent: converting removes "clients", so a
  428. // second call is a no-op, as is any inbound that carries no "clients".
  429. func WireguardClientsToPeers(settings string) (string, bool) {
  430. if settings == "" {
  431. return settings, false
  432. }
  433. var parsed map[string]any
  434. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  435. return settings, false
  436. }
  437. clients, ok := parsed["clients"].([]any)
  438. if !ok {
  439. return settings, false
  440. }
  441. peers := make([]any, 0, len(clients))
  442. for _, raw := range clients {
  443. cm, ok := raw.(map[string]any)
  444. if !ok {
  445. continue
  446. }
  447. if enable, ok := cm["enable"].(bool); ok && !enable {
  448. continue
  449. }
  450. encoded, err := json.Marshal(cm)
  451. if err != nil {
  452. continue
  453. }
  454. var c Client
  455. if err := json.Unmarshal(encoded, &c); err != nil {
  456. continue
  457. }
  458. peers = append(peers, WireguardPeerFromClient(c))
  459. }
  460. delete(parsed, "clients")
  461. parsed["peers"] = peers
  462. out, err := json.MarshalIndent(parsed, "", " ")
  463. if err != nil {
  464. return settings, false
  465. }
  466. return string(out), true
  467. }
  468. func StripVlessInboundEncryption(settings string) (string, bool) {
  469. if settings == "" {
  470. return settings, false
  471. }
  472. var parsed map[string]any
  473. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  474. return settings, false
  475. }
  476. if _, has := parsed["encryption"]; !has {
  477. return settings, false
  478. }
  479. delete(parsed, "encryption")
  480. out, err := json.MarshalIndent(parsed, "", " ")
  481. if err != nil {
  482. return settings, false
  483. }
  484. return string(out), true
  485. }
  486. // ReplaceRemovedShadowsocksCipher maps ciphers that xray-core v26.7.11
  487. // deleted ("none"/"plain" make the whole config fail with "unknown cipher
  488. // method") to a still-supported replacement. Returns the replacement and
  489. // true when the given method is one of the removed ciphers.
  490. func ReplaceRemovedShadowsocksCipher(method string) (string, bool) {
  491. switch method {
  492. case "none", "plain":
  493. return "chacha20-ietf-poly1305", true
  494. }
  495. return method, false
  496. }
  497. // HealShadowsocksClientMethods normalises the `method` fields on a
  498. // shadowsocks inbound's settings JSON before it leaves for xray-core:
  499. // - Ciphers removed upstream (none/plain): rewritten via
  500. // ReplaceRemovedShadowsocksCipher so one legacy row cannot prevent
  501. // xray from starting.
  502. // - Legacy ciphers (aes-*, chacha20-*): every client must carry a
  503. // per-user `method` matching the inbound's top-level method, otherwise
  504. // xray fails with "unsupported cipher method:".
  505. // - Shadowsocks 2022 (2022-blake3-*): xray's multi-user code rejects the
  506. // inbound with "users must have empty method" when a client carries
  507. // one — strip stale entries left over from a switch off a legacy
  508. // cipher.
  509. //
  510. // Returns the rewritten settings string and true when anything changed.
  511. func HealShadowsocksClientMethods(settings string) (string, bool) {
  512. if settings == "" {
  513. return settings, false
  514. }
  515. var parsed map[string]any
  516. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  517. return settings, false
  518. }
  519. method, _ := parsed["method"].(string)
  520. changed := false
  521. if replacement, removed := ReplaceRemovedShadowsocksCipher(method); removed {
  522. method = replacement
  523. parsed["method"] = method
  524. changed = true
  525. }
  526. clients, ok := parsed["clients"].([]any)
  527. if !ok {
  528. if !changed {
  529. return settings, false
  530. }
  531. out, err := json.MarshalIndent(parsed, "", " ")
  532. if err != nil {
  533. return settings, false
  534. }
  535. return string(out), true
  536. }
  537. is2022 := strings.HasPrefix(method, "2022-blake3-")
  538. for i := range clients {
  539. cm, ok := clients[i].(map[string]any)
  540. if !ok {
  541. continue
  542. }
  543. if is2022 {
  544. if _, hasKey := cm["method"]; hasKey {
  545. delete(cm, "method")
  546. clients[i] = cm
  547. changed = true
  548. }
  549. continue
  550. }
  551. if method == "" {
  552. continue
  553. }
  554. existing, _ := cm["method"].(string)
  555. if existing == method {
  556. continue
  557. }
  558. cm["method"] = method
  559. clients[i] = cm
  560. changed = true
  561. }
  562. if !changed {
  563. return settings, false
  564. }
  565. out, err := json.MarshalIndent(parsed, "", " ")
  566. if err != nil {
  567. return settings, false
  568. }
  569. return string(out), true
  570. }
  571. // GenerateFakeTLSSecret builds an MTProto FakeTLS secret for the given domain:
  572. // the "ee" FakeTLS marker, 16 random bytes, then the domain encoded as hex.
  573. // MTProto is multi-client, so this value belongs to one client: mtg's [secrets]
  574. // config and that client's tg:// link both read it per client.
  575. func GenerateFakeTLSSecret(domain string) string {
  576. return "ee" + mtprotoRandomMiddle() + hex.EncodeToString([]byte(domain))
  577. }
  578. func mtprotoRandomMiddle() string {
  579. buf := make([]byte, 16)
  580. if _, err := rand.Read(buf); err != nil {
  581. panic(fmt.Errorf("mtproto: crypto/rand read failed: %w", err))
  582. }
  583. return hex.EncodeToString(buf)
  584. }
  585. // mtprotoSecretMiddle returns the 16-byte random middle of an existing secret
  586. // when it is well-formed, otherwise a freshly generated one. Reusing the middle
  587. // keeps the secret stable when only the FakeTLS domain changes.
  588. func mtprotoSecretMiddle(secret string) string {
  589. s := secret
  590. if strings.HasPrefix(s, "ee") || strings.HasPrefix(s, "dd") {
  591. s = s[2:]
  592. }
  593. if len(s) >= 32 {
  594. mid := s[:32]
  595. if _, err := hex.DecodeString(mid); err == nil {
  596. return mid
  597. }
  598. }
  599. return mtprotoRandomMiddle()
  600. }
  601. // ValidMtprotoAdTag reports whether a Telegram advertising tag from
  602. // @MTProxybot is well-formed: exactly 16 bytes as 32 hex characters. mtg
  603. // refuses to start (or rejects a live update) on a malformed tag, so every
  604. // write path validates before the tag can reach a generated config.
  605. func ValidMtprotoAdTag(tag string) bool {
  606. if len(tag) != 32 {
  607. return false
  608. }
  609. _, err := hex.DecodeString(tag)
  610. return err == nil
  611. }
  612. // StripMtprotoInboundSecret removes the vestigial inbound-level `secret` from an
  613. // mtproto inbound's settings JSON. MTProto is multi-client: every secret lives on
  614. // a client, and mtg's [secrets] config plus every share link read only the
  615. // per-client secrets. A lingering inbound-level secret is dead data — it once
  616. // leaked into stale links that mtg rejected as "incorrect client random". Returns
  617. // the rewritten settings and true when a `secret` key was removed.
  618. func StripMtprotoInboundSecret(settings string) (string, bool) {
  619. if settings == "" {
  620. return settings, false
  621. }
  622. var parsed map[string]any
  623. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  624. return settings, false
  625. }
  626. if _, ok := parsed["secret"]; !ok {
  627. return settings, false
  628. }
  629. delete(parsed, "secret")
  630. out, err := json.MarshalIndent(parsed, "", " ")
  631. if err != nil {
  632. return settings, false
  633. }
  634. return string(out), true
  635. }
  636. // StripMtprotoInboundAdTag drops the dead inbound-level `adTag` — tags live on clients.
  637. func StripMtprotoInboundAdTag(settings string) (string, bool) {
  638. if settings == "" {
  639. return settings, false
  640. }
  641. var parsed map[string]any
  642. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  643. return settings, false
  644. }
  645. if _, ok := parsed["adTag"]; !ok {
  646. return settings, false
  647. }
  648. delete(parsed, "adTag")
  649. out, err := json.MarshalIndent(parsed, "", " ")
  650. if err != nil {
  651. return settings, false
  652. }
  653. return string(out), true
  654. }
  655. // mtprotoSecretDomain extracts the FakeTLS domain embedded in the tail of a
  656. // secret, returning an empty string when the secret is malformed. Each mtproto
  657. // client carries its own domain inside its secret, so healing preserves it
  658. // instead of forcing every client onto the inbound-level default.
  659. func mtprotoSecretDomain(secret string) string {
  660. s := secret
  661. if strings.HasPrefix(s, "ee") || strings.HasPrefix(s, "dd") {
  662. s = s[2:]
  663. }
  664. if len(s) <= 32 {
  665. return ""
  666. }
  667. decoded, err := hex.DecodeString(s[32:])
  668. if err != nil || len(decoded) == 0 {
  669. return ""
  670. }
  671. return string(decoded)
  672. }
  673. // HealMtprotoClientSecrets normalises every client's FakeTLS secret in an
  674. // mtproto inbound's settings JSON: each secret is rebuilt so it stays a valid
  675. // FakeTLS value, keeping the client's own embedded domain when present and
  676. // falling back to the inbound-level fakeTlsDomain otherwise. Returns the
  677. // rewritten settings and true when anything changed.
  678. func HealMtprotoClientSecrets(settings string) (string, bool) {
  679. if settings == "" {
  680. return settings, false
  681. }
  682. var parsed map[string]any
  683. if err := json.Unmarshal([]byte(settings), &parsed); err != nil {
  684. return settings, false
  685. }
  686. clients, ok := parsed["clients"].([]any)
  687. if !ok || len(clients) == 0 {
  688. return settings, false
  689. }
  690. defaultDomain, _ := parsed["fakeTlsDomain"].(string)
  691. defaultDomain = strings.TrimSpace(defaultDomain)
  692. changed := false
  693. for _, raw := range clients {
  694. client, ok := raw.(map[string]any)
  695. if !ok {
  696. continue
  697. }
  698. secret, _ := client["secret"].(string)
  699. domain := mtprotoSecretDomain(secret)
  700. if domain == "" {
  701. domain = defaultDomain
  702. }
  703. if domain == "" {
  704. continue
  705. }
  706. expected := "ee" + mtprotoSecretMiddle(secret) + hex.EncodeToString([]byte(domain))
  707. if secret != expected {
  708. client["secret"] = expected
  709. changed = true
  710. }
  711. }
  712. if !changed {
  713. return settings, false
  714. }
  715. out, err := json.MarshalIndent(parsed, "", " ")
  716. if err != nil {
  717. return settings, false
  718. }
  719. return string(out), true
  720. }
  721. // Setting stores key-value configuration settings for the 3x-ui panel.
  722. type Setting struct {
  723. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  724. Key string `json:"key" form:"key" gorm:"index:idx_settings_key"`
  725. Value string `json:"value" form:"value"`
  726. }
  727. // Node represents a remote 3x-ui panel registered with the central panel.
  728. // The central panel polls each node's existing /panel/api/server/status
  729. // endpoint over HTTP using the per-node ApiToken to populate the runtime
  730. // status fields below.
  731. type Node struct {
  732. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"`
  733. Name string `json:"name" form:"name" gorm:"uniqueIndex" validate:"required" example:"de-fra-1"`
  734. Remark string `json:"remark" form:"remark"`
  735. Scheme string `json:"scheme" form:"scheme" validate:"omitempty,oneof=http https" example:"https"`
  736. Address string `json:"address" form:"address" validate:"required" example:"node1.example.com"`
  737. Port int `json:"port" form:"port" validate:"gte=1,lte=65535" example:"2053"`
  738. BasePath string `json:"basePath" form:"basePath" example:"/"`
  739. ApiToken string `json:"-" form:"-" gorm:"column:api_token" validate:"required_unless=TlsVerifyMode mtls" example:"abcdef0123456789"`
  740. Enable bool `json:"enable" form:"enable" gorm:"default:true" example:"true"`
  741. AllowPrivateAddress bool `json:"allowPrivateAddress" form:"allowPrivateAddress" gorm:"default:false"`
  742. TlsVerifyMode string `json:"tlsVerifyMode" form:"tlsVerifyMode" gorm:"column:tls_verify_mode;default:verify" validate:"omitempty,oneof=verify skip pin mtls"`
  743. PinnedCertSha256 string `json:"pinnedCertSha256" form:"pinnedCertSha256" gorm:"column:pinned_cert_sha256"`
  744. InboundSyncMode string `json:"inboundSyncMode" form:"inboundSyncMode" gorm:"column:inbound_sync_mode;default:all" validate:"omitempty,oneof=all selected"`
  745. InboundTags []string `json:"inboundTags" form:"inboundTags" gorm:"serializer:json;column:inbound_tags"`
  746. OutboundTag string `json:"outboundTag" form:"outboundTag" gorm:"column:outbound_tag"`
  747. // Guid is the remote panel's stable self-identifier (its panelGuid),
  748. // learned from each heartbeat. It is the globally stable node identity used
  749. // to attribute online clients/inbounds to the physical node across a chain
  750. // of nodes (#4983); panel-local autoincrement ids don't survive a hop.
  751. // Observed-state only — never user-edited.
  752. Guid string `json:"guid" gorm:"column:guid;index"`
  753. // Heartbeat-updated fields. UpdatedAt advances on every probe even when
  754. // the row is otherwise unchanged so the UI's "last seen" tooltip is
  755. // truthful without us having to read LastHeartbeat separately.
  756. Status string `json:"status" gorm:"default:unknown" example:"online"` // online|offline|unknown
  757. LastHeartbeat int64 `json:"lastHeartbeat" example:"1700000000"` // unix seconds, 0 = never
  758. LatencyMs int `json:"latencyMs" example:"42"`
  759. XrayVersion string `json:"xrayVersion" example:"25.10.31"`
  760. PanelVersion string `json:"panelVersion" gorm:"column:panel_version" example:"v3.x.x"`
  761. CpuPct float64 `json:"cpuPct" example:"23.5"`
  762. MemPct float64 `json:"memPct" example:"45.1"`
  763. UptimeSecs uint64 `json:"uptimeSecs" example:"86400"`
  764. NetUp uint64 `json:"netUp" gorm:"column:net_up" example:"1048576"`
  765. NetDown uint64 `json:"netDown" gorm:"column:net_down" example:"2097152"`
  766. LastError string `json:"lastError"`
  767. // XrayState and XrayError are captured from the remote node's /panel/api/server/status
  768. // during heartbeats. They let the central panel distinguish "panel API reachable"
  769. // (status=online) from "Xray core itself has failed on the node" for monitoring.
  770. XrayState string `json:"xrayState" gorm:"column:xray_state"`
  771. XrayError string `json:"xrayError" gorm:"column:xray_error"`
  772. ConfigDirty bool `json:"configDirty" gorm:"default:false"`
  773. ConfigDirtyAt int64 `json:"configDirtyAt"`
  774. // InboundsAdoptedAt records the first clean traffic sync that imported the
  775. // node's pre-existing inbounds; reconcile must not sweep remote tags before it.
  776. InboundsAdoptedAt int64 `json:"-" gorm:"column:inbounds_adopted_at;default:0"`
  777. InboundCount int `json:"inboundCount" gorm:"-" example:"5"`
  778. ClientCount int `json:"clientCount" gorm:"-" example:"27"`
  779. OnlineCount int `json:"onlineCount" gorm:"-" example:"3"`
  780. ActiveCount int `json:"activeCount" gorm:"-" example:"23"`
  781. DisabledCount int `json:"disabledCount" gorm:"-" example:"3"`
  782. DepletedCount int `json:"depletedCount" gorm:"-" example:"1"`
  783. // ParentGuid + Transitive are set only when a node is surfaced as part of a
  784. // node tree (#4983): direct nodes carry the master panel's own GUID, a
  785. // transitive sub-node carries its parent node's GUID. Transitive nodes are
  786. // read-only projections (Id == 0, not persisted) — never edited or deployed.
  787. ParentGuid string `json:"parentGuid,omitempty" gorm:"-"`
  788. Transitive bool `json:"transitive,omitempty" gorm:"-"`
  789. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli" example:"1700000000"`
  790. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli" example:"1700000000"`
  791. }
  792. // NodeSummary is the read-only identity of a node as published one hop up: the
  793. // view a panel exposes about the nodes it directly manages, so a master can
  794. // surface transitive sub-nodes in a chained topology (#4983). Counts are
  795. // computed by the consuming master from its own per-GUID data, never trusted
  796. // from the child, so this carries identity/health only.
  797. type NodeSummary struct {
  798. Guid string `json:"guid"`
  799. ParentGuid string `json:"parentGuid"`
  800. Name string `json:"name"`
  801. Address string `json:"address"`
  802. Scheme string `json:"scheme"`
  803. Port int `json:"port"`
  804. Status string `json:"status"`
  805. LastHeartbeat int64 `json:"lastHeartbeat"`
  806. LatencyMs int `json:"latencyMs"`
  807. PanelVersion string `json:"panelVersion"`
  808. XrayVersion string `json:"xrayVersion"`
  809. // XrayState/XrayError forwarded so masters can surface xray failure on transitive sub-nodes too.
  810. XrayState string `json:"xrayState"`
  811. XrayError string `json:"xrayError,omitempty"`
  812. }
  813. type ClientReverse struct {
  814. Tag string `json:"tag"`
  815. }
  816. // Client represents a client configuration for Xray inbounds with traffic limits and settings.
  817. type Client struct {
  818. ID string `json:"id,omitempty"` // Unique client identifier
  819. Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
  820. Password string `json:"password,omitempty"` // Client password
  821. Flow string `json:"flow,omitempty"` // Flow control (XTLS)
  822. Reverse *ClientReverse `json:"reverse,omitempty"` // VLESS simple reverse proxy settings
  823. Auth string `json:"auth,omitempty"` // Auth password (Hysteria)
  824. PrivateKey string `json:"privateKey,omitempty"`
  825. PublicKey string `json:"publicKey,omitempty"`
  826. AllowedIPs []string `json:"allowedIPs,omitempty"`
  827. // AllowedIPsByInbound optionally overrides AllowedIPs on a per-inbound
  828. // basis, keyed by inbound id. Lets one identity attached to both
  829. // WireGuard and AmneziaWG carry two genuinely different addresses in a
  830. // single Create/Update call instead of the shared AllowedIPs field
  831. // being broadcast to every attached tunnel inbound. Absent/unset for a
  832. // given inbound id falls back to the shared AllowedIPs exactly as
  833. // before -- fully backward compatible for callers that never set this.
  834. AllowedIPsByInbound map[int][]string `json:"allowedIPsByInbound,omitempty"`
  835. PreSharedKey string `json:"preSharedKey,omitempty"`
  836. KeepAlive int `json:"keepAlive,omitempty"`
  837. ForwardedPorts string `json:"forwardedPorts,omitempty"` // AmneziaWG per-client port-forwarding spec, e.g. "80,443,8000-8100"
  838. Secret string `json:"secret,omitempty" example:"ee1234567890abcdef1234567890abcd7777772e636c6f7564666c6172652e636f6d"`
  839. AdTag string `json:"adTag,omitempty" example:"0123456789abcdef0123456789abcdef"`
  840. Email string `json:"email"` // Client email identifier
  841. LimitIP int `json:"limitIp"` // IP limit for this client
  842. TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB
  843. ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` // Expiration timestamp
  844. Enable bool `json:"enable" form:"enable"` // Whether the client is enabled
  845. TgID int64 `json:"tgId" form:"tgId"` // Telegram user ID for notifications
  846. SubID string `json:"subId" form:"subId"` // Subscription identifier
  847. Group string `json:"group,omitempty" form:"group"` // Logical grouping label
  848. Comment string `json:"comment" form:"comment"` // Client comment
  849. Reset int `json:"reset" form:"reset"` // Reset period in days
  850. ResetDay int `json:"resetDay" form:"resetDay"` // Calendar renewal day 1-31, 0 = interval mode
  851. ResetMax int `json:"resetMax" form:"resetMax"` // Max auto-renew count, 0 = unlimited
  852. // Per-client traffic reset cycle, independent of the inbound's own (#5497).
  853. TrafficReset string `json:"trafficReset,omitempty" form:"trafficReset" validate:"omitempty,oneof=never hourly daily weekly monthly"`
  854. TrafficResetDay int `json:"trafficResetDay,omitempty" form:"trafficResetDay" validate:"omitempty,gte=1,lte=31"`
  855. CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
  856. UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
  857. }
  858. type ClientRecord struct {
  859. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  860. Email string `json:"email" gorm:"uniqueIndex;not null"`
  861. SubID string `json:"subId" gorm:"index;column:sub_id"`
  862. UUID string `json:"uuid" gorm:"column:uuid"`
  863. Password string `json:"password"`
  864. Auth string `json:"auth"`
  865. Flow string `json:"flow"`
  866. Security string `json:"security"`
  867. Reverse string `json:"reverse" gorm:"column:reverse"`
  868. PrivateKey string `json:"privateKey" gorm:"column:wg_private_key"`
  869. PublicKey string `json:"publicKey" gorm:"column:wg_public_key"`
  870. AllowedIPs string `json:"allowedIPs" gorm:"column:wg_allowed_ips"`
  871. PreSharedKey string `json:"preSharedKey" gorm:"column:wg_pre_shared_key"`
  872. KeepAlive int `json:"keepAlive" gorm:"column:wg_keep_alive;default:0"`
  873. ForwardedPorts string `json:"forwardedPorts" gorm:"column:wg_forwarded_ports"`
  874. Secret string `json:"secret" gorm:"column:secret"`
  875. AdTag string `json:"adTag" gorm:"column:ad_tag;default:''"`
  876. LimitIP int `json:"limitIp" gorm:"column:limit_ip"`
  877. LimitHwid int `json:"limitHwid" gorm:"column:limit_hwid;default:0"`
  878. TotalGB int64 `json:"totalGB" gorm:"column:total_gb"`
  879. ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time"`
  880. Enable bool `json:"enable" gorm:"default:true"`
  881. TgID int64 `json:"tgId" gorm:"column:tg_id;index:idx_clients_tg_id"`
  882. Group string `json:"group" gorm:"column:group_name;default:'';index:idx_client_record_group"`
  883. Comment string `json:"comment"`
  884. Reset int `json:"reset" gorm:"default:0"`
  885. ResetDay int `json:"resetDay" gorm:"column:reset_day;default:0"`
  886. ResetMax int `json:"resetMax" gorm:"column:reset_max;default:0"`
  887. TrafficReset string `json:"trafficReset" gorm:"column:traffic_reset;default:never;index:idx_clients_traffic_reset"`
  888. TrafficResetDay int `json:"trafficResetDay" gorm:"column:traffic_reset_day;default:1"`
  889. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  890. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  891. // Owned solely by the node-snapshot sweep, which soft-orphans instead of
  892. // deleting; orphans from any other cause stay at zero and are never reaped.
  893. SyncOrphanedAt int64 `json:"-" gorm:"column:sync_orphaned_at;default:0"`
  894. }
  895. func (ClientRecord) TableName() string { return "clients" }
  896. type ClientGroup struct {
  897. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  898. Name string `json:"name" gorm:"uniqueIndex;not null"`
  899. ResetUp int64 `json:"resetUp" gorm:"column:reset_up;default:0"`
  900. ResetDown int64 `json:"resetDown" gorm:"column:reset_down;default:0"`
  901. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  902. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  903. }
  904. func (ClientGroup) TableName() string { return "client_groups" }
  905. // MarshalJSON emits the reverse column as a nested JSON object rather than an
  906. // escaped JSON-text string, matching the same convention Inbound uses for its
  907. // JSON-text columns. Empty storage renders as null.
  908. func (r ClientRecord) MarshalJSON() ([]byte, error) {
  909. type alias ClientRecord
  910. return json.Marshal(struct {
  911. alias
  912. Reverse json.RawMessage `json:"reverse"`
  913. }{
  914. alias: alias(r),
  915. Reverse: jsonStringFieldToRaw(r.Reverse),
  916. })
  917. }
  918. // UnmarshalJSON accepts reverse as either a JSON object (modern shape) or a
  919. // JSON-encoded string (legacy shape).
  920. func (r *ClientRecord) UnmarshalJSON(data []byte) error {
  921. type alias ClientRecord
  922. aux := struct {
  923. *alias
  924. Reverse json.RawMessage `json:"reverse"`
  925. }{
  926. alias: (*alias)(r),
  927. }
  928. if err := json.Unmarshal(data, &aux); err != nil {
  929. return err
  930. }
  931. r.Reverse = jsonStringFieldFromRaw(aux.Reverse)
  932. return nil
  933. }
  934. type ClientInbound struct {
  935. ClientId int `json:"clientId" gorm:"primaryKey;column:client_id;index"`
  936. InboundId int `json:"inboundId" gorm:"primaryKey;column:inbound_id;index"`
  937. FlowOverride string `json:"flowOverride" gorm:"column:flow_override"`
  938. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  939. }
  940. func (ClientInbound) TableName() string { return "client_inbounds" }
  941. type ClientHwid struct {
  942. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  943. SubID string `json:"subId" gorm:"column:sub_id;not null;index;uniqueIndex:idx_client_hwids_sub_hash,priority:1"`
  944. HwidHash string `json:"-" gorm:"column:hwid_hash;size:64;not null;uniqueIndex:idx_client_hwids_sub_hash,priority:2"`
  945. FirstSeen int64 `json:"firstSeen" gorm:"column:first_seen;not null"`
  946. LastSeen int64 `json:"lastSeen" gorm:"column:last_seen;not null;index"`
  947. UserAgent string `json:"userAgent" gorm:"column:user_agent"`
  948. DeviceOS string `json:"deviceOs" gorm:"column:device_os"`
  949. OsVersion string `json:"osVersion" gorm:"column:os_version"`
  950. DeviceModel string `json:"deviceModel" gorm:"column:device_model"`
  951. }
  952. func (ClientHwid) TableName() string { return "client_hwids" }
  953. // ClientExternalLink is a per-client entry surfaced in the client's
  954. // subscription. Two kinds:
  955. // - "link": a single third-party share link (vless://, vmess://, trojan://,
  956. // ss://, hysteria2://, wireguard://). Emitted verbatim in raw subs; parsed
  957. // into an outbound/proxy for JSON and Clash.
  958. // - "subscription": a remote subscription URL. The panel fetches it (cached),
  959. // decodes its links, and merges them into the client's subscription.
  960. type ClientExternalLink struct {
  961. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  962. ClientId int `json:"clientId" gorm:"index;column:client_id"`
  963. Kind string `json:"kind" gorm:"column:kind"`
  964. Value string `json:"value" gorm:"column:value"`
  965. Remark string `json:"remark" gorm:"column:remark"`
  966. Enable *bool `json:"enable" gorm:"column:enable;default:true"`
  967. ExpiryTime int64 `json:"expiryTime" gorm:"column:expiry_time;default:0"`
  968. NamePrefix string `json:"namePrefix" gorm:"column:name_prefix"`
  969. LastFetchAt int64 `json:"lastFetchAt" gorm:"column:last_fetch_at;default:0"`
  970. LastFetchError string `json:"lastFetchError" gorm:"column:last_fetch_error"`
  971. SortIndex int `json:"sortIndex" gorm:"column:sort_index"`
  972. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  973. }
  974. func (ClientExternalLink) TableName() string { return "client_external_links" }
  975. // External link kinds.
  976. const (
  977. ExternalLinkKindLink = "link"
  978. ExternalLinkKindSubscription = "subscription"
  979. )
  980. type InboundFallback struct {
  981. Id int `json:"id" gorm:"primaryKey;autoIncrement"`
  982. MasterId int `json:"masterId" gorm:"index;not null;column:master_id"`
  983. ChildId int `json:"childId" gorm:"index;not null;column:child_id"`
  984. Name string `json:"name"`
  985. Alpn string `json:"alpn"`
  986. Path string `json:"path"`
  987. Dest string `json:"dest"`
  988. Xver int `json:"xver"`
  989. SortOrder int `json:"sortOrder" gorm:"default:0;column:sort_order"`
  990. }
  991. func (InboundFallback) TableName() string { return "inbound_fallbacks" }
  992. type Host struct {
  993. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"`
  994. GroupId string `json:"groupId" form:"groupId" gorm:"column:group_id;index"`
  995. InboundId int `json:"inboundId" form:"inboundId" gorm:"index;not null;column:inbound_id" validate:"required" example:"1"`
  996. SortOrder int `json:"sortOrder" form:"sortOrder" gorm:"default:0;column:sort_order"`
  997. Remark string `json:"remark" form:"remark" validate:"required,max=256" example:"cdn-front"`
  998. ServerDescription string `json:"serverDescription" form:"serverDescription" gorm:"column:server_description" validate:"omitempty,max=64"`
  999. IsDisabled bool `json:"isDisabled" form:"isDisabled" gorm:"default:false;column:is_disabled"`
  1000. IsHidden bool `json:"isHidden" form:"isHidden" gorm:"default:false;column:is_hidden"`
  1001. Tags []string `json:"tags" form:"tags" gorm:"serializer:json"`
  1002. Address string `json:"address" form:"address" example:"cdn.example.com"`
  1003. Port int `json:"port" form:"port" gorm:"default:0" validate:"gte=0,lte=65535" example:"8443"`
  1004. Security string `json:"security" form:"security" gorm:"default:same" validate:"omitempty,oneof=same tls none reality" example:"same"`
  1005. Sni string `json:"sni" form:"sni"`
  1006. HostHeader string `json:"hostHeader" form:"hostHeader" gorm:"column:host_header"`
  1007. Path string `json:"path" form:"path"`
  1008. Alpn []string `json:"alpn" form:"alpn" gorm:"serializer:json"`
  1009. Fingerprint string `json:"fingerprint" form:"fingerprint"`
  1010. OverrideSniFromAddress bool `json:"overrideSniFromAddress" form:"overrideSniFromAddress" gorm:"column:override_sni_from_address"`
  1011. KeepSniBlank bool `json:"keepSniBlank" form:"keepSniBlank" gorm:"column:keep_sni_blank"`
  1012. PinnedPeerCertSha256 []string `json:"pinnedPeerCertSha256" form:"pinnedPeerCertSha256" gorm:"serializer:json;column:pinned_peer_cert_sha256"`
  1013. VerifyPeerCertByName string `json:"verifyPeerCertByName" form:"verifyPeerCertByName" gorm:"column:verify_peer_cert_by_name"`
  1014. AllowInsecure bool `json:"allowInsecure" form:"allowInsecure" gorm:"column:allow_insecure"`
  1015. EchConfigList string `json:"echConfigList" form:"echConfigList" gorm:"column:ech_config_list"`
  1016. MuxParams string `json:"muxParams" form:"muxParams" gorm:"type:text;column:mux_params"`
  1017. SockoptParams string `json:"sockoptParams" form:"sockoptParams" gorm:"type:text;column:sockopt_params"`
  1018. // FinalMask is a JSON object of xray finalmask masks (tcp/udp/quicParams),
  1019. // merged into this host's JSON-subscription stream. Empty = no override.
  1020. FinalMask string `json:"finalMask" form:"finalMask" gorm:"type:text;column:final_mask"`
  1021. // Single VLESS route value (0-65535) baked into the subscription UUID's 3rd
  1022. // group (bytes 6-7), which xray reads via net.PortFromBytes(id[6:8]). Empty = none.
  1023. VlessRoute string `json:"vlessRoute" form:"vlessRoute" gorm:"column:vless_route" example:"443"`
  1024. ExcludeFromSubTypes []string `json:"excludeFromSubTypes" form:"excludeFromSubTypes" gorm:"serializer:json;column:exclude_from_sub_types"`
  1025. MihomoIpVersion string `json:"mihomoIpVersion" form:"mihomoIpVersion" gorm:"column:mihomo_ip_version" validate:"omitempty,oneof=dual ipv4 ipv6 ipv4-prefer ipv6-prefer"`
  1026. MihomoX25519 bool `json:"mihomoX25519" form:"mihomoX25519" gorm:"column:mihomo_x25519"`
  1027. ShuffleHost bool `json:"shuffleHost" form:"shuffleHost" gorm:"column:shuffle_host"`
  1028. NodeGuids []string `json:"nodeGuids,omitempty" form:"nodeGuids" gorm:"serializer:json;column:node_guids"`
  1029. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  1030. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  1031. }
  1032. func (Host) TableName() string { return "hosts" }
  1033. func (c *Client) ToRecord() *ClientRecord {
  1034. rec := &ClientRecord{
  1035. Email: c.Email,
  1036. SubID: c.SubID,
  1037. UUID: c.ID,
  1038. Password: c.Password,
  1039. Auth: c.Auth,
  1040. Flow: c.Flow,
  1041. Security: c.Security,
  1042. LimitIP: c.LimitIP,
  1043. TotalGB: c.TotalGB,
  1044. ExpiryTime: c.ExpiryTime,
  1045. Enable: c.Enable,
  1046. TgID: c.TgID,
  1047. Group: c.Group,
  1048. Comment: c.Comment,
  1049. Reset: c.Reset,
  1050. ResetDay: c.ResetDay,
  1051. ResetMax: c.ResetMax,
  1052. TrafficReset: c.TrafficReset,
  1053. TrafficResetDay: c.TrafficResetDay,
  1054. CreatedAt: c.CreatedAt,
  1055. UpdatedAt: c.UpdatedAt,
  1056. PrivateKey: c.PrivateKey,
  1057. PublicKey: c.PublicKey,
  1058. AllowedIPs: strings.Join(c.AllowedIPs, ","),
  1059. PreSharedKey: c.PreSharedKey,
  1060. KeepAlive: c.KeepAlive,
  1061. ForwardedPorts: c.ForwardedPorts,
  1062. Secret: c.Secret,
  1063. AdTag: c.AdTag,
  1064. }
  1065. if c.Reverse != nil {
  1066. if b, err := json.Marshal(c.Reverse); err == nil {
  1067. rec.Reverse = string(b)
  1068. }
  1069. }
  1070. return rec
  1071. }
  1072. func splitWireguardAllowedIPs(csv string) []string {
  1073. if csv == "" {
  1074. return nil
  1075. }
  1076. parts := strings.Split(csv, ",")
  1077. out := make([]string, 0, len(parts))
  1078. for _, p := range parts {
  1079. if trimmed := strings.TrimSpace(p); trimmed != "" {
  1080. out = append(out, trimmed)
  1081. }
  1082. }
  1083. if len(out) == 0 {
  1084. return nil
  1085. }
  1086. return out
  1087. }
  1088. func (r *ClientRecord) ToClient() *Client {
  1089. c := &Client{
  1090. ID: r.UUID,
  1091. Email: r.Email,
  1092. SubID: r.SubID,
  1093. Password: r.Password,
  1094. Auth: r.Auth,
  1095. Flow: r.Flow,
  1096. Security: r.Security,
  1097. LimitIP: r.LimitIP,
  1098. TotalGB: r.TotalGB,
  1099. ExpiryTime: r.ExpiryTime,
  1100. Enable: r.Enable,
  1101. TgID: r.TgID,
  1102. Group: r.Group,
  1103. Comment: r.Comment,
  1104. Reset: r.Reset,
  1105. ResetDay: r.ResetDay,
  1106. ResetMax: r.ResetMax,
  1107. TrafficReset: r.TrafficReset,
  1108. TrafficResetDay: r.TrafficResetDay,
  1109. CreatedAt: r.CreatedAt,
  1110. UpdatedAt: r.UpdatedAt,
  1111. PrivateKey: r.PrivateKey,
  1112. PublicKey: r.PublicKey,
  1113. AllowedIPs: splitWireguardAllowedIPs(r.AllowedIPs),
  1114. PreSharedKey: r.PreSharedKey,
  1115. KeepAlive: r.KeepAlive,
  1116. ForwardedPorts: r.ForwardedPorts,
  1117. Secret: r.Secret,
  1118. AdTag: r.AdTag,
  1119. }
  1120. if r.Reverse != "" {
  1121. var rev ClientReverse
  1122. if err := json.Unmarshal([]byte(r.Reverse), &rev); err == nil {
  1123. c.Reverse = &rev
  1124. }
  1125. }
  1126. return c
  1127. }
  1128. type ClientMergeConflict struct {
  1129. Field string
  1130. Old any
  1131. New any
  1132. Kept any
  1133. }
  1134. type OutboundSubscription struct {
  1135. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
  1136. Remark string `json:"remark" form:"remark"`
  1137. Url string `json:"url" form:"url"`
  1138. Enabled bool `json:"enabled" form:"enabled" gorm:"default:true"`
  1139. AllowPrivate bool `json:"allowPrivate" form:"allowPrivate" gorm:"default:false"`
  1140. AllowInsecure bool `json:"allowInsecure" form:"allowInsecure" gorm:"default:false"`
  1141. TagPrefix string `json:"tagPrefix" form:"tagPrefix"`
  1142. UpdateInterval int `json:"updateInterval" form:"updateInterval" gorm:"default:600"` // seconds between refreshes
  1143. Priority int `json:"priority" form:"priority" gorm:"default:0"` // order among subscriptions in the merged outbounds (lower = earlier)
  1144. Prepend bool `json:"prepend" form:"prepend" gorm:"default:false"` // place this subscription's outbounds before the manual template outbounds
  1145. LastUpdated int64 `json:"lastUpdated" form:"lastUpdated"`
  1146. LastError string `json:"lastError" form:"lastError"`
  1147. LastFetchedOutbounds string `json:"lastFetchedOutbounds" form:"lastFetchedOutbounds" gorm:"type:text"`
  1148. LinkIdentities string `json:"-" gorm:"type:text;column:link_identities"`
  1149. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli"`
  1150. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli"`
  1151. OutboundCount int `json:"outboundCount" gorm:"-"`
  1152. }
  1153. // SubBalancer is one extra JSON-subscription config document whose members are
  1154. // the selected inbounds' proxy outbounds. SortOrder shares SubSortIndex semantics.
  1155. type SubBalancer struct {
  1156. Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement" example:"1"`
  1157. Remark string `json:"remark" form:"remark" validate:"required,max=256" example:"auto-fastest"`
  1158. Strategy string `json:"strategy" form:"strategy" validate:"omitempty,oneof=leastLoad leastPing random roundRobin" example:"random"`
  1159. InboundIds []int `json:"inboundIds" form:"inboundIds" gorm:"serializer:json;column:inbound_ids" example:"[1,3]"`
  1160. SortOrder int `json:"sortOrder" form:"sortOrder" gorm:"column:sort_order" validate:"omitempty,gte=1" example:"1"`
  1161. // No gorm default:true — a bool default makes an explicit false at insert
  1162. // collapse back to the column default (zero value is skipped).
  1163. Enabled bool `json:"enabled" form:"enabled" example:"true"`
  1164. CreatedAt int64 `json:"createdAt" gorm:"autoCreateTime:milli" example:"1710000000000"`
  1165. UpdatedAt int64 `json:"updatedAt" gorm:"autoUpdateTime:milli" example:"1710000000000"`
  1166. }
  1167. func MergeClientRecord(existing *ClientRecord, incoming *ClientRecord) []ClientMergeConflict {
  1168. var conflicts []ClientMergeConflict
  1169. keep := func(field string, oldV, newV, kept any) {
  1170. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: oldV, New: newV, Kept: kept})
  1171. }
  1172. const redacted = "<redacted>"
  1173. keepSecret := func(field string) {
  1174. conflicts = append(conflicts, ClientMergeConflict{Field: field, Old: redacted, New: redacted, Kept: redacted})
  1175. }
  1176. incomingNewer := incoming.UpdatedAt > existing.UpdatedAt ||
  1177. (incoming.UpdatedAt == existing.UpdatedAt && incoming.CreatedAt > existing.CreatedAt)
  1178. if existing.UUID != incoming.UUID && incoming.UUID != "" {
  1179. if incomingNewer || existing.UUID == "" {
  1180. existing.UUID = incoming.UUID
  1181. }
  1182. keepSecret("uuid")
  1183. }
  1184. if existing.Password != incoming.Password && incoming.Password != "" {
  1185. if incomingNewer || existing.Password == "" {
  1186. existing.Password = incoming.Password
  1187. keepSecret("password")
  1188. }
  1189. }
  1190. if existing.Auth != incoming.Auth && incoming.Auth != "" {
  1191. if incomingNewer || existing.Auth == "" {
  1192. existing.Auth = incoming.Auth
  1193. keepSecret("auth")
  1194. }
  1195. }
  1196. if existing.Flow != incoming.Flow && incoming.Flow != "" {
  1197. if incomingNewer || existing.Flow == "" {
  1198. keep("flow", existing.Flow, incoming.Flow, incoming.Flow)
  1199. existing.Flow = incoming.Flow
  1200. }
  1201. }
  1202. if existing.Security != incoming.Security && incoming.Security != "" {
  1203. if incomingNewer || existing.Security == "" {
  1204. keep("security", existing.Security, incoming.Security, incoming.Security)
  1205. existing.Security = incoming.Security
  1206. }
  1207. }
  1208. if existing.SubID != incoming.SubID && incoming.SubID != "" {
  1209. if incomingNewer || existing.SubID == "" {
  1210. existing.SubID = incoming.SubID
  1211. keepSecret("subId")
  1212. }
  1213. }
  1214. if existing.TotalGB != incoming.TotalGB {
  1215. picked := existing.TotalGB
  1216. if existing.TotalGB == 0 || (incoming.TotalGB != 0 && incoming.TotalGB > existing.TotalGB) {
  1217. picked = incoming.TotalGB
  1218. }
  1219. if picked != existing.TotalGB {
  1220. keep("totalGB", existing.TotalGB, incoming.TotalGB, picked)
  1221. existing.TotalGB = picked
  1222. }
  1223. }
  1224. if existing.ExpiryTime != incoming.ExpiryTime {
  1225. picked := existing.ExpiryTime
  1226. if existing.ExpiryTime == 0 || (incoming.ExpiryTime != 0 && incoming.ExpiryTime > existing.ExpiryTime) {
  1227. picked = incoming.ExpiryTime
  1228. }
  1229. if picked != existing.ExpiryTime {
  1230. keep("expiryTime", existing.ExpiryTime, incoming.ExpiryTime, picked)
  1231. existing.ExpiryTime = picked
  1232. }
  1233. }
  1234. if existing.LimitIP != incoming.LimitIP && incoming.LimitIP != 0 {
  1235. picked := existing.LimitIP
  1236. if existing.LimitIP == 0 || incoming.LimitIP > existing.LimitIP {
  1237. picked = incoming.LimitIP
  1238. }
  1239. if picked != existing.LimitIP {
  1240. keep("limitIp", existing.LimitIP, incoming.LimitIP, picked)
  1241. existing.LimitIP = picked
  1242. }
  1243. }
  1244. if existing.LimitHwid != incoming.LimitHwid && incoming.LimitHwid != 0 {
  1245. picked := existing.LimitHwid
  1246. if existing.LimitHwid == 0 || incoming.LimitHwid > existing.LimitHwid {
  1247. picked = incoming.LimitHwid
  1248. }
  1249. if picked != existing.LimitHwid {
  1250. keep("limitHwid", existing.LimitHwid, incoming.LimitHwid, picked)
  1251. existing.LimitHwid = picked
  1252. }
  1253. }
  1254. if existing.TgID != incoming.TgID && incoming.TgID != 0 {
  1255. if incomingNewer || existing.TgID == 0 {
  1256. keep("tgId", existing.TgID, incoming.TgID, incoming.TgID)
  1257. existing.TgID = incoming.TgID
  1258. }
  1259. }
  1260. if existing.Reset != incoming.Reset && incoming.Reset != 0 {
  1261. if incomingNewer || existing.Reset == 0 {
  1262. keep("reset", existing.Reset, incoming.Reset, incoming.Reset)
  1263. existing.Reset = incoming.Reset
  1264. }
  1265. }
  1266. if existing.ResetDay != incoming.ResetDay && incoming.ResetDay != 0 {
  1267. if incomingNewer || existing.ResetDay == 0 {
  1268. keep("resetDay", existing.ResetDay, incoming.ResetDay, incoming.ResetDay)
  1269. existing.ResetDay = incoming.ResetDay
  1270. }
  1271. }
  1272. if existing.ResetMax != incoming.ResetMax && incoming.ResetMax != 0 {
  1273. if incomingNewer || existing.ResetMax == 0 {
  1274. keep("resetMax", existing.ResetMax, incoming.ResetMax, incoming.ResetMax)
  1275. existing.ResetMax = incoming.ResetMax
  1276. }
  1277. }
  1278. if existing.TrafficReset != incoming.TrafficReset && incoming.TrafficReset != "" {
  1279. if incomingNewer || existing.TrafficReset == "" {
  1280. keep("trafficReset", existing.TrafficReset, incoming.TrafficReset, incoming.TrafficReset)
  1281. existing.TrafficReset = incoming.TrafficReset
  1282. }
  1283. }
  1284. if existing.TrafficResetDay != incoming.TrafficResetDay && incoming.TrafficResetDay != 0 {
  1285. if incomingNewer || existing.TrafficResetDay == 0 {
  1286. keep("trafficResetDay", existing.TrafficResetDay, incoming.TrafficResetDay, incoming.TrafficResetDay)
  1287. existing.TrafficResetDay = incoming.TrafficResetDay
  1288. }
  1289. }
  1290. if existing.Reverse != incoming.Reverse && incoming.Reverse != "" {
  1291. if incomingNewer || existing.Reverse == "" {
  1292. keep("reverse", existing.Reverse, incoming.Reverse, incoming.Reverse)
  1293. existing.Reverse = incoming.Reverse
  1294. }
  1295. }
  1296. if existing.PrivateKey != incoming.PrivateKey && incoming.PrivateKey != "" {
  1297. if incomingNewer || existing.PrivateKey == "" {
  1298. existing.PrivateKey = incoming.PrivateKey
  1299. keepSecret("privateKey")
  1300. }
  1301. }
  1302. if existing.PublicKey != incoming.PublicKey && incoming.PublicKey != "" {
  1303. if incomingNewer || existing.PublicKey == "" {
  1304. existing.PublicKey = incoming.PublicKey
  1305. keepSecret("publicKey")
  1306. }
  1307. }
  1308. if existing.PreSharedKey != incoming.PreSharedKey && incoming.PreSharedKey != "" {
  1309. if incomingNewer || existing.PreSharedKey == "" {
  1310. existing.PreSharedKey = incoming.PreSharedKey
  1311. keepSecret("preSharedKey")
  1312. }
  1313. }
  1314. if existing.Secret != incoming.Secret && incoming.Secret != "" {
  1315. if incomingNewer || existing.Secret == "" {
  1316. existing.Secret = incoming.Secret
  1317. keepSecret("secret")
  1318. }
  1319. }
  1320. if existing.AllowedIPs != incoming.AllowedIPs && incoming.AllowedIPs != "" {
  1321. if incomingNewer || existing.AllowedIPs == "" {
  1322. keep("allowedIPs", existing.AllowedIPs, incoming.AllowedIPs, incoming.AllowedIPs)
  1323. existing.AllowedIPs = incoming.AllowedIPs
  1324. }
  1325. }
  1326. if existing.KeepAlive != incoming.KeepAlive && incoming.KeepAlive != 0 {
  1327. if incomingNewer || existing.KeepAlive == 0 {
  1328. keep("keepAlive", existing.KeepAlive, incoming.KeepAlive, incoming.KeepAlive)
  1329. existing.KeepAlive = incoming.KeepAlive
  1330. }
  1331. }
  1332. if existing.ForwardedPorts != incoming.ForwardedPorts && incoming.ForwardedPorts != "" {
  1333. if incomingNewer || existing.ForwardedPorts == "" {
  1334. keep("forwardedPorts", existing.ForwardedPorts, incoming.ForwardedPorts, incoming.ForwardedPorts)
  1335. existing.ForwardedPorts = incoming.ForwardedPorts
  1336. }
  1337. }
  1338. if existing.Comment != incoming.Comment && incoming.Comment != "" {
  1339. if incomingNewer || existing.Comment == "" {
  1340. keep("comment", existing.Comment, incoming.Comment, incoming.Comment)
  1341. existing.Comment = incoming.Comment
  1342. }
  1343. }
  1344. if existing.Group != incoming.Group && incoming.Group != "" {
  1345. if incomingNewer || existing.Group == "" {
  1346. keep("group", existing.Group, incoming.Group, incoming.Group)
  1347. existing.Group = incoming.Group
  1348. }
  1349. }
  1350. if existing.Enable != incoming.Enable {
  1351. if incoming.Enable {
  1352. if !existing.Enable {
  1353. keep("enable", existing.Enable, incoming.Enable, true)
  1354. existing.Enable = true
  1355. }
  1356. }
  1357. }
  1358. if incoming.CreatedAt != 0 && (existing.CreatedAt == 0 || incoming.CreatedAt < existing.CreatedAt) {
  1359. existing.CreatedAt = incoming.CreatedAt
  1360. }
  1361. if incoming.UpdatedAt > existing.UpdatedAt {
  1362. existing.UpdatedAt = incoming.UpdatedAt
  1363. }
  1364. return conflicts
  1365. }