server.go 88 KB

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