server.go 80 KB

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