server.go 89 KB

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