server.go 81 KB

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