server.go 87 KB

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