1
0

server.go 74 KB

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