server.go 62 KB

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