1
0

server.go 90 KB

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