1
0

model.go 58 KB

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