server.go 69 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388
  1. package service
  2. import (
  3. "archive/zip"
  4. "bufio"
  5. "bytes"
  6. "context"
  7. "crypto/sha256"
  8. "crypto/x509"
  9. "encoding/hex"
  10. "encoding/json"
  11. "encoding/pem"
  12. "errors"
  13. "fmt"
  14. "io"
  15. "mime/multipart"
  16. stdnet "net"
  17. "net/http"
  18. "net/url"
  19. "os"
  20. "os/exec"
  21. "path/filepath"
  22. "regexp"
  23. "runtime"
  24. "slices"
  25. "strconv"
  26. "strings"
  27. "sync"
  28. "time"
  29. "github.com/mhsanaei/3x-ui/v3/internal/config"
  30. "github.com/mhsanaei/3x-ui/v3/internal/database"
  31. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  32. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  33. "github.com/mhsanaei/3x-ui/v3/internal/util/sys"
  34. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  35. "github.com/google/uuid"
  36. utls "github.com/refraction-networking/utls"
  37. "github.com/shirou/gopsutil/v4/cpu"
  38. "github.com/shirou/gopsutil/v4/disk"
  39. "github.com/shirou/gopsutil/v4/host"
  40. "github.com/shirou/gopsutil/v4/load"
  41. "github.com/shirou/gopsutil/v4/mem"
  42. "github.com/shirou/gopsutil/v4/net"
  43. )
  44. // ProcessState represents the current state of a system process.
  45. type ProcessState string
  46. // Process state constants
  47. const (
  48. Running ProcessState = "running" // Process is running normally
  49. Stop ProcessState = "stop" // Process is stopped
  50. Error ProcessState = "error" // Process is in error state
  51. )
  52. // Status represents comprehensive system and application status information.
  53. // It includes CPU, memory, disk, network statistics, and Xray process status.
  54. type Status struct {
  55. T time.Time `json:"-"`
  56. Cpu float64 `json:"cpu"`
  57. CpuCores int `json:"cpuCores"`
  58. LogicalPro int `json:"logicalPro"`
  59. CpuSpeedMhz float64 `json:"cpuSpeedMhz"`
  60. Mem struct {
  61. Current uint64 `json:"current"`
  62. Total uint64 `json:"total"`
  63. } `json:"mem"`
  64. Swap struct {
  65. Current uint64 `json:"current"`
  66. Total uint64 `json:"total"`
  67. } `json:"swap"`
  68. Disk struct {
  69. Current uint64 `json:"current"`
  70. Total uint64 `json:"total"`
  71. } `json:"disk"`
  72. DiskIO struct {
  73. Read uint64 `json:"read"`
  74. Write uint64 `json:"write"`
  75. } `json:"diskIO"`
  76. DiskTraffic struct {
  77. Read uint64 `json:"read"`
  78. Write uint64 `json:"write"`
  79. } `json:"diskTraffic"`
  80. Xray struct {
  81. State ProcessState `json:"state"`
  82. ErrorMsg string `json:"errorMsg"`
  83. Version string `json:"version"`
  84. } `json:"xray"`
  85. PanelVersion string `json:"panelVersion"`
  86. PanelGuid string `json:"panelGuid"`
  87. Uptime uint64 `json:"uptime"`
  88. Loads []float64 `json:"loads"`
  89. TcpCount int `json:"tcpCount"`
  90. UdpCount int `json:"udpCount"`
  91. NetIO struct {
  92. Up uint64 `json:"up"`
  93. Down uint64 `json:"down"`
  94. PktUp uint64 `json:"pktUp"`
  95. PktDown uint64 `json:"pktDown"`
  96. } `json:"netIO"`
  97. NetTraffic struct {
  98. Sent uint64 `json:"sent"`
  99. Recv uint64 `json:"recv"`
  100. PktSent uint64 `json:"pktSent"`
  101. PktRecv uint64 `json:"pktRecv"`
  102. } `json:"netTraffic"`
  103. PublicIP struct {
  104. IPv4 string `json:"ipv4"`
  105. IPv6 string `json:"ipv6"`
  106. } `json:"publicIP"`
  107. AppStats struct {
  108. Threads uint32 `json:"threads"`
  109. Mem uint64 `json:"mem"`
  110. Uptime uint64 `json:"uptime"`
  111. } `json:"appStats"`
  112. }
  113. // Release represents information about a software release from GitHub.
  114. type Release struct {
  115. TagName string `json:"tag_name"` // The tag name of the release
  116. Body string `json:"body"` // The release notes; the dev channel reads its commit from here
  117. TargetCommitish string `json:"target_commitish"` // The branch/commit the tag points at
  118. Prerelease bool `json:"prerelease"` // Whether this is a pre-release
  119. }
  120. // ServerService provides business logic for server monitoring and management.
  121. // It handles system status collection, IP detection, and application statistics.
  122. type ServerService struct {
  123. xrayService XrayService
  124. inboundService InboundService
  125. settingService SettingService
  126. cachedIPv4 string
  127. cachedIPv6 string
  128. noIPv6 bool
  129. mu sync.Mutex
  130. lastCPUTimes cpu.TimesStat
  131. hasLastCPUSample bool
  132. hasNativeCPUSample bool
  133. emaCPU float64
  134. cachedCpuSpeedMhz float64
  135. lastCpuInfoAttempt time.Time
  136. lastStatusMu sync.RWMutex
  137. lastStatus *Status
  138. versionsCacheMu sync.Mutex
  139. versionsCache *cachedXrayVersions
  140. fail2banMu sync.Mutex
  141. fail2banInstalled bool
  142. fail2banCheckedAt time.Time
  143. }
  144. type cachedXrayVersions struct {
  145. versions []string
  146. fetchedAt time.Time
  147. }
  148. // xrayVersionsCacheTTL bounds how often /getXrayVersion hits GitHub. The list
  149. // is purely informational (rendered in the "switch Xray version" picker) so a
  150. // quarter-hour staleness window is fine and saves the API budget.
  151. const xrayVersionsCacheTTL = 15 * time.Minute
  152. // allowedHistoryBuckets is the bucket-second whitelist for time-series
  153. // aggregation endpoints (server + node metrics). Restricting it prevents
  154. // callers from triggering arbitrary aggregation work and keeps the
  155. // frontend's bucket selector self-documenting.
  156. var allowedHistoryBuckets = map[int]bool{
  157. 2: true, // 2m
  158. 30: true, // 30m
  159. 60: true, // 1h
  160. 180: true, // 3h
  161. 360: true, // 6h
  162. 720: true, // 12h
  163. 1440: true, // 24h
  164. 2880: true, // 2d
  165. 10080: true, // 7d
  166. }
  167. // IsAllowedHistoryBucket reports whether a bucket-seconds value is in the
  168. // whitelist used by /server/history, /server/cpuHistory, /server/xrayMetricsHistory,
  169. // /server/xrayObservatoryHistory, and /nodes/history.
  170. func IsAllowedHistoryBucket(bucketSeconds int) bool {
  171. return allowedHistoryBuckets[bucketSeconds]
  172. }
  173. // LastStatus returns the most recent Status snapshot collected by
  174. // RefreshStatus. Safe for concurrent readers.
  175. func (s *ServerService) LastStatus() *Status {
  176. s.lastStatusMu.RLock()
  177. defer s.lastStatusMu.RUnlock()
  178. return s.lastStatus
  179. }
  180. // Fail2banStatus tells the frontend whether the per-client IP limit can
  181. // actually be enforced. Enforcement depends on fail2ban, so a limit set
  182. // without it would silently do nothing.
  183. type Fail2banStatus struct {
  184. Enabled bool `json:"enabled"`
  185. Installed bool `json:"installed"`
  186. Usable bool `json:"usable"`
  187. Windows bool `json:"windows"`
  188. }
  189. const fail2banInstalledCacheTTL = 30 * time.Second
  190. func (s *ServerService) GetFail2banStatus() Fail2banStatus {
  191. enabled := isFail2banEnabled()
  192. installed := false
  193. if enabled {
  194. installed = s.isFail2banInstalled()
  195. }
  196. return Fail2banStatus{
  197. Enabled: enabled,
  198. Installed: installed,
  199. Usable: enabled && installed,
  200. Windows: runtime.GOOS == "windows",
  201. }
  202. }
  203. func isFail2banEnabled() bool {
  204. value, ok := os.LookupEnv("XUI_ENABLE_FAIL2BAN")
  205. return !ok || value == "true"
  206. }
  207. func (s *ServerService) isFail2banInstalled() bool {
  208. s.fail2banMu.Lock()
  209. defer s.fail2banMu.Unlock()
  210. if !s.fail2banCheckedAt.IsZero() && time.Since(s.fail2banCheckedAt) < fail2banInstalledCacheTTL {
  211. return s.fail2banInstalled
  212. }
  213. err := exec.CommandContext(context.Background(), "fail2ban-client", "-h").Run()
  214. s.fail2banInstalled = err == nil
  215. s.fail2banCheckedAt = time.Now()
  216. return s.fail2banInstalled
  217. }
  218. // RefreshStatus collects a new system snapshot, stores it as LastStatus, and
  219. // appends it to the system-metrics time series. Returns the new snapshot (may
  220. // be nil if collection failed). Called by the background ticker; the caller is
  221. // responsible for any side effects (websocket broadcast, xray metrics sample).
  222. func (s *ServerService) RefreshStatus() *Status {
  223. next := s.GetStatus(s.LastStatus())
  224. if next == nil {
  225. return nil
  226. }
  227. s.lastStatusMu.Lock()
  228. s.lastStatus = next
  229. s.lastStatusMu.Unlock()
  230. s.AppendStatusSample(time.Now(), next)
  231. return next
  232. }
  233. // GetXrayVersionsCached wraps GetXrayVersions with a TTL cache. On fetch
  234. // failure we serve the last successful list (if any) so the UI doesn't go
  235. // blank during a GitHub API hiccup; if there's no cache at all the underlying
  236. // error is surfaced.
  237. func (s *ServerService) GetXrayVersionsCached() ([]string, error) {
  238. s.versionsCacheMu.Lock()
  239. cache := s.versionsCache
  240. s.versionsCacheMu.Unlock()
  241. if cache != nil && time.Since(cache.fetchedAt) <= xrayVersionsCacheTTL {
  242. return cache.versions, nil
  243. }
  244. versions, err := s.GetXrayVersions()
  245. if err != nil {
  246. if cache != nil {
  247. logger.Warning("GetXrayVersionsCached: serving stale list:", err)
  248. return cache.versions, nil
  249. }
  250. return nil, err
  251. }
  252. s.versionsCacheMu.Lock()
  253. s.versionsCache = &cachedXrayVersions{versions: versions, fetchedAt: time.Now()}
  254. s.versionsCacheMu.Unlock()
  255. return versions, nil
  256. }
  257. // GetDefaultLogOutboundTags scans the default Xray config for freedom and
  258. // blackhole outbound tags so /getXrayLogs can colour-code log lines without
  259. // the controller re-doing the JSON walk. Falls back to the historical
  260. // "direct"/"blocked" defaults when the config can't be read.
  261. func (s *ServerService) GetDefaultLogOutboundTags() (freedoms, blackholes []string) {
  262. config, err := s.settingService.GetDefaultXrayConfig()
  263. if err == nil && config != nil {
  264. if cfgMap, ok := config.(map[string]any); ok {
  265. if outbounds, ok := cfgMap["outbounds"].([]any); ok {
  266. for _, outbound := range outbounds {
  267. obMap, ok := outbound.(map[string]any)
  268. if !ok {
  269. continue
  270. }
  271. tag, _ := obMap["tag"].(string)
  272. if tag == "" {
  273. continue
  274. }
  275. switch obMap["protocol"] {
  276. case "freedom":
  277. freedoms = append(freedoms, tag)
  278. case "blackhole":
  279. blackholes = append(blackholes, tag)
  280. }
  281. }
  282. }
  283. }
  284. }
  285. if len(freedoms) == 0 {
  286. freedoms = []string{"direct"}
  287. }
  288. if len(blackholes) == 0 {
  289. blackholes = []string{"blocked"}
  290. }
  291. return freedoms, blackholes
  292. }
  293. // AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds.
  294. // Kept for back-compat with the original /panel/api/server/cpuHistory/:bucket route;
  295. // the response key is "cpu" (not "v") so legacy consumers parse unchanged.
  296. func (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any {
  297. out := systemMetrics.aggregate("cpu", bucketSeconds, maxPoints)
  298. for _, p := range out {
  299. p["cpu"] = p["v"]
  300. delete(p, "v")
  301. }
  302. return out
  303. }
  304. // AggregateSystemMetric returns up to maxPoints averaged buckets for any
  305. // known system metric (see SystemMetricKeys). Output points have keys
  306. // {"t": unixSec, "v": value}; the caller decides how to format the value.
  307. func (s *ServerService) AggregateSystemMetric(metric string, bucketSeconds int, maxPoints int) []map[string]any {
  308. return systemMetrics.aggregate(metric, bucketSeconds, maxPoints)
  309. }
  310. type LogEntry struct {
  311. DateTime time.Time
  312. FromAddress string
  313. ToAddress string
  314. Inbound string
  315. Outbound string
  316. Email string
  317. Event int
  318. }
  319. func getPublicIP(url string) string {
  320. client := &http.Client{
  321. Timeout: 3 * time.Second,
  322. }
  323. req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
  324. if reqErr != nil {
  325. return "N/A"
  326. }
  327. resp, err := client.Do(req)
  328. if err != nil {
  329. return "N/A"
  330. }
  331. defer resp.Body.Close()
  332. // Don't retry if access is blocked or region-restricted
  333. if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnavailableForLegalReasons {
  334. return "N/A"
  335. }
  336. if resp.StatusCode != http.StatusOK {
  337. return "N/A"
  338. }
  339. ip, err := io.ReadAll(resp.Body)
  340. if err != nil {
  341. return "N/A"
  342. }
  343. ipString := strings.TrimSpace(string(ip))
  344. if ipString == "" {
  345. return "N/A"
  346. }
  347. return ipString
  348. }
  349. var publicIPv4Services = []string{
  350. "https://api4.ipify.org",
  351. "https://ipv4.icanhazip.com",
  352. "https://v4.api.ipinfo.io/ip",
  353. "https://ipv4.myexternalip.com/raw",
  354. "https://4.ident.me",
  355. "https://check-host.net/ip",
  356. }
  357. var publicIPv6Services = []string{
  358. "https://api6.ipify.org",
  359. "https://ipv6.icanhazip.com",
  360. "https://v6.api.ipinfo.io/ip",
  361. "https://ipv6.myexternalip.com/raw",
  362. "https://6.ident.me",
  363. }
  364. // resolvePublicIPs caches the public IPv4/IPv6 addresses on first use. Guarded
  365. // by s.mu because the bot's ServerService may call it from sendBackup while a
  366. // status report runs concurrently.
  367. func (s *ServerService) resolvePublicIPs() {
  368. s.mu.Lock()
  369. defer s.mu.Unlock()
  370. if s.cachedIPv4 == "" {
  371. for _, ip4Service := range publicIPv4Services {
  372. s.cachedIPv4 = getPublicIP(ip4Service)
  373. if s.cachedIPv4 != "N/A" {
  374. break
  375. }
  376. }
  377. }
  378. if s.cachedIPv6 == "" && !s.noIPv6 {
  379. for _, ip6Service := range publicIPv6Services {
  380. s.cachedIPv6 = getPublicIP(ip6Service)
  381. if s.cachedIPv6 != "N/A" {
  382. break
  383. }
  384. }
  385. }
  386. if s.cachedIPv6 == "N/A" {
  387. s.noIPv6 = true
  388. }
  389. }
  390. func (s *ServerService) GetStatus(lastStatus *Status) *Status {
  391. now := time.Now()
  392. status := &Status{
  393. T: now,
  394. }
  395. // CPU stats
  396. util, err := s.sampleCPUUtilization()
  397. if err != nil {
  398. logger.Warning("get cpu percent failed:", err)
  399. } else {
  400. status.Cpu = util
  401. }
  402. status.CpuCores, err = cpu.Counts(false)
  403. if err != nil {
  404. logger.Warning("get cpu cores count failed:", err)
  405. }
  406. status.LogicalPro = runtime.NumCPU()
  407. if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute {
  408. s.lastCpuInfoAttempt = time.Now()
  409. done := make(chan struct{})
  410. go func() {
  411. defer close(done)
  412. cpuInfos, err := cpu.Info()
  413. if err != nil {
  414. logger.Warning("get cpu info failed:", err)
  415. return
  416. }
  417. if len(cpuInfos) > 0 {
  418. s.cachedCpuSpeedMhz = cpuInfos[0].Mhz
  419. status.CpuSpeedMhz = s.cachedCpuSpeedMhz
  420. } else {
  421. logger.Warning("could not find cpu info")
  422. }
  423. }()
  424. select {
  425. case <-done:
  426. case <-time.After(1500 * time.Millisecond):
  427. logger.Warning("cpu info query timed out; will retry later")
  428. }
  429. } else if s.cachedCpuSpeedMhz != 0 {
  430. status.CpuSpeedMhz = s.cachedCpuSpeedMhz
  431. }
  432. // Uptime
  433. upTime, err := host.Uptime()
  434. if err != nil {
  435. logger.Warning("get uptime failed:", err)
  436. } else {
  437. status.Uptime = upTime
  438. }
  439. // Memory stats
  440. memInfo, err := mem.VirtualMemory()
  441. if err != nil {
  442. logger.Warning("get virtual memory failed:", err)
  443. } else {
  444. status.Mem.Current = memInfo.Used
  445. status.Mem.Total = memInfo.Total
  446. }
  447. swapInfo, err := mem.SwapMemory()
  448. if err != nil {
  449. logger.Warning("get swap memory failed:", err)
  450. } else {
  451. status.Swap.Current = swapInfo.Used
  452. status.Swap.Total = swapInfo.Total
  453. }
  454. // Disk stats
  455. diskInfo, err := disk.Usage("/")
  456. if err != nil {
  457. logger.Warning("get disk usage failed:", err)
  458. } else {
  459. status.Disk.Current = diskInfo.Used
  460. status.Disk.Total = diskInfo.Total
  461. }
  462. diskIOStats, err := disk.IOCounters()
  463. if err != nil {
  464. logger.Warning("get disk io counters failed:", err)
  465. } else {
  466. var totalRead, totalWrite uint64
  467. for _, counter := range diskIOStats {
  468. totalRead += counter.ReadBytes
  469. totalWrite += counter.WriteBytes
  470. }
  471. status.DiskTraffic.Read = totalRead
  472. status.DiskTraffic.Write = totalWrite
  473. if lastStatus != nil {
  474. duration := now.Sub(lastStatus.T)
  475. seconds := float64(duration) / float64(time.Second)
  476. if seconds > 0 && status.DiskTraffic.Read >= lastStatus.DiskTraffic.Read {
  477. status.DiskIO.Read = uint64(float64(status.DiskTraffic.Read-lastStatus.DiskTraffic.Read) / seconds)
  478. }
  479. if seconds > 0 && status.DiskTraffic.Write >= lastStatus.DiskTraffic.Write {
  480. status.DiskIO.Write = uint64(float64(status.DiskTraffic.Write-lastStatus.DiskTraffic.Write) / seconds)
  481. }
  482. }
  483. }
  484. // Load averages
  485. avgState, err := load.Avg()
  486. if err != nil {
  487. logger.Warning("get load avg failed:", err)
  488. } else {
  489. status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15}
  490. }
  491. // Network stats
  492. ioStats, err := net.IOCounters(true)
  493. if err != nil {
  494. logger.Warning("get io counters failed:", err)
  495. } else {
  496. var totalSent, totalRecv, totalPktSent, totalPktRecv uint64
  497. for _, iface := range ioStats {
  498. name := strings.ToLower(iface.Name)
  499. if isVirtualInterface(name) {
  500. continue
  501. }
  502. totalSent += iface.BytesSent
  503. totalRecv += iface.BytesRecv
  504. totalPktSent += iface.PacketsSent
  505. totalPktRecv += iface.PacketsRecv
  506. }
  507. status.NetTraffic.Sent = totalSent
  508. status.NetTraffic.Recv = totalRecv
  509. status.NetTraffic.PktSent = totalPktSent
  510. status.NetTraffic.PktRecv = totalPktRecv
  511. if lastStatus != nil {
  512. duration := now.Sub(lastStatus.T)
  513. seconds := float64(duration) / float64(time.Second)
  514. up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds)
  515. down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds)
  516. status.NetIO.Up = up
  517. status.NetIO.Down = down
  518. if seconds > 0 && status.NetTraffic.PktSent >= lastStatus.NetTraffic.PktSent {
  519. status.NetIO.PktUp = uint64(float64(status.NetTraffic.PktSent-lastStatus.NetTraffic.PktSent) / seconds)
  520. }
  521. if seconds > 0 && status.NetTraffic.PktRecv >= lastStatus.NetTraffic.PktRecv {
  522. status.NetIO.PktDown = uint64(float64(status.NetTraffic.PktRecv-lastStatus.NetTraffic.PktRecv) / seconds)
  523. }
  524. }
  525. }
  526. // TCP/UDP connections
  527. status.TcpCount, err = sys.GetTCPCount()
  528. if err != nil {
  529. logger.Warning("get tcp connections failed:", err)
  530. }
  531. status.UdpCount, err = sys.GetUDPCount()
  532. if err != nil {
  533. logger.Warning("get udp connections failed:", err)
  534. }
  535. s.resolvePublicIPs()
  536. status.PublicIP.IPv4 = s.cachedIPv4
  537. status.PublicIP.IPv6 = s.cachedIPv6
  538. // Xray status
  539. if s.xrayService.IsXrayRunning() {
  540. status.Xray.State = Running
  541. status.Xray.ErrorMsg = ""
  542. } else {
  543. err := s.xrayService.GetXrayErr()
  544. if err != nil {
  545. status.Xray.State = Error
  546. } else {
  547. status.Xray.State = Stop
  548. }
  549. status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
  550. }
  551. status.Xray.Version = s.xrayService.GetXrayVersion()
  552. status.PanelVersion = config.GetPanelVersion()
  553. if guid, err := s.settingService.GetPanelGuid(); err == nil {
  554. status.PanelGuid = guid
  555. }
  556. // Application stats
  557. if rss := sys.SelfRSS(); rss > 0 {
  558. status.AppStats.Mem = rss
  559. } else {
  560. var rtm runtime.MemStats
  561. runtime.ReadMemStats(&rtm)
  562. status.AppStats.Mem = rtm.Sys
  563. }
  564. status.AppStats.Threads = uint32(runtime.NumGoroutine())
  565. if p != nil && p.IsRunning() {
  566. status.AppStats.Uptime = p.GetUptime()
  567. } else {
  568. status.AppStats.Uptime = 0
  569. }
  570. return status
  571. }
  572. // AppendCpuSample is preserved for callers that only have the CPU number.
  573. // New callers should prefer AppendStatusSample which writes the full set.
  574. func (s *ServerService) AppendCpuSample(t time.Time, v float64) {
  575. systemMetrics.append("cpu", t, v)
  576. }
  577. // AppendStatusSample writes one tick of every metric we keep — CPU, memory
  578. // percent, network throughput (bytes/s), online client count, and the three
  579. // load averages. Called by RefreshStatus on the same @2s cadence as
  580. // AppendCpuSample, so all series stay aligned.
  581. func (s *ServerService) AppendStatusSample(t time.Time, status *Status) {
  582. if status == nil {
  583. return
  584. }
  585. systemMetrics.append("cpu", t, status.Cpu)
  586. if status.Mem.Total > 0 {
  587. systemMetrics.append("mem", t, float64(status.Mem.Current)*100.0/float64(status.Mem.Total))
  588. }
  589. if status.Swap.Total > 0 {
  590. systemMetrics.append("swap", t, float64(status.Swap.Current)*100.0/float64(status.Swap.Total))
  591. } else {
  592. systemMetrics.append("swap", t, 0)
  593. }
  594. systemMetrics.append("netUp", t, float64(status.NetIO.Up))
  595. systemMetrics.append("netDown", t, float64(status.NetIO.Down))
  596. systemMetrics.append("diskRead", t, float64(status.DiskIO.Read))
  597. systemMetrics.append("diskWrite", t, float64(status.DiskIO.Write))
  598. if status.Disk.Total > 0 {
  599. systemMetrics.append("diskUsage", t, float64(status.Disk.Current)*100.0/float64(status.Disk.Total))
  600. }
  601. systemMetrics.append("pktUp", t, float64(status.NetIO.PktUp))
  602. systemMetrics.append("pktDown", t, float64(status.NetIO.PktDown))
  603. systemMetrics.append("tcpCount", t, float64(status.TcpCount))
  604. systemMetrics.append("udpCount", t, float64(status.UdpCount))
  605. online := 0
  606. if p != nil && p.IsRunning() {
  607. online = len(p.GetOnlineClients())
  608. }
  609. systemMetrics.append("online", t, float64(online))
  610. if len(status.Loads) >= 3 {
  611. systemMetrics.append("load1", t, status.Loads[0])
  612. systemMetrics.append("load5", t, status.Loads[1])
  613. systemMetrics.append("load15", t, status.Loads[2])
  614. }
  615. }
  616. func (s *ServerService) sampleCPUUtilization() (float64, error) {
  617. // Try native platform-specific CPU implementation first (Windows, Linux, macOS)
  618. if pct, err := sys.CPUPercentRaw(); err == nil {
  619. s.mu.Lock()
  620. // First call to native method returns 0 (initializes baseline)
  621. if !s.hasNativeCPUSample {
  622. s.hasNativeCPUSample = true
  623. s.mu.Unlock()
  624. return 0, nil
  625. }
  626. // Smooth with EMA
  627. const alpha = 0.3
  628. if s.emaCPU == 0 {
  629. s.emaCPU = pct
  630. } else {
  631. s.emaCPU = alpha*pct + (1-alpha)*s.emaCPU
  632. }
  633. val := s.emaCPU
  634. s.mu.Unlock()
  635. return val, nil
  636. }
  637. // If native call fails, fall back to gopsutil times
  638. // Read aggregate CPU times (all CPUs combined)
  639. times, err := cpu.Times(false)
  640. if err != nil {
  641. return 0, err
  642. }
  643. if len(times) == 0 {
  644. return 0, fmt.Errorf("no cpu times available")
  645. }
  646. cur := times[0]
  647. s.mu.Lock()
  648. defer s.mu.Unlock()
  649. // If this is the first sample, initialize and return current EMA (0 by default)
  650. if !s.hasLastCPUSample {
  651. s.lastCPUTimes = cur
  652. s.hasLastCPUSample = true
  653. return s.emaCPU, nil
  654. }
  655. // Compute busy and total deltas
  656. // Note: Guest and GuestNice times are already included in User and Nice respectively,
  657. // so we exclude them to avoid double-counting (Linux kernel accounting)
  658. idleDelta := cur.Idle - s.lastCPUTimes.Idle
  659. busyDelta := (cur.User - s.lastCPUTimes.User) +
  660. (cur.System - s.lastCPUTimes.System) +
  661. (cur.Nice - s.lastCPUTimes.Nice) +
  662. (cur.Iowait - s.lastCPUTimes.Iowait) +
  663. (cur.Irq - s.lastCPUTimes.Irq) +
  664. (cur.Softirq - s.lastCPUTimes.Softirq) +
  665. (cur.Steal - s.lastCPUTimes.Steal)
  666. totalDelta := busyDelta + idleDelta
  667. // Update last sample for next time
  668. s.lastCPUTimes = cur
  669. // Guard against division by zero or negative deltas (e.g., counter resets)
  670. if totalDelta <= 0 {
  671. return s.emaCPU, nil
  672. }
  673. raw := 100.0 * (busyDelta / totalDelta)
  674. if raw < 0 {
  675. raw = 0
  676. }
  677. if raw > 100 {
  678. raw = 100
  679. }
  680. // Exponential moving average to smooth spikes
  681. const alpha = 0.3 // smoothing factor (0<alpha<=1). Higher = more responsive, lower = smoother
  682. if s.emaCPU == 0 {
  683. // Initialize EMA with the first real reading to avoid long warm-up from zero
  684. s.emaCPU = raw
  685. } else {
  686. s.emaCPU = alpha*raw + (1-alpha)*s.emaCPU
  687. }
  688. return s.emaCPU, nil
  689. }
  690. const (
  691. maxXrayArchiveBytes = 200 << 20
  692. maxXrayBinaryBytes = 200 << 20
  693. // maxXrayDigestBytes caps the .dgst checksum sidecar read; it is a few
  694. // hundred bytes in practice.
  695. maxXrayDigestBytes = 64 << 10
  696. )
  697. func (s *ServerService) GetXrayVersions() ([]string, error) {
  698. const (
  699. XrayURL = "https://api.github.com/repos/XTLS/Xray-core/releases"
  700. bufferSize = 8192
  701. )
  702. req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, XrayURL, nil)
  703. if reqErr != nil {
  704. return nil, reqErr
  705. }
  706. resp, err := s.settingService.NewProxiedHTTPClient(10 * time.Second).Do(req)
  707. if err != nil {
  708. return nil, err
  709. }
  710. defer resp.Body.Close()
  711. // Check HTTP status code - GitHub API returns object instead of array on error
  712. if resp.StatusCode != http.StatusOK {
  713. bodyBytes, _ := io.ReadAll(resp.Body)
  714. var errorResponse struct {
  715. Message string `json:"message"`
  716. }
  717. if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" {
  718. return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message)
  719. }
  720. return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status)
  721. }
  722. buffer := bytes.NewBuffer(make([]byte, bufferSize))
  723. buffer.Reset()
  724. if _, err := buffer.ReadFrom(resp.Body); err != nil {
  725. return nil, err
  726. }
  727. var releases []Release
  728. if err := json.Unmarshal(buffer.Bytes(), &releases); err != nil {
  729. return nil, err
  730. }
  731. var versions []string
  732. for _, release := range releases {
  733. tagVersion := strings.TrimPrefix(release.TagName, "v")
  734. tagParts := strings.Split(tagVersion, ".")
  735. if len(tagParts) != 3 {
  736. continue
  737. }
  738. major, err1 := strconv.Atoi(tagParts[0])
  739. minor, err2 := strconv.Atoi(tagParts[1])
  740. patch, err3 := strconv.Atoi(tagParts[2])
  741. if err1 != nil || err2 != nil || err3 != nil {
  742. continue
  743. }
  744. if major > 26 || (major == 26 && minor > 6) || (major == 26 && minor == 6 && patch >= 27) {
  745. versions = append(versions, release.TagName)
  746. }
  747. }
  748. return versions, nil
  749. }
  750. func (s *ServerService) StopXrayService() error {
  751. err := s.xrayService.StopXray()
  752. if err != nil {
  753. logger.Error("stop xray failed:", err)
  754. return err
  755. }
  756. return nil
  757. }
  758. func (s *ServerService) RestartXrayService() error {
  759. err := s.xrayService.RestartXray(true)
  760. if err != nil {
  761. logger.Error("start xray failed:", err)
  762. return err
  763. }
  764. return nil
  765. }
  766. func (s *ServerService) downloadXRay(version string) (string, error) {
  767. osName := runtime.GOOS
  768. arch := runtime.GOARCH
  769. switch osName {
  770. case "darwin":
  771. osName = "macos"
  772. case "windows":
  773. osName = "windows"
  774. }
  775. switch arch {
  776. case "amd64":
  777. arch = "64"
  778. case "arm64":
  779. arch = "arm64-v8a"
  780. case "armv7":
  781. arch = "arm32-v7a"
  782. case "armv6":
  783. arch = "arm32-v6"
  784. case "armv5":
  785. arch = "arm32-v5"
  786. case "386":
  787. arch = "32"
  788. case "s390x":
  789. arch = "s390x"
  790. }
  791. fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
  792. url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
  793. client := s.settingService.NewProxiedHTTPClient(60 * time.Second)
  794. req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
  795. if reqErr != nil {
  796. return "", reqErr
  797. }
  798. resp, err := client.Do(req)
  799. if err != nil {
  800. return "", err
  801. }
  802. defer resp.Body.Close()
  803. if resp.StatusCode != http.StatusOK {
  804. return "", fmt.Errorf("download xray: unexpected HTTP %d", resp.StatusCode)
  805. }
  806. if resp.ContentLength > maxXrayArchiveBytes {
  807. return "", fmt.Errorf("download xray: archive exceeds %d bytes", maxXrayArchiveBytes)
  808. }
  809. file, err := os.CreateTemp("", "xray-*.zip")
  810. if err != nil {
  811. return "", err
  812. }
  813. path := file.Name()
  814. ok := false
  815. defer func() {
  816. _ = file.Close()
  817. if !ok {
  818. _ = os.Remove(path)
  819. }
  820. }()
  821. n, err := io.Copy(file, io.LimitReader(resp.Body, maxXrayArchiveBytes+1))
  822. if err != nil {
  823. return "", err
  824. }
  825. if n > maxXrayArchiveBytes {
  826. return "", fmt.Errorf("download xray: archive exceeds %d bytes", maxXrayArchiveBytes)
  827. }
  828. // Verify the archive against the SHA2-256 published in the release's .dgst
  829. // sidecar before installing it. TLS protects the transport, not the artifact;
  830. // a corrupted or tampered asset must not be installed and run as xray.
  831. want, err := s.fetchXrayDigestSHA256(client, url+".dgst")
  832. if err != nil {
  833. return "", err
  834. }
  835. if _, err := file.Seek(0, io.SeekStart); err != nil {
  836. return "", err
  837. }
  838. hasher := sha256.New()
  839. if _, err := io.Copy(hasher, file); err != nil {
  840. return "", err
  841. }
  842. if got := hex.EncodeToString(hasher.Sum(nil)); !strings.EqualFold(got, want) {
  843. // User-facing warning: the archive's SHA-256 does not match the official
  844. // release checksum, so the download is corrupted or has been tampered
  845. // with. Abort the install so a bad binary is never run, and tell the user
  846. // to retry/re-download rather than proceed with a mismatched image.
  847. return "", fmt.Errorf("Xray update aborted: the downloaded archive does not match the official SHA-256 checksum, so the image is corrupted or differs from the official release. Please exit and re-download the official image, then try again (expected %s, got %s)", want, got)
  848. }
  849. ok = true
  850. return path, nil
  851. }
  852. // fetchXrayDigestSHA256 downloads the .dgst sidecar XTLS publishes next to each
  853. // release asset and returns the SHA2-256 hex digest it lists.
  854. func (s *ServerService) fetchXrayDigestSHA256(client *http.Client, dgstURL string) (string, error) {
  855. req, reqErr := http.NewRequestWithContext(context.Background(), http.MethodGet, dgstURL, nil)
  856. if reqErr != nil {
  857. return "", fmt.Errorf("download xray checksum: %w", reqErr)
  858. }
  859. resp, err := client.Do(req)
  860. if err != nil {
  861. return "", fmt.Errorf("download xray checksum: %w", err)
  862. }
  863. defer resp.Body.Close()
  864. if resp.StatusCode != http.StatusOK {
  865. return "", fmt.Errorf("download xray checksum: unexpected HTTP %d", resp.StatusCode)
  866. }
  867. raw, err := io.ReadAll(io.LimitReader(resp.Body, maxXrayDigestBytes))
  868. if err != nil {
  869. return "", fmt.Errorf("download xray checksum: %w", err)
  870. }
  871. return parseXrayDigestSHA256(raw)
  872. }
  873. // parseXrayDigestSHA256 extracts the lowercase SHA2-256 hex from an XTLS .dgst
  874. // file, whose lines are "ALGO= <hex>" (the relevant one being "SHA2-256= ...").
  875. func parseXrayDigestSHA256(dgst []byte) (string, error) {
  876. for line := range strings.SplitSeq(string(dgst), "\n") {
  877. rest, ok := strings.CutPrefix(strings.TrimSpace(line), "SHA2-256=")
  878. if !ok {
  879. continue
  880. }
  881. h := strings.ToLower(strings.TrimSpace(rest))
  882. if len(h) != 64 {
  883. return "", fmt.Errorf("xray checksum: malformed SHA2-256 entry in digest")
  884. }
  885. return h, nil
  886. }
  887. return "", fmt.Errorf("xray checksum: no SHA2-256 entry in digest")
  888. }
  889. func (s *ServerService) UpdateXray(version string) error {
  890. versions, err := s.GetXrayVersions()
  891. if err != nil {
  892. return err
  893. }
  894. if !slices.Contains(versions, version) {
  895. return fmt.Errorf("xray version %q is not in the fetched release list", version)
  896. }
  897. // 1. Stop xray before doing anything
  898. if err := s.StopXrayService(); err != nil {
  899. logger.Warning("failed to stop xray before update:", err)
  900. }
  901. // 2. Download the zip
  902. zipFileName, err := s.downloadXRay(version)
  903. if err != nil {
  904. return err
  905. }
  906. defer os.Remove(zipFileName)
  907. zipFile, err := os.Open(zipFileName)
  908. if err != nil {
  909. return err
  910. }
  911. defer zipFile.Close()
  912. stat, err := zipFile.Stat()
  913. if err != nil {
  914. return err
  915. }
  916. reader, err := zip.NewReader(zipFile, stat.Size())
  917. if err != nil {
  918. return err
  919. }
  920. // 3. Helper to extract files
  921. copyZipFile := func(zipName string, fileName string) error {
  922. zipFile, err := reader.Open(zipName)
  923. if err != nil {
  924. return err
  925. }
  926. defer zipFile.Close()
  927. if err := os.MkdirAll(filepath.Dir(fileName), 0o755); err != nil {
  928. return err
  929. }
  930. tmpFile, err := os.CreateTemp(filepath.Dir(fileName), ".xray-*")
  931. if err != nil {
  932. return err
  933. }
  934. tmpPath := tmpFile.Name()
  935. ok := false
  936. defer func() {
  937. _ = tmpFile.Close()
  938. if !ok {
  939. _ = os.Remove(tmpPath)
  940. }
  941. }()
  942. n, err := io.Copy(tmpFile, io.LimitReader(zipFile, maxXrayBinaryBytes+1))
  943. if err != nil {
  944. return err
  945. }
  946. if n > maxXrayBinaryBytes {
  947. return fmt.Errorf("xray binary exceeds %d bytes", maxXrayBinaryBytes)
  948. }
  949. if err := tmpFile.Chmod(0o755); err != nil {
  950. return err
  951. }
  952. if err := tmpFile.Close(); err != nil {
  953. return err
  954. }
  955. if runtime.GOOS == "windows" {
  956. _ = os.Remove(fileName)
  957. }
  958. if err := os.Rename(tmpPath, fileName); err != nil {
  959. return err
  960. }
  961. ok = true
  962. return nil
  963. }
  964. // 4. Extract correct binary
  965. if runtime.GOOS == "windows" {
  966. targetBinary := filepath.Join(config.GetBinFolderPath(), "xray-windows-amd64.exe")
  967. err = copyZipFile("xray.exe", targetBinary)
  968. } else {
  969. err = copyZipFile("xray", xray.GetBinaryPath())
  970. }
  971. if err != nil {
  972. return err
  973. }
  974. // 5. Restart xray
  975. if err := s.xrayService.RestartXray(true); err != nil {
  976. logger.Error("start xray failed:", err)
  977. return err
  978. }
  979. return nil
  980. }
  981. func (s *ServerService) GetLogs(count string, level string, syslog string) []string {
  982. c, _ := strconv.Atoi(count)
  983. var lines []string
  984. if syslog == "true" {
  985. // Check if running on Windows - journalctl is not available
  986. if runtime.GOOS == "windows" {
  987. return []string{"Syslog is not supported on Windows. Please use application logs instead by unchecking the 'Syslog' option."}
  988. }
  989. // Validate and sanitize count parameter
  990. countInt, err := strconv.Atoi(count)
  991. if err != nil || countInt < 1 || countInt > 10000 {
  992. return []string{"Invalid count parameter - must be a number between 1 and 10000"}
  993. }
  994. // Validate level parameter - only allow valid syslog levels
  995. validLevels := map[string]bool{
  996. "0": true, "emerg": true,
  997. "1": true, "alert": true,
  998. "2": true, "crit": true,
  999. "3": true, "err": true,
  1000. "4": true, "warning": true,
  1001. "5": true, "notice": true,
  1002. "6": true, "info": true,
  1003. "7": true, "debug": true,
  1004. }
  1005. if !validLevels[level] {
  1006. return []string{"Invalid level parameter - must be a valid syslog level"}
  1007. }
  1008. // Use hardcoded command with validated parameters
  1009. cmd := exec.CommandContext(context.Background(), "journalctl", "-u", "x-ui", "--no-pager", "-n", strconv.Itoa(countInt), "-p", level)
  1010. var out bytes.Buffer
  1011. cmd.Stdout = &out
  1012. err = cmd.Run()
  1013. if err != nil {
  1014. return []string{"Failed to run journalctl command! Make sure systemd is available and x-ui service is registered."}
  1015. }
  1016. lines = strings.Split(out.String(), "\n")
  1017. } else {
  1018. lines = logger.GetLogs(c, level)
  1019. }
  1020. return lines
  1021. }
  1022. func (s *ServerService) GetXrayLogs(
  1023. count string,
  1024. filter string,
  1025. showDirect string,
  1026. showBlocked string,
  1027. showProxy string,
  1028. freedoms []string,
  1029. blackholes []string,
  1030. ) []LogEntry {
  1031. const (
  1032. Direct = iota
  1033. Blocked
  1034. Proxied
  1035. )
  1036. countInt, _ := strconv.Atoi(count)
  1037. var entries []LogEntry
  1038. pathToAccessLog, err := xray.GetAccessLogPath()
  1039. if err != nil {
  1040. return nil
  1041. }
  1042. file, err := os.Open(pathToAccessLog)
  1043. if err != nil {
  1044. return nil
  1045. }
  1046. defer file.Close()
  1047. scanner := bufio.NewScanner(file)
  1048. for scanner.Scan() {
  1049. line := strings.TrimSpace(scanner.Text())
  1050. if line == "" || strings.Contains(line, "api -> api") {
  1051. // skipping empty lines and api calls
  1052. continue
  1053. }
  1054. if filter != "" && !strings.Contains(line, filter) {
  1055. // applying filter if it's not empty
  1056. continue
  1057. }
  1058. var entry LogEntry
  1059. parts := strings.Fields(line)
  1060. for i, part := range parts {
  1061. if i == 0 {
  1062. dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
  1063. if err != nil {
  1064. continue
  1065. }
  1066. entry.DateTime = dateTime.UTC()
  1067. }
  1068. if part == "from" {
  1069. entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
  1070. } else if part == "accepted" {
  1071. entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
  1072. } else if strings.HasPrefix(part, "[") {
  1073. entry.Inbound = part[1:]
  1074. } else if strings.HasSuffix(part, "]") {
  1075. entry.Outbound = part[:len(part)-1]
  1076. } else if part == "email:" {
  1077. entry.Email = parts[i+1]
  1078. }
  1079. }
  1080. if logEntryContains(line, freedoms) {
  1081. if showDirect == "false" {
  1082. continue
  1083. }
  1084. entry.Event = Direct
  1085. } else if logEntryContains(line, blackholes) {
  1086. if showBlocked == "false" {
  1087. continue
  1088. }
  1089. entry.Event = Blocked
  1090. } else {
  1091. if showProxy == "false" {
  1092. continue
  1093. }
  1094. entry.Event = Proxied
  1095. }
  1096. entries = append(entries, entry)
  1097. }
  1098. if err := scanner.Err(); err != nil {
  1099. return nil
  1100. }
  1101. if len(entries) > countInt {
  1102. entries = entries[len(entries)-countInt:]
  1103. }
  1104. return entries
  1105. }
  1106. // isVirtualInterface returns true for loopback and virtual/tunnel interfaces
  1107. // that should be excluded from network traffic statistics.
  1108. func isVirtualInterface(name string) bool {
  1109. // Exact matches
  1110. if name == "lo" || name == "lo0" {
  1111. return true
  1112. }
  1113. // Prefix matches for virtual/tunnel interfaces
  1114. virtualPrefixes := []string{
  1115. "loopback",
  1116. "docker",
  1117. "br-",
  1118. "veth",
  1119. "virbr",
  1120. "tun",
  1121. "tap",
  1122. "wg",
  1123. "tailscale",
  1124. "zt",
  1125. }
  1126. for _, prefix := range virtualPrefixes {
  1127. if strings.HasPrefix(name, prefix) {
  1128. return true
  1129. }
  1130. }
  1131. return false
  1132. }
  1133. func logEntryContains(line string, suffixes []string) bool {
  1134. for _, sfx := range suffixes {
  1135. if strings.Contains(line, sfx+"]") {
  1136. return true
  1137. }
  1138. }
  1139. return false
  1140. }
  1141. func (s *ServerService) GetConfigJson() (any, error) {
  1142. config, err := s.xrayService.GetXrayConfig()
  1143. if err != nil {
  1144. return nil, err
  1145. }
  1146. contents, err := json.MarshalIndent(config, "", " ")
  1147. if err != nil {
  1148. return nil, err
  1149. }
  1150. var jsonData any
  1151. err = json.Unmarshal(contents, &jsonData)
  1152. if err != nil {
  1153. return nil, err
  1154. }
  1155. return jsonData, nil
  1156. }
  1157. func (s *ServerService) GetDb() ([]byte, error) {
  1158. if database.IsPostgres() {
  1159. return s.exportPostgresDB()
  1160. }
  1161. // Update by manually trigger a checkpoint operation
  1162. err := database.Checkpoint()
  1163. if err != nil {
  1164. return nil, err
  1165. }
  1166. // Open the file for reading
  1167. file, err := os.Open(config.GetDBPath())
  1168. if err != nil {
  1169. return nil, err
  1170. }
  1171. defer file.Close()
  1172. // Read the file contents
  1173. fileContents, err := io.ReadAll(file)
  1174. if err != nil {
  1175. return nil, err
  1176. }
  1177. return fileContents, nil
  1178. }
  1179. // BackupFilename returns the filename for a database backup, named after the
  1180. // panel's address so a downloaded or Telegram-sent backup identifies the server
  1181. // it came from, followed by the current date and time (_YYYY-MM-DD_HHMMSS) so
  1182. // files accumulated in Telegram chat history group by server then sort
  1183. // chronologically and same-day backups stay distinct. requestHost is the
  1184. // browser's address: the getDb handler passes c.Request.Host so a panel download
  1185. // is named after whatever address the user reached the panel with, no Listen
  1186. // Domain needed. The Telegram bot has no request and passes "", falling back to
  1187. // the configured Listen Domain (webDomain) and then the public IP. The extension
  1188. // is .dump on PostgreSQL and .db on SQLite; the base falls back to "x-ui" when
  1189. // no address is known.
  1190. func (s *ServerService) BackupFilename(requestHost string) string {
  1191. ext := ".db"
  1192. if database.IsPostgres() {
  1193. ext = ".dump"
  1194. }
  1195. return s.backupHost(requestHost) + backupDateSuffix(time.Now()) + ext
  1196. }
  1197. // backupDateSuffix returns the _YYYY-MM-DD_HHMMSS chronological suffix appended
  1198. // after the host in backup filenames. Uses server-local time for consistency
  1199. // with the timestamp printed in the Telegram backup message body.
  1200. func backupDateSuffix(now time.Time) string {
  1201. return "_" + now.Format("2006-01-02_150405")
  1202. }
  1203. // backupHost picks the address used to name backup files: the browser's request
  1204. // host (port stripped) when available, otherwise the configured Listen Domain
  1205. // (webDomain) and then the resolved public IP (IPv4 before IPv6), reduced to safe
  1206. // filename characters. The public IP is resolved directly rather than read from
  1207. // LastStatus so callers whose ServerService never runs the status ticker —
  1208. // notably the Telegram bot — still get a real address instead of the "x-ui"
  1209. // fallback.
  1210. func (s *ServerService) backupHost(requestHost string) string {
  1211. host := extractHostname(strings.TrimSpace(requestHost))
  1212. if host == "" {
  1213. if domain, err := s.settingService.GetWebDomain(); err == nil {
  1214. host = strings.TrimSpace(domain)
  1215. }
  1216. }
  1217. if host == "" {
  1218. s.resolvePublicIPs()
  1219. if ip := s.cachedIPv4; ip != "" && ip != "N/A" {
  1220. host = ip
  1221. } else if ip := s.cachedIPv6; ip != "" && ip != "N/A" {
  1222. host = ip
  1223. }
  1224. }
  1225. return sanitizeBackupHost(host)
  1226. }
  1227. // sanitizeBackupHost reduces a host to characters safe in a download filename
  1228. // (the getDb handler enforces ^[a-zA-Z0-9_\-.]+$). IPv6 brackets are stripped
  1229. // and any other character — such as the colons in an IPv6 address — becomes a
  1230. // hyphen. Returns "x-ui" when nothing usable remains.
  1231. func sanitizeBackupHost(host string) string {
  1232. host = strings.Trim(host, "[]")
  1233. var b strings.Builder
  1234. for _, r := range host {
  1235. switch {
  1236. case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '-', r == '_':
  1237. b.WriteRune(r)
  1238. default:
  1239. b.WriteRune('-')
  1240. }
  1241. }
  1242. out := strings.Trim(b.String(), ".-")
  1243. if out == "" {
  1244. return "x-ui"
  1245. }
  1246. return out
  1247. }
  1248. // GetMigration produces a cross-engine migration file plus its filename: on a
  1249. // SQLite panel it returns a portable .dump (SQL text), and on a PostgreSQL panel
  1250. // it returns a .db SQLite database built from the live data. Either output can
  1251. // then seed a panel running on the other backend.
  1252. func (s *ServerService) GetMigration() ([]byte, string, error) {
  1253. if database.IsPostgres() {
  1254. tmp, err := os.CreateTemp("", "x-ui-migration-*.db")
  1255. if err != nil {
  1256. return nil, "", err
  1257. }
  1258. tmpPath := tmp.Name()
  1259. tmp.Close()
  1260. defer os.Remove(tmpPath)
  1261. if err := database.ExportPostgresToSQLite(config.GetDBDSN(), tmpPath); err != nil {
  1262. return nil, "", err
  1263. }
  1264. data, err := os.ReadFile(tmpPath)
  1265. if err != nil {
  1266. return nil, "", err
  1267. }
  1268. return data, "x-ui.db", nil
  1269. }
  1270. // SQLite panel: checkpoint so the .db reflects the latest writes, then dump.
  1271. if err := database.Checkpoint(); err != nil {
  1272. return nil, "", err
  1273. }
  1274. data, err := database.DumpSQLiteToBytes(config.GetDBPath())
  1275. if err != nil {
  1276. return nil, "", err
  1277. }
  1278. return data, "x-ui.dump", nil
  1279. }
  1280. func (s *ServerService) ImportDB(file multipart.File) error {
  1281. if database.IsPostgres() {
  1282. return s.importPostgresDB(file)
  1283. }
  1284. kind, err := sniffUploadKind(file)
  1285. if err != nil {
  1286. return common.NewErrorf("Error reading uploaded file: %v", err)
  1287. }
  1288. switch kind {
  1289. case importKindSQLiteDB, importKindSQLiteDump:
  1290. case importKindPgDump:
  1291. return common.NewError("This file is a PostgreSQL backup; it can only be restored on a panel running PostgreSQL")
  1292. default:
  1293. return common.NewError("Invalid file: expected a SQLite database (.db) from Back Up or a SQLite migration dump (.dump)")
  1294. }
  1295. tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
  1296. if _, err := os.Stat(tempPath); err == nil {
  1297. if errRemove := os.Remove(tempPath); errRemove != nil {
  1298. return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
  1299. }
  1300. }
  1301. defer func() {
  1302. if _, err := os.Stat(tempPath); err == nil {
  1303. if rerr := os.Remove(tempPath); rerr != nil {
  1304. logger.Warningf("Warning: failed to remove temp file: %v", rerr)
  1305. }
  1306. }
  1307. }()
  1308. if err := stageSQLiteUpload(file, kind, tempPath); err != nil {
  1309. return err
  1310. }
  1311. if err = database.ValidateSQLiteDB(tempPath); err != nil {
  1312. return common.NewErrorf("Invalid or corrupt db file: %v", err)
  1313. }
  1314. if err = database.PrepareSQLiteForMigration(tempPath); err != nil {
  1315. return common.NewErrorf("This file cannot be imported: %v", err)
  1316. }
  1317. xrayStopped := true
  1318. defer func() {
  1319. if xrayStopped {
  1320. if errR := s.RestartXrayService(); errR != nil {
  1321. logger.Warningf("Failed to restart Xray after DB import error: %v", errR)
  1322. }
  1323. }
  1324. }()
  1325. if errStop := s.StopXrayService(); errStop != nil {
  1326. logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
  1327. }
  1328. if errClose := database.CloseDB(); errClose != nil {
  1329. logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
  1330. }
  1331. // Registered after the xray-restart defer so it runs first (LIFO): every
  1332. // error return below leaves a database file at the configured path, and the
  1333. // restart needs an open pool to build the xray config from it.
  1334. dbReopened := false
  1335. defer func() {
  1336. if dbReopened {
  1337. return
  1338. }
  1339. if errReopen := database.InitDB(config.GetDBPath()); errReopen != nil {
  1340. logger.Warningf("Failed to reopen the database after import error: %v", errReopen)
  1341. }
  1342. }()
  1343. // Backup the current database for fallback
  1344. fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
  1345. // Remove the existing fallback file (if any)
  1346. if _, err := os.Stat(fallbackPath); err == nil {
  1347. if errRemove := os.Remove(fallbackPath); errRemove != nil {
  1348. return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
  1349. }
  1350. }
  1351. // Move the current database to the fallback location
  1352. if err = os.Rename(config.GetDBPath(), fallbackPath); err != nil {
  1353. return common.NewErrorf("Error backing up current db file: %v", err)
  1354. }
  1355. // Move temp to DB path
  1356. if err = os.Rename(tempPath, config.GetDBPath()); err != nil {
  1357. // Restore from fallback
  1358. if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
  1359. return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
  1360. }
  1361. return common.NewErrorf("Error moving db file: %v", err)
  1362. }
  1363. // Open & migrate new DB
  1364. if err = database.InitDB(config.GetDBPath()); err != nil {
  1365. // A failed InitDB still holds the imported file open; close before the
  1366. // rename or Windows refuses to replace it.
  1367. if errClose := database.CloseDB(); errClose != nil {
  1368. logger.Warningf("Failed to close the imported DB before restoring fallback: %v", errClose)
  1369. }
  1370. if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
  1371. return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
  1372. }
  1373. return common.NewErrorf("Error migrating db: %v", err)
  1374. }
  1375. dbReopened = true
  1376. s.inboundService.MigrateDB()
  1377. xrayStopped = false
  1378. if err = s.RestartXrayService(); err != nil {
  1379. return common.NewErrorf("Imported DB but failed to start Xray: %v; the previous database was kept at %s", err, fallbackPath)
  1380. }
  1381. if _, err := os.Stat(fallbackPath); err == nil {
  1382. if rerr := os.Remove(fallbackPath); rerr != nil {
  1383. logger.Warningf("Warning: failed to remove fallback file: %v", rerr)
  1384. }
  1385. }
  1386. return nil
  1387. }
  1388. // pgConnEnv turns the configured PostgreSQL DSN into the PG* environment used by
  1389. // pg_dump/pg_restore, keeping the password out of the process argument list.
  1390. func pgConnEnv(dsn string) (env []string, dbname string, err error) {
  1391. u, err := url.Parse(strings.TrimSpace(dsn))
  1392. if err != nil {
  1393. return nil, "", err
  1394. }
  1395. if u.Scheme != "postgres" && u.Scheme != "postgresql" {
  1396. return nil, "", common.NewErrorf("unsupported DSN scheme %q", u.Scheme)
  1397. }
  1398. dbname = strings.TrimPrefix(u.Path, "/")
  1399. if dbname == "" {
  1400. return nil, "", common.NewError("PostgreSQL DSN is missing a database name")
  1401. }
  1402. host := u.Hostname()
  1403. if host == "" {
  1404. host = "127.0.0.1"
  1405. }
  1406. port := u.Port()
  1407. if port == "" {
  1408. port = "5432"
  1409. }
  1410. env = append(os.Environ(), "PGHOST="+host, "PGPORT="+port, "PGDATABASE="+dbname)
  1411. if user := u.User.Username(); user != "" {
  1412. env = append(env, "PGUSER="+user)
  1413. }
  1414. if pass, ok := u.User.Password(); ok {
  1415. env = append(env, "PGPASSWORD="+pass)
  1416. }
  1417. if sslmode := u.Query().Get("sslmode"); sslmode != "" {
  1418. env = append(env, "PGSSLMODE="+sslmode)
  1419. }
  1420. return env, dbname, nil
  1421. }
  1422. func (s *ServerService) exportPostgresDB() ([]byte, error) {
  1423. bin, err := exec.LookPath("pg_dump")
  1424. if err != nil {
  1425. return nil, common.NewError("pg_dump not found on the server; install the postgresql-client package to back up a PostgreSQL database")
  1426. }
  1427. env, dbname, err := pgConnEnv(config.GetDBDSN())
  1428. if err != nil {
  1429. return nil, common.NewErrorf("invalid PostgreSQL DSN: %v", err)
  1430. }
  1431. cmd := exec.CommandContext(context.Background(), bin, "--format=custom", "--no-owner", "--no-privileges", "--dbname", dbname)
  1432. cmd.Env = env
  1433. var out, stderr bytes.Buffer
  1434. cmd.Stdout = &out
  1435. cmd.Stderr = &stderr
  1436. if err := cmd.Run(); err != nil {
  1437. return nil, common.NewErrorf("pg_dump failed: %v: %s", err, strings.TrimSpace(stderr.String()))
  1438. }
  1439. return out.Bytes(), nil
  1440. }
  1441. var (
  1442. pgUnsupportedDumpVersionPattern = regexp.MustCompile(`unsupported version \((\d+\.\d+)\) in file header`)
  1443. pgToolVersionPattern = regexp.MustCompile(`\d+(?:\.\d+)+`)
  1444. )
  1445. var pgArchiveVersionIntroducedIn = map[string]int{
  1446. "1.15": 16,
  1447. "1.16": 17,
  1448. }
  1449. // checkPgRestoreCanRead probes the dump with pg_restore --list (reads only the
  1450. // TOC, no database connection) so an unreadable file fails before Xray is stopped.
  1451. func checkPgRestoreCanRead(bin, dumpPath string) error {
  1452. cmd := exec.CommandContext(context.Background(), bin, "--list", dumpPath)
  1453. cmd.Stdout = io.Discard
  1454. var stderr bytes.Buffer
  1455. cmd.Stderr = &stderr
  1456. if cmd.Run() == nil {
  1457. return nil
  1458. }
  1459. return pgRestoreReadFailureError(strings.TrimSpace(stderr.String()), pgRestoreVersion(bin))
  1460. }
  1461. func pgRestoreReadFailureError(probeOutput, localVersion string) error {
  1462. m := pgUnsupportedDumpVersionPattern.FindStringSubmatch(probeOutput)
  1463. if m == nil {
  1464. return common.NewErrorf("pg_restore cannot read this dump file: %s", probeOutput)
  1465. }
  1466. if localVersion == "" {
  1467. localVersion = "unknown"
  1468. }
  1469. if major, known := pgArchiveVersionIntroducedIn[m[1]]; known {
  1470. return common.NewErrorf("This backup was created by pg_dump from PostgreSQL %d or newer, but the server's pg_restore is version %s and cannot read it; run 'x-ui pgclient %d' on the server (or upgrade the postgresql-client package to version %d or newer), then retry the import", major, localVersion, major, major)
  1471. }
  1472. return common.NewErrorf("This backup was created by a newer pg_dump than the server's pg_restore (version %s) can read; upgrade the postgresql-client package and retry the import", localVersion)
  1473. }
  1474. func pgRestoreVersion(bin string) string {
  1475. out, err := exec.CommandContext(context.Background(), bin, "--version").Output()
  1476. if err != nil {
  1477. return ""
  1478. }
  1479. return parsePgToolVersion(string(out))
  1480. }
  1481. func parsePgToolVersion(versionOutput string) string {
  1482. return pgToolVersionPattern.FindString(versionOutput)
  1483. }
  1484. const (
  1485. importKindUnknown = iota
  1486. importKindPgDump
  1487. importKindSQLiteDB
  1488. importKindSQLiteDump
  1489. )
  1490. // sniffImportKind classifies an uploaded restore file by its leading bytes:
  1491. // a pg_dump custom archive, a raw SQLite database, or a SQLite SQL text dump.
  1492. func sniffImportKind(header []byte) int {
  1493. if bytes.HasPrefix(header, []byte("PGDMP")) {
  1494. return importKindPgDump
  1495. }
  1496. if bytes.HasPrefix(header, []byte("SQLite format 3\x00")) {
  1497. return importKindSQLiteDB
  1498. }
  1499. text := bytes.TrimLeft(bytes.TrimPrefix(header, []byte("\xef\xbb\xbf")), " \t\r\n")
  1500. if bytes.HasPrefix(text, []byte("PRAGMA")) || bytes.HasPrefix(text, []byte("BEGIN TRANSACTION")) {
  1501. return importKindSQLiteDump
  1502. }
  1503. return importKindUnknown
  1504. }
  1505. func sniffUploadKind(file multipart.File) (int, error) {
  1506. header := make([]byte, 64)
  1507. n, err := file.ReadAt(header, 0)
  1508. if err != nil && !errors.Is(err, io.EOF) {
  1509. return importKindUnknown, err
  1510. }
  1511. if _, err := file.Seek(0, 0); err != nil {
  1512. return importKindUnknown, err
  1513. }
  1514. return sniffImportKind(header[:n]), nil
  1515. }
  1516. func (s *ServerService) importPostgresDB(file multipart.File) error {
  1517. kind, err := sniffUploadKind(file)
  1518. if err != nil {
  1519. return common.NewErrorf("Error reading uploaded file: %v", err)
  1520. }
  1521. switch kind {
  1522. case importKindPgDump:
  1523. return s.restorePostgresDump(file)
  1524. case importKindSQLiteDB:
  1525. return s.migrateSQLiteIntoPostgres(file, false)
  1526. case importKindSQLiteDump:
  1527. return s.migrateSQLiteIntoPostgres(file, true)
  1528. default:
  1529. return common.NewError("Invalid file: expected a PostgreSQL custom-format dump (.dump) from this panel's Back Up, a SQLite database (.db), or a SQLite migration dump")
  1530. }
  1531. }
  1532. func (s *ServerService) restorePostgresDump(file multipart.File) error {
  1533. bin, err := exec.LookPath("pg_restore")
  1534. if err != nil {
  1535. return common.NewError("pg_restore not found on the server; install the postgresql-client package to restore a PostgreSQL database")
  1536. }
  1537. env, dbname, err := pgConnEnv(config.GetDBDSN())
  1538. if err != nil {
  1539. return common.NewErrorf("invalid PostgreSQL DSN: %v", err)
  1540. }
  1541. tempFile, err := os.CreateTemp("", "x-ui-pg-restore-*.dump")
  1542. if err != nil {
  1543. return common.NewErrorf("Error creating temporary dump file: %v", err)
  1544. }
  1545. tempPath := tempFile.Name()
  1546. defer os.Remove(tempPath)
  1547. if _, err := io.Copy(tempFile, file); err != nil {
  1548. tempFile.Close()
  1549. return common.NewErrorf("Error saving dump: %v", err)
  1550. }
  1551. if err := tempFile.Close(); err != nil {
  1552. return common.NewErrorf("Error closing temporary dump file: %v", err)
  1553. }
  1554. if err := checkPgRestoreCanRead(bin, tempPath); err != nil {
  1555. return err
  1556. }
  1557. xrayStopped := true
  1558. defer func() {
  1559. if xrayStopped {
  1560. if errR := s.RestartXrayService(); errR != nil {
  1561. logger.Warningf("Failed to restart Xray after DB restore error: %v", errR)
  1562. }
  1563. }
  1564. }()
  1565. if errStop := s.StopXrayService(); errStop != nil {
  1566. logger.Warningf("Failed to stop Xray before DB restore: %v", errStop)
  1567. }
  1568. if errClose := database.CloseDB(); errClose != nil {
  1569. logger.Warningf("Failed to close existing DB before restore: %v", errClose)
  1570. }
  1571. cmd := exec.CommandContext(context.Background(), bin,
  1572. "--clean", "--if-exists", "--no-owner", "--no-privileges",
  1573. "--single-transaction", "--dbname", dbname, tempPath,
  1574. )
  1575. cmd.Env = env
  1576. var stderr bytes.Buffer
  1577. cmd.Stderr = &stderr
  1578. runErr := cmd.Run()
  1579. if errInit := database.InitDB(config.GetDBPath()); errInit != nil {
  1580. return common.NewErrorf("Restore finished but reopening the database failed: %v", errInit)
  1581. }
  1582. s.inboundService.MigrateDB()
  1583. if runErr != nil {
  1584. return common.NewErrorf("pg_restore failed (database left unchanged): %v: %s", runErr, strings.TrimSpace(stderr.String()))
  1585. }
  1586. xrayStopped = false
  1587. if err := s.RestartXrayService(); err != nil {
  1588. return common.NewErrorf("Restored DB but failed to start Xray: %v", err)
  1589. }
  1590. return nil
  1591. }
  1592. func (s *ServerService) migrateSQLiteIntoPostgres(file multipart.File, isSQLDump bool) error {
  1593. tempDir, err := os.MkdirTemp("", "x-ui-pg-migrate-*")
  1594. if err != nil {
  1595. return common.NewErrorf("Error creating temporary folder: %v", err)
  1596. }
  1597. defer os.RemoveAll(tempDir)
  1598. uploadPath := filepath.Join(tempDir, "upload.db")
  1599. if isSQLDump {
  1600. uploadPath = filepath.Join(tempDir, "upload.dump")
  1601. }
  1602. if err := saveUploadedFile(file, uploadPath); err != nil {
  1603. return common.NewErrorf("Error saving uploaded file: %v", err)
  1604. }
  1605. dbPath := uploadPath
  1606. if isSQLDump {
  1607. dbPath = filepath.Join(tempDir, "restored.db")
  1608. if err := database.RestoreSQLite(uploadPath, dbPath); err != nil {
  1609. return common.NewErrorf("Error rebuilding a SQLite database from the migration dump: %v", err)
  1610. }
  1611. }
  1612. if err := database.ValidateSQLiteDB(dbPath); err != nil {
  1613. return common.NewErrorf("Invalid or corrupt db file: %v", err)
  1614. }
  1615. if err := database.PrepareSQLiteForMigration(dbPath); err != nil {
  1616. return common.NewErrorf("This file cannot be imported: %v", err)
  1617. }
  1618. xrayStopped := true
  1619. defer func() {
  1620. if xrayStopped {
  1621. if errR := s.RestartXrayService(); errR != nil {
  1622. logger.Warningf("Failed to restart Xray after DB restore error: %v", errR)
  1623. }
  1624. }
  1625. }()
  1626. if errStop := s.StopXrayService(); errStop != nil {
  1627. logger.Warningf("Failed to stop Xray before DB restore: %v", errStop)
  1628. }
  1629. if errClose := database.CloseDB(); errClose != nil {
  1630. logger.Warningf("Failed to close existing DB before restore: %v", errClose)
  1631. }
  1632. migrateErr := database.MigrateData(dbPath, config.GetDBDSN())
  1633. if errInit := database.InitDB(config.GetDBPath()); errInit != nil {
  1634. return common.NewErrorf("Restore finished but reopening the database failed: %v", errInit)
  1635. }
  1636. s.inboundService.MigrateDB()
  1637. if migrateErr != nil {
  1638. return common.NewErrorf("Importing the SQLite data into PostgreSQL failed: %v; the import runs in a single transaction, so the database was left unchanged", migrateErr)
  1639. }
  1640. xrayStopped = false
  1641. if err := s.RestartXrayService(); err != nil {
  1642. return common.NewErrorf("Restored DB but failed to start Xray: %v", err)
  1643. }
  1644. return nil
  1645. }
  1646. func saveUploadedFile(file multipart.File, dstPath string) error {
  1647. dst, err := os.Create(dstPath)
  1648. if err != nil {
  1649. return err
  1650. }
  1651. if _, err := io.Copy(dst, file); err != nil {
  1652. dst.Close()
  1653. return err
  1654. }
  1655. return dst.Close()
  1656. }
  1657. func stageSQLiteUpload(file multipart.File, kind int, tempPath string) error {
  1658. if kind == importKindSQLiteDump {
  1659. dumpPath := tempPath + ".dump"
  1660. defer os.Remove(dumpPath)
  1661. if err := saveUploadedFile(file, dumpPath); err != nil {
  1662. return common.NewErrorf("Error saving migration dump: %v", err)
  1663. }
  1664. if err := database.RestoreSQLite(dumpPath, tempPath); err != nil {
  1665. return common.NewErrorf("Error rebuilding a SQLite database from the migration dump: %v", err)
  1666. }
  1667. return nil
  1668. }
  1669. if err := saveUploadedFile(file, tempPath); err != nil {
  1670. return common.NewErrorf("Error saving db: %v", err)
  1671. }
  1672. return nil
  1673. }
  1674. // IsValidGeofileName validates that the filename is safe for geofile operations.
  1675. // It checks for path traversal attempts and ensures the filename contains only safe characters.
  1676. func (s *ServerService) IsValidGeofileName(filename string) bool {
  1677. if filename == "" {
  1678. return false
  1679. }
  1680. // Check for path traversal attempts
  1681. if strings.Contains(filename, "..") {
  1682. return false
  1683. }
  1684. // Check for path separators (both forward and backward slash)
  1685. if strings.ContainsAny(filename, `/\`) {
  1686. return false
  1687. }
  1688. // Check for absolute path indicators
  1689. if filepath.IsAbs(filename) {
  1690. return false
  1691. }
  1692. // Additional security: only allow alphanumeric, dots, underscores, and hyphens
  1693. // This is stricter than the general filename regex
  1694. validGeofilePattern := `^[a-zA-Z0-9._-]+\.dat$`
  1695. matched, _ := regexp.MatchString(validGeofilePattern, filename)
  1696. return matched
  1697. }
  1698. func (s *ServerService) UpdateGeofile(fileName string) error {
  1699. type geofileEntry struct {
  1700. URL string
  1701. FileName string
  1702. }
  1703. geofileAllowlist := map[string]geofileEntry{
  1704. "geoip.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip.dat"},
  1705. "geosite.dat": {"https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite.dat"},
  1706. "geoip_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat", "geoip_IR.dat"},
  1707. "geosite_IR.dat": {"https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat", "geosite_IR.dat"},
  1708. "geoip_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat", "geoip_RU.dat"},
  1709. "geosite_RU.dat": {"https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat", "geosite_RU.dat"},
  1710. }
  1711. // Strict allowlist check to avoid writing uncontrolled files
  1712. if fileName != "" {
  1713. if _, ok := geofileAllowlist[fileName]; !ok {
  1714. return common.NewErrorf("Invalid geofile name: %q not in allowlist", fileName)
  1715. }
  1716. }
  1717. client := s.settingService.NewProxiedHTTPClient(0)
  1718. downloadFile := func(url, destPath string) error {
  1719. var req *http.Request
  1720. req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
  1721. if err != nil {
  1722. return common.NewErrorf("Failed to create HTTP request for %s: %v", url, err)
  1723. }
  1724. var localFileModTime time.Time
  1725. if fileInfo, err := os.Stat(destPath); err == nil {
  1726. localFileModTime = fileInfo.ModTime()
  1727. if !localFileModTime.IsZero() {
  1728. req.Header.Set("If-Modified-Since", localFileModTime.UTC().Format(http.TimeFormat))
  1729. }
  1730. }
  1731. resp, err := client.Do(req)
  1732. if err != nil {
  1733. return common.NewErrorf("Failed to download Geofile from %s: %v", url, err)
  1734. }
  1735. defer resp.Body.Close()
  1736. // Parse Last-Modified header from server
  1737. var serverModTime time.Time
  1738. serverModTimeStr := resp.Header.Get("Last-Modified")
  1739. if serverModTimeStr != "" {
  1740. parsedTime, err := time.Parse(http.TimeFormat, serverModTimeStr)
  1741. if err != nil {
  1742. logger.Warningf("Failed to parse Last-Modified header for %s: %v", url, err)
  1743. } else {
  1744. serverModTime = parsedTime
  1745. }
  1746. }
  1747. // Function to update local file's modification time
  1748. updateFileModTime := func() {
  1749. if !serverModTime.IsZero() {
  1750. if err := os.Chtimes(destPath, serverModTime, serverModTime); err != nil {
  1751. logger.Warningf("Failed to update modification time for %s: %v", destPath, err)
  1752. }
  1753. }
  1754. }
  1755. // Handle 304 Not Modified
  1756. if resp.StatusCode == http.StatusNotModified {
  1757. updateFileModTime()
  1758. return nil
  1759. }
  1760. if resp.StatusCode != http.StatusOK {
  1761. return common.NewErrorf("Failed to download Geofile from %s: received status code %d", url, resp.StatusCode)
  1762. }
  1763. file, err := os.Create(destPath)
  1764. if err != nil {
  1765. return common.NewErrorf("Failed to create Geofile %s: %v", destPath, err)
  1766. }
  1767. defer file.Close()
  1768. _, err = io.Copy(file, resp.Body)
  1769. if err != nil {
  1770. return common.NewErrorf("Failed to save Geofile %s: %v", destPath, err)
  1771. }
  1772. updateFileModTime()
  1773. return nil
  1774. }
  1775. var errorMessages []string
  1776. if fileName == "" {
  1777. // Download all geofiles
  1778. for _, entry := range geofileAllowlist {
  1779. destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
  1780. if err := downloadFile(entry.URL, destPath); err != nil {
  1781. errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
  1782. }
  1783. }
  1784. } else {
  1785. entry := geofileAllowlist[fileName]
  1786. destPath := filepath.Join(config.GetBinFolderPath(), entry.FileName)
  1787. if err := downloadFile(entry.URL, destPath); err != nil {
  1788. errorMessages = append(errorMessages, fmt.Sprintf("Error downloading Geofile '%s': %v", entry.FileName, err))
  1789. }
  1790. }
  1791. err := s.RestartXrayService()
  1792. if err != nil {
  1793. errorMessages = append(errorMessages, fmt.Sprintf("Updated Geofile '%s' but Failed to start Xray: %v", fileName, err))
  1794. }
  1795. if len(errorMessages) > 0 {
  1796. return common.NewErrorf("%s", strings.Join(errorMessages, "\r\n"))
  1797. }
  1798. return nil
  1799. }
  1800. func (s *ServerService) GetNewX25519Cert() (any, error) {
  1801. // Run the command
  1802. cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "x25519")
  1803. var out bytes.Buffer
  1804. cmd.Stdout = &out
  1805. err := cmd.Run()
  1806. if err != nil {
  1807. return nil, err
  1808. }
  1809. lines := strings.Split(out.String(), "\n")
  1810. privateKeyLine := strings.Split(lines[0], ":")
  1811. publicKeyLine := strings.Split(lines[1], ":")
  1812. privateKey := strings.TrimSpace(privateKeyLine[1])
  1813. publicKey := strings.TrimSpace(publicKeyLine[1])
  1814. keyPair := map[string]any{
  1815. "privateKey": privateKey,
  1816. "publicKey": publicKey,
  1817. }
  1818. return keyPair, nil
  1819. }
  1820. func (s *ServerService) GetNewmldsa65() (any, error) {
  1821. // Run the command
  1822. cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "mldsa65")
  1823. var out bytes.Buffer
  1824. cmd.Stdout = &out
  1825. err := cmd.Run()
  1826. if err != nil {
  1827. return nil, err
  1828. }
  1829. lines := strings.Split(out.String(), "\n")
  1830. SeedLine := strings.Split(lines[0], ":")
  1831. VerifyLine := strings.Split(lines[1], ":")
  1832. seed := strings.TrimSpace(SeedLine[1])
  1833. verify := strings.TrimSpace(VerifyLine[1])
  1834. keyPair := map[string]any{
  1835. "seed": seed,
  1836. "verify": verify,
  1837. }
  1838. return keyPair, nil
  1839. }
  1840. // GetCertHash parses a certificate (from a file path or inline PEM/DER content)
  1841. // and returns the hex-encoded SHA-256 over each certificate's raw DER — the
  1842. // value xray-core's pinnedPeerCertSha256 (pcs) expects. Lets the panel fill the
  1843. // pinned-cert field from the inbound's own certificate without the user
  1844. // computing the hash by hand.
  1845. func (s *ServerService) GetCertHash(certFile string, certContent string) ([]string, error) {
  1846. var certBytes []byte
  1847. if path := strings.TrimSpace(certFile); path != "" {
  1848. // Guard against path traversal: only hash certificate files the panel
  1849. // already references in its own configuration (an inbound's TLS
  1850. // certificateFile or the panel's own web cert). The path handed to
  1851. // os.ReadFile comes from that allow-list, never directly from the
  1852. // caller-supplied value.
  1853. known, ok := s.resolveKnownCertFile(path)
  1854. if !ok {
  1855. return nil, common.NewError("certificate file is not referenced by any inbound or panel setting")
  1856. }
  1857. b, err := os.ReadFile(known)
  1858. if err != nil {
  1859. return nil, err
  1860. }
  1861. certBytes = b
  1862. } else if strings.TrimSpace(certContent) != "" {
  1863. certBytes = []byte(certContent)
  1864. } else {
  1865. return nil, common.NewError("no certificate provided")
  1866. }
  1867. var certs []*x509.Certificate
  1868. if bytes.Contains(certBytes, []byte("BEGIN")) {
  1869. rest := certBytes
  1870. for {
  1871. block, remain := pem.Decode(rest)
  1872. if block == nil {
  1873. break
  1874. }
  1875. cert, err := x509.ParseCertificate(block.Bytes)
  1876. if err != nil {
  1877. return nil, common.NewError("unable to decode certificate: ", err)
  1878. }
  1879. certs = append(certs, cert)
  1880. rest = remain
  1881. }
  1882. } else {
  1883. parsed, err := x509.ParseCertificates(certBytes)
  1884. if err != nil {
  1885. return nil, common.NewError("unable to parse certificates: ", err)
  1886. }
  1887. certs = parsed
  1888. }
  1889. if len(certs) == 0 {
  1890. return nil, common.NewError("no certificates found")
  1891. }
  1892. hashes := make([]string, 0, len(certs))
  1893. for _, cert := range certs {
  1894. sum := sha256.Sum256(cert.Raw)
  1895. hashes = append(hashes, hex.EncodeToString(sum[:]))
  1896. }
  1897. return hashes, nil
  1898. }
  1899. // resolveKnownCertFile checks the caller-supplied certificate path against the
  1900. // set of certificate files the panel already references (inbound TLS configs
  1901. // plus the panel's own web cert) and, on a match, returns the path taken from
  1902. // that configuration — not the caller's value. This both confines reads to
  1903. // known certificates and breaks the user-input-to-filesystem taint flow.
  1904. func (s *ServerService) resolveKnownCertFile(certFile string) (string, bool) {
  1905. want := filepath.Clean(certFile)
  1906. for _, known := range s.knownCertFiles() {
  1907. if filepath.Clean(known) == want {
  1908. return known, true
  1909. }
  1910. }
  1911. return "", false
  1912. }
  1913. // knownCertFiles collects every certificate file path the panel legitimately
  1914. // references: the certificateFile of each inbound's TLS settings and the
  1915. // panel's own web TLS certificate.
  1916. func (s *ServerService) knownCertFiles() []string {
  1917. var files []string
  1918. if cert, err := s.settingService.GetCertFile(); err == nil {
  1919. if cert = strings.TrimSpace(cert); cert != "" {
  1920. files = append(files, cert)
  1921. }
  1922. }
  1923. if inbounds, err := s.inboundService.GetAllInbounds(); err == nil {
  1924. for _, inbound := range inbounds {
  1925. files = collectCertFiles(inbound.StreamSettings, files)
  1926. }
  1927. }
  1928. return files
  1929. }
  1930. // collectCertFiles walks a stream-settings JSON document and appends the value
  1931. // of every "certificateFile" field it finds (TLS settings may nest them under
  1932. // several keys depending on the security type).
  1933. func collectCertFiles(streamSettings string, out []string) []string {
  1934. streamSettings = strings.TrimSpace(streamSettings)
  1935. if streamSettings == "" {
  1936. return out
  1937. }
  1938. var parsed any
  1939. if err := json.Unmarshal([]byte(streamSettings), &parsed); err != nil {
  1940. return out
  1941. }
  1942. return walkCertFiles(parsed, out)
  1943. }
  1944. func walkCertFiles(node any, out []string) []string {
  1945. switch v := node.(type) {
  1946. case map[string]any:
  1947. for key, val := range v {
  1948. if key == "certificateFile" {
  1949. if path, ok := val.(string); ok {
  1950. if path = strings.TrimSpace(path); path != "" {
  1951. out = append(out, path)
  1952. }
  1953. }
  1954. }
  1955. out = walkCertFiles(val, out)
  1956. }
  1957. case []any:
  1958. for _, item := range v {
  1959. out = walkCertFiles(item, out)
  1960. }
  1961. }
  1962. return out
  1963. }
  1964. // GetRemoteCertHash opens a uTLS (Chrome fingerprint) handshake to a remote
  1965. // endpoint and returns the hex-encoded SHA-256 of its leaf certificate — the
  1966. // value to put in pinnedPeerCertSha256 (pcs) when pinning a server whose
  1967. // certificate file you don't hold (a CDN front, a REALITY dest, an external
  1968. // proxy). A native handshake replaces the old `xray tls ping` subprocess so the
  1969. // real dial/handshake failure (connection refused, timeout, …) surfaces
  1970. // verbatim. `server` may be host or host:port; the port defaults to 443.
  1971. func (s *ServerService) GetRemoteCertHash(server string) ([]string, error) {
  1972. server = strings.TrimSpace(server)
  1973. if server == "" {
  1974. return nil, common.NewError("no server provided")
  1975. }
  1976. host, port := server, "443"
  1977. if h, p, err := stdnet.SplitHostPort(server); err == nil {
  1978. host, port = h, p
  1979. }
  1980. dialer := stdnet.Dialer{Timeout: 10 * time.Second}
  1981. tcpConn, err := dialer.Dial("tcp", stdnet.JoinHostPort(host, port))
  1982. if err != nil {
  1983. return nil, common.NewErrorf("failed to dial %s: %s", stdnet.JoinHostPort(host, port), err)
  1984. }
  1985. defer tcpConn.Close()
  1986. _ = tcpConn.SetDeadline(time.Now().Add(15 * time.Second))
  1987. tlsConn := utls.UClient(tcpConn, &utls.Config{
  1988. ServerName: host,
  1989. InsecureSkipVerify: true,
  1990. NextProtos: []string{"h2", "http/1.1"},
  1991. }, utls.HelloChrome_Auto)
  1992. defer tlsConn.Close()
  1993. if err := tlsConn.Handshake(); err != nil {
  1994. return nil, common.NewErrorf("tls handshake with %s failed: %s", host, err)
  1995. }
  1996. certs := tlsConn.ConnectionState().PeerCertificates
  1997. if len(certs) == 0 {
  1998. return nil, common.NewError("no certificate returned by ", host)
  1999. }
  2000. // PeerCertificates[0] is always the leaf the connection verifies against —
  2001. // robust for IP-only self-signed certs that carry no DNS SANs.
  2002. sum := sha256.Sum256(certs[0].Raw)
  2003. return []string{hex.EncodeToString(sum[:])}, nil
  2004. }
  2005. func (s *ServerService) GetNewEchCert(sni string) (any, error) {
  2006. // Run the command
  2007. cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)
  2008. var out bytes.Buffer
  2009. cmd.Stdout = &out
  2010. err := cmd.Run()
  2011. if err != nil {
  2012. return nil, err
  2013. }
  2014. lines := strings.Split(out.String(), "\n")
  2015. if len(lines) < 4 {
  2016. return nil, common.NewError("invalid ech cert")
  2017. }
  2018. configList := lines[1]
  2019. serverKeys := lines[3]
  2020. return map[string]any{
  2021. "echServerKeys": serverKeys,
  2022. "echConfigList": configList,
  2023. }, nil
  2024. }
  2025. func (s *ServerService) GetNewVlessEnc() (any, error) {
  2026. cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "vlessenc")
  2027. var out bytes.Buffer
  2028. cmd.Stdout = &out
  2029. if err := cmd.Run(); err != nil {
  2030. return nil, err
  2031. }
  2032. auths := parseVlessEncAuths(out.String())
  2033. auths = append(auths, deriveVlessEncModes(auths)...)
  2034. return map[string]any{
  2035. "auths": auths,
  2036. }, nil
  2037. }
  2038. func deriveVlessEncModes(auths []map[string]string) []map[string]string {
  2039. var extra []map[string]string
  2040. for _, a := range auths {
  2041. for _, mode := range []string{"xorpub", "random"} {
  2042. dec := strings.Replace(a["decryption"], ".native.", "."+mode+".", 1)
  2043. enc := strings.Replace(a["encryption"], ".native.", "."+mode+".", 1)
  2044. if dec == a["decryption"] && enc == a["encryption"] {
  2045. continue
  2046. }
  2047. extra = append(extra, map[string]string{
  2048. "id": a["id"] + "_" + mode,
  2049. "label": a["label"] + " (" + mode + ")",
  2050. "decryption": dec,
  2051. "encryption": enc,
  2052. })
  2053. }
  2054. }
  2055. return extra
  2056. }
  2057. func parseVlessEncAuths(output string) []map[string]string {
  2058. lines := strings.Split(output, "\n")
  2059. var auths []map[string]string
  2060. var current map[string]string
  2061. for _, line := range lines {
  2062. line = strings.TrimSpace(line)
  2063. if strings.HasPrefix(line, "Authentication:") {
  2064. if current != nil {
  2065. auths = append(auths, current)
  2066. }
  2067. label := strings.TrimSpace(strings.TrimPrefix(line, "Authentication:"))
  2068. current = map[string]string{
  2069. "id": vlessEncAuthID(label),
  2070. "label": label,
  2071. }
  2072. } else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
  2073. parts := strings.SplitN(line, ":", 2)
  2074. if len(parts) == 2 && current != nil {
  2075. key := strings.Trim(parts[0], `" `)
  2076. val := strings.TrimSpace(parts[1])
  2077. val = strings.TrimSuffix(val, ",")
  2078. val = strings.Trim(val, `" `)
  2079. current[key] = val
  2080. }
  2081. }
  2082. }
  2083. if current != nil {
  2084. auths = append(auths, current)
  2085. }
  2086. return auths
  2087. }
  2088. func vlessEncAuthID(label string) string {
  2089. normalized := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(label))
  2090. switch {
  2091. case strings.Contains(normalized, "mlkem768"):
  2092. return "mlkem768"
  2093. case strings.Contains(normalized, "x25519"):
  2094. return "x25519"
  2095. default:
  2096. return normalized
  2097. }
  2098. }
  2099. func (s *ServerService) GetNewUUID() (map[string]string, error) {
  2100. newUUID, err := uuid.NewRandom()
  2101. if err != nil {
  2102. return nil, fmt.Errorf("failed to generate UUID: %w", err)
  2103. }
  2104. return map[string]string{
  2105. "uuid": newUUID.String(),
  2106. }, nil
  2107. }
  2108. func (s *ServerService) GetNewmlkem768() (any, error) {
  2109. // Run the command
  2110. cmd := exec.CommandContext(context.Background(), xray.GetBinaryPath(), "mlkem768")
  2111. var out bytes.Buffer
  2112. cmd.Stdout = &out
  2113. err := cmd.Run()
  2114. if err != nil {
  2115. return nil, err
  2116. }
  2117. lines := strings.Split(out.String(), "\n")
  2118. SeedLine := strings.Split(lines[0], ":")
  2119. ClientLine := strings.Split(lines[1], ":")
  2120. seed := strings.TrimSpace(SeedLine[1])
  2121. client := strings.TrimSpace(ClientLine[1])
  2122. keyPair := map[string]any{
  2123. "seed": seed,
  2124. "client": client,
  2125. }
  2126. return keyPair, nil
  2127. }