server.go 90 KB

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