1
0

model.go 64 KB

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